mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2024-12-29 14:30:46 +08:00
deploy: cb23d19dba
This commit is contained in:
parent
266cceb3e4
commit
0919dc53c3
@ -166,7 +166,7 @@ f(x); //用一个int类型的变量调用f
|
||||
<p><code>T</code>被推导为<code>int</code>,<code>ParamType</code>却被推导为<code>const int&</code></p>
|
||||
<p>我们可能很自然的期望<code>T</code>和传递进函数的实参是相同的类型,也就是,<code>T</code>为<code>expr</code>的类型。在上面的例子中,事实就是那样:<code>x</code>是<code>int</code>,<code>T</code>被推导为<code>int</code>。但有时情况并非总是如此,<code>T</code>的类型推导不仅取决于<code>expr</code>的类型,也取决于<code>ParamType</code>的类型。这里有三种情况:</p>
|
||||
<ul>
|
||||
<li><code>ParamType</code>是一个指针或引用,但不是通用引用(关于通用引用请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>。在这里你只需要知道它存在,而且不同于左值引用和右值引用)</li>
|
||||
<li><code>ParamType</code>是一个指针或引用,但不是通用引用(关于通用引用请参见<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>。在这里你只需要知道它存在,而且不同于左值引用和右值引用)</li>
|
||||
<li><code>ParamType</code>一个通用引用</li>
|
||||
<li><code>ParamType</code>既不是指针也不是引用</li>
|
||||
</ul>
|
||||
@ -224,7 +224,7 @@ f(px); //T是const int,param的类型是const int*
|
||||
</code></pre>
|
||||
<p>到现在为止,你会发现你自己打哈欠犯困,因为C++的类型推导规则对引用和指针形参如此自然,书面形式来看这些非常枯燥。所有事情都那么理所当然!那正是在类型推导系统中你所想要的。</p>
|
||||
<h3 id="情景二paramtype是一个通用引用"><a class="header" href="#情景二paramtype是一个通用引用">情景二:<code>ParamType</code>是一个通用引用</a></h3>
|
||||
<p>模板使用通用引用形参的话,那事情就不那么明显了。这样的形参被声明为像右值引用一样(也就是,在函数模板中假设有一个类型形参<code>T</code>,那么通用引用声明形式就是<code>T&&</code>),它们的行为在传入左值实参时大不相同。完整的叙述请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>,在这有些最必要的你还是需要知道:</p>
|
||||
<p>模板使用通用引用形参的话,那事情就不那么明显了。这样的形参被声明为像右值引用一样(也就是,在函数模板中假设有一个类型形参<code>T</code>,那么通用引用声明形式就是<code>T&&</code>),它们的行为在传入左值实参时大不相同。完整的叙述请参见<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>,在这有些最必要的你还是需要知道:</p>
|
||||
<ul>
|
||||
<li>如果<code>expr</code>是左值,<code>T</code>和<code>ParamType</code>都会被推导为左值引用。这非常不寻常,第一,这是模板类型推导中唯一一种<code>T</code>被推导为引用的情况。第二,虽然<code>ParamType</code>被声明为右值引用类型,但是最后推导的结果是左值引用。</li>
|
||||
<li>如果<code>expr</code>是右值,就使用正常的(也就是<strong>情景一</strong>)推导规则</li>
|
||||
@ -249,7 +249,7 @@ f(rx); //rx是左值,所以T是const int&,
|
||||
f(27); //27是右值,所以T是int,
|
||||
//param类型就是int&&
|
||||
</code></pre>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>详细解释了为什么这些例子是像这样发生的。这里关键在于通用引用的类型推导规则是不同于普通的左值或者右值引用的。尤其是,当通用引用被使用时,类型推导会区分左值实参和右值实参,但是对非通用引用时不会区分。</p>
|
||||
<p><a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>详细解释了为什么这些例子是像这样发生的。这里关键在于通用引用的类型推导规则是不同于普通的左值或者右值引用的。尤其是,当通用引用被使用时,类型推导会区分左值实参和右值实参,但是对非通用引用时不会区分。</p>
|
||||
<h3 id="情景三paramtype既不是指针也不是引用"><a class="header" href="#情景三paramtype既不是指针也不是引用">情景三:<code>ParamType</code>既不是指针也不是引用</a></h3>
|
||||
<p>当<code>ParamType</code>既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
@ -258,7 +258,7 @@ void f(T param); //以传值的方式处理param
|
||||
<p>这意味着无论传递什么<code>param</code>都会成为它的一份拷贝——一个完整的新对象。事实上<code>param</code>成为一个新对象这一行为会影响<code>T</code>如何从<code>expr</code>中推导出结果。</p>
|
||||
<ol>
|
||||
<li>和之前一样,如果<code>expr</code>的类型是一个引用,忽略这个引用部分</li>
|
||||
<li>如果忽略<code>expr</code>的引用性(reference-ness)之后,<code>expr</code>是一个<code>const</code>,那就再忽略<code>const</code>。如果它是<code>volatile</code>,也忽略<code>volatile</code>(<code>volatile</code>对象不常见,它通常用于驱动程序的开发中。关于<code>volatile</code>的细节请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md">Item40</a>)</li>
|
||||
<li>如果忽略<code>expr</code>的引用性(reference-ness)之后,<code>expr</code>是一个<code>const</code>,那就再忽略<code>const</code>。如果它是<code>volatile</code>,也忽略<code>volatile</code>(<code>volatile</code>对象不常见,它通常用于驱动程序的开发中。关于<code>volatile</code>的细节请参见<a href="../7.TheConcurrencyAPI/item40.html">Item40</a>)</li>
|
||||
</ol>
|
||||
<p>因此</p>
|
||||
<pre><code class="language-cpp">int x=27; //如之前一样
|
||||
@ -320,7 +320,7 @@ constexpr std::size_t arraySize(T (&)[N]) noexcept //constexpr
|
||||
return N; //的信息
|
||||
} //请看下面
|
||||
</code></pre>
|
||||
<p>在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item15.md">Item15</a>提到将一个函数声明为<code>constexpr</code>使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组,然后第二个数组可以使用第一个数组的大小作为它的大小,就像这样:</p>
|
||||
<p>在<a href="../3.MovingToModernCpp/item15.html">Item15</a>提到将一个函数声明为<code>constexpr</code>使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组,然后第二个数组可以使用第一个数组的大小作为它的大小,就像这样:</p>
|
||||
<pre><code class="language-cpp">int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; //keyVals有七个元素
|
||||
|
||||
int mappedVals[arraySize(keyVals)]; //mappedVals也有七个
|
||||
@ -328,7 +328,7 @@ int mappedVals[arraySize(keyVals)]; //mappedVals也有七个
|
||||
<p>当然作为一个现代C++程序员,你自然应该想到使用<code>std::array</code>而不是内置的数组:</p>
|
||||
<pre><code class="language-cpp">std::array<int, arraySize(keyVals)> mappedVals; //mappedVals的大小为7
|
||||
</code></pre>
|
||||
<p>至于<code>arraySize</code>被声明为<code>noexcept</code>,会使得编译器生成更好的代码,具体的细节请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md">Item14</a>。</p>
|
||||
<p>至于<code>arraySize</code>被声明为<code>noexcept</code>,会使得编译器生成更好的代码,具体的细节请参见<a href="../3.MovingToModernCpp/item14.html">Item14</a>。</p>
|
||||
<h3 id="函数实参"><a class="header" href="#函数实参">函数实参</a></h3>
|
||||
<p>在C++中不只是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组类型推导的全部讨论都可以应用到函数类型推导和退化为函数指针上来。结果是:</p>
|
||||
<pre><code class="language-cpp">void someFunc(int, double); //someFunc是一个函数,
|
||||
@ -346,7 +346,7 @@ f2(someFunc); //param被推导为指向函数的引用,
|
||||
//类型是void(&)(int, double)
|
||||
</code></pre>
|
||||
<p>这个实际上没有什么不同,但是如果你知道数组退化为指针,你也会知道函数退化为指针。</p>
|
||||
<p>这里你需要知道:<code>auto</code>依赖于模板类型推导。正如我在开始谈论的,在大多数情况下它们的行为很直接。在通用引用中对于左值的特殊处理使得本来很直接的行为变得有些污点,然而,数组和函数退化为指针把这团水搅得更浑浊。有时你只需要编译器告诉你推导出的类型是什么。这种情况下,翻到<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item4.md">item4</a>,它会告诉你如何让编译器这么做。</p>
|
||||
<p>这里你需要知道:<code>auto</code>依赖于模板类型推导。正如我在开始谈论的,在大多数情况下它们的行为很直接。在通用引用中对于左值的特殊处理使得本来很直接的行为变得有些污点,然而,数组和函数退化为指针把这团水搅得更浑浊。有时你只需要编译器告诉你推导出的类型是什么。这种情况下,翻到<a href="../1.DeducingTypes/item4.html">item4</a>,它会告诉你如何让编译器这么做。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略</li>
|
||||
|
@ -141,9 +141,9 @@
|
||||
<main>
|
||||
<h2 id="条款二理解auto类型推导"><a class="header" href="#条款二理解auto类型推导">条款二:理解<code>auto</code>类型推导</a></h2>
|
||||
<p><strong>Item 2: Understand <code>auto</code> type deduction</strong></p>
|
||||
<p>如果你已经读过<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>的模板类型推导,那么你几乎已经知道了<code>auto</code>类型推导的大部分内容,至于为什么不是全部是因为这里有一个<code>auto</code>不同于模板类型推导的例外。但这怎么可能?模板类型推导包括模板,函数,形参,但<code>auto</code>不处理这些东西啊。</p>
|
||||
<p>如果你已经读过<a href="../1.DeducingTypes/item1.html">Item1</a>的模板类型推导,那么你几乎已经知道了<code>auto</code>类型推导的大部分内容,至于为什么不是全部是因为这里有一个<code>auto</code>不同于模板类型推导的例外。但这怎么可能?模板类型推导包括模板,函数,形参,但<code>auto</code>不处理这些东西啊。</p>
|
||||
<p>你是对的,但没关系。<code>auto</code>类型推导和模板类型推导有一个直接的映射关系。它们之间可以通过一个非常规范非常系统化的转换流程来转换彼此。</p>
|
||||
<p>在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item1</a>中,模板类型推导使用下面这个函数模板</p>
|
||||
<p>在<a href="../1.DeducingTypes/item2.html">Item1</a>中,模板类型推导使用下面这个函数模板</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void f(ParmaType param);
|
||||
</code></pre>
|
||||
@ -180,7 +180,7 @@ func_for_rx(x); //概念化调用:
|
||||
//param的推导类型是rx的类型
|
||||
</code></pre>
|
||||
<p>正如我说的,<code>auto</code>类型推导除了一个例外(我们很快就会讨论),其他情况都和模板类型推导一样。</p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>基于<code>ParamType</code>——在函数模板中<code>param</code>的类型说明符——的不同特征,把模板类型推导分成三个部分来讨论。在使用<code>auto</code>作为类型说明符的变量声明中,类型说明符代替了<code>ParamType</code>,因此Item1描述的三个情景稍作修改就能适用于auto:</p>
|
||||
<p><a href="../1.DeducingTypes/item1.html">Item1</a>基于<code>ParamType</code>——在函数模板中<code>param</code>的类型说明符——的不同特征,把模板类型推导分成三个部分来讨论。在使用<code>auto</code>作为类型说明符的变量声明中,类型说明符代替了<code>ParamType</code>,因此Item1描述的三个情景稍作修改就能适用于auto:</p>
|
||||
<ul>
|
||||
<li>情景一:类型说明符是一个指针或引用但不是通用引用</li>
|
||||
<li>情景二:类型说明符一个通用引用</li>
|
||||
@ -199,7 +199,7 @@ auto&& uref2 = cx; //cx是const int左值,
|
||||
auto&& uref3 = 27; //27是int右值,
|
||||
//所以uref3类型为int&&
|
||||
</code></pre>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>讨论并总结了对于non-reference类型说明符,数组和函数名如何退化为指针。那些内容也同样适用于<code>auto</code>类型推导:</p>
|
||||
<p><a href="../1.DeducingTypes/item1.html">Item1</a>讨论并总结了对于non-reference类型说明符,数组和函数名如何退化为指针。那些内容也同样适用于<code>auto</code>类型推导:</p>
|
||||
<pre><code class="language-cpp">const char name[] = //name的类型是const char[13]
|
||||
"R. N. Briggs";
|
||||
|
||||
@ -223,7 +223,7 @@ int x2(27);
|
||||
int x4{ 27 };
|
||||
</code></pre>
|
||||
<p>总之,这四种不同的语法只会产生一个相同的结果:变量类型为<code>int</code>值为27</p>
|
||||
<p>但是<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/2.Auto/item5.md">Item5</a>解释了使用<code>auto</code>说明符代替指定类型说明符的好处,所以我们应该很乐意把上面声明中的<code>int</code>替换为<code>auto</code>,我们会得到这样的代码:</p>
|
||||
<p>但是<a href="../2.Auto/item5.html">Item5</a>解释了使用<code>auto</code>说明符代替指定类型说明符的好处,所以我们应该很乐意把上面声明中的<code>int</code>替换为<code>auto</code>,我们会得到这样的代码:</p>
|
||||
<pre><code class="language-cpp">auto x1 = 27;
|
||||
auto x2(27);
|
||||
auto x3 = { 27 };
|
||||
@ -256,8 +256,8 @@ f({ 11, 23, 9 }); //T被推导为int,initList的类型为
|
||||
//std::initializer_list<int>
|
||||
</code></pre>
|
||||
<p>因此<code>auto</code>类型推导和模板类型推导的真正区别在于,<code>auto</code>类型推导假定花括号表示<code>std::initializer_list</code>而模板类型推导不会这样(确切的说是不知道怎么办)。</p>
|
||||
<p>你可能想知道为什么<code>auto</code>类型推导和模板类型推导对于花括号有不同的处理方式。我也想知道。哎,我至今没找到一个令人信服的解释。但是规则就是规则,这意味着你必须记住如果你使用<code>auto</code>声明一个变量,并用花括号进行初始化,<code>auto</code>类型推导总会得出<code>std::initializer_list</code>的结果。如果你使用**uniform initialization(花括号的方式进行初始化)**用得很爽你就得记住这个例外以免犯错,在C++11编程中一个典型的错误就是偶然使用了<code>std::initializer_list<T></code>类型的变量,这个陷阱也导致了很多C++程序员抛弃花括号初始化,只有不得不使用的时候再做考虑。(在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item7.md">Item7</a>讨论了必须使用时该怎么做)</p>
|
||||
<p>对于C++11故事已经说完了。但是对于C++14故事还在继续,C++14允许<code>auto</code>用于函数返回值并会被推导(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>),而且C++14的<em>lambda</em>函数也允许在形参声明中使用<code>auto</code>。但是在这些情况下<code>auto</code>实际上使用<strong>模板类型推导</strong>的那一套规则在工作,而不是<code>auto</code>类型推导,所以说下面这样的代码不会通过编译:</p>
|
||||
<p>你可能想知道为什么<code>auto</code>类型推导和模板类型推导对于花括号有不同的处理方式。我也想知道。哎,我至今没找到一个令人信服的解释。但是规则就是规则,这意味着你必须记住如果你使用<code>auto</code>声明一个变量,并用花括号进行初始化,<code>auto</code>类型推导总会得出<code>std::initializer_list</code>的结果。如果你使用**uniform initialization(花括号的方式进行初始化)**用得很爽你就得记住这个例外以免犯错,在C++11编程中一个典型的错误就是偶然使用了<code>std::initializer_list<T></code>类型的变量,这个陷阱也导致了很多C++程序员抛弃花括号初始化,只有不得不使用的时候再做考虑。(在<a href="../3.MovingToModernCpp/item7.html">Item7</a>讨论了必须使用时该怎么做)</p>
|
||||
<p>对于C++11故事已经说完了。但是对于C++14故事还在继续,C++14允许<code>auto</code>用于函数返回值并会被推导(参见<a href="../1.DeducingTypes/item3.html">Item3</a>),而且C++14的<em>lambda</em>函数也允许在形参声明中使用<code>auto</code>。但是在这些情况下<code>auto</code>实际上使用<strong>模板类型推导</strong>的那一套规则在工作,而不是<code>auto</code>类型推导,所以说下面这样的代码不会通过编译:</p>
|
||||
<pre><code class="language-cpp">auto createInitList()
|
||||
{
|
||||
return { 1, 2, 3 }; //错误!不能推导{ 1, 2, 3 }的类型
|
||||
|
@ -142,7 +142,7 @@
|
||||
<h2 id="条款三理解decltype"><a class="header" href="#条款三理解decltype">条款三:理解<code>decltype</code></a></h2>
|
||||
<p><strong>Item 3: Understand decltype</strong></p>
|
||||
<p><code>decltype</code>是一个奇怪的东西。给它一个名字或者表达式<code>decltype</code>就会告诉你这个名字或者表达式的类型。通常,它会精确的告诉你你想要的结果。但有时候它得出的结果也会让你挠头半天,最后只能求助网上问答或参考资料寻求启示。</p>
|
||||
<p>我们将从一个简单的情况开始,没有任何令人惊讶的情况。相比模板类型推导和<code>auto</code>类型推导(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>和<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>),<code>decltype</code>只是简单的返回名字或者表达式的类型:</p>
|
||||
<p>我们将从一个简单的情况开始,没有任何令人惊讶的情况。相比模板类型推导和<code>auto</code>类型推导(参见<a href="../1.DeducingTypes/item1.html">Item1</a>和<a href="../1.DeducingTypes/item2.html">Item2</a>),<code>decltype</code>只是简单的返回名字或者表达式的类型:</p>
|
||||
<pre><code class="language-cpp">const int i = 0; //decltype(i)是const int
|
||||
|
||||
bool f(const Widget& w); //decltype(w)是const Widget&
|
||||
@ -170,7 +170,7 @@ if (v[0] == 0)… //decltype(v[0])是int&
|
||||
</code></pre>
|
||||
<p>看见了吧?没有任何奇怪的东西。</p>
|
||||
<p>在C++11中,<code>decltype</code>最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。举个例子,假定我们写一个函数,一个形参为容器,一个形参为索引值,这个函数支持使用方括号的方式(也就是使用“<code>[]</code>”)访问容器中指定索引值的数据,然后在返回索引操作的结果前执行认证用户操作。函数的返回类型应该和索引操作返回的类型相同。</p>
|
||||
<p>对一个<code>T</code>类型的容器使用<code>operator[]</code> 通常会返回一个<code>T&</code>对象,比如<code>std::deque</code>就是这样。但是<code>std::vector</code>有一个例外,对于<code>std::vector<bool></code>,<code>operator[]</code>不会返回<code>bool&</code>,它会返回一个全新的对象(译注:MSVC的STL实现中返回的是<code>std::_Vb_reference<std::_Wrap_alloc<std::allocator<unsigned int>>></code>对象)。关于这个问题的详细讨论请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/2.Auto/item6.md">Item6</a>,这里重要的是我们可以看到对一个容器进行<code>operator[]</code>操作返回的类型取决于容器本身。</p>
|
||||
<p>对一个<code>T</code>类型的容器使用<code>operator[]</code> 通常会返回一个<code>T&</code>对象,比如<code>std::deque</code>就是这样。但是<code>std::vector</code>有一个例外,对于<code>std::vector<bool></code>,<code>operator[]</code>不会返回<code>bool&</code>,它会返回一个全新的对象(译注:MSVC的STL实现中返回的是<code>std::_Vb_reference<std::_Wrap_alloc<std::allocator<unsigned int>>></code>对象)。关于这个问题的详细讨论请参见<a href="../2.Auto/item6.html">Item6</a>,这里重要的是我们可以看到对一个容器进行<code>operator[]</code>操作返回的类型取决于容器本身。</p>
|
||||
<p>使用<code>decltype</code>使得我们很容易去实现它,这是我们写的第一个版本,使用<code>decltype</code>计算返回类型,这个模板需要改良,我们把这个推迟到后面:</p>
|
||||
<pre><code class="language-cpp">template<typename Container, typename Index> //可以工作,
|
||||
auto authAndAccess(Container& c, Index i) //但是需要改良
|
||||
@ -190,7 +190,7 @@ auto authAndAccess(Container& c, Index i) //不那么正确
|
||||
return c[i]; //从c[i]中推导返回类型
|
||||
}
|
||||
</code></pre>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>解释了函数返回类型中使用<code>auto</code>,编译器实际上是使用的模板类型推导的那套规则。如果那样的话这里就会有一些问题。正如我们之前讨论的,<code>operator[]</code>对于大多数<code>T</code>类型的容器会返回一个<code>T&</code>,但是<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>解释了在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。基于这样的规则,考虑它会对下面用户的代码有哪些影响:</p>
|
||||
<p><a href="../1.DeducingTypes/item2.html">Item2</a>解释了函数返回类型中使用<code>auto</code>,编译器实际上是使用的模板类型推导的那套规则。如果那样的话这里就会有一些问题。正如我们之前讨论的,<code>operator[]</code>对于大多数<code>T</code>类型的容器会返回一个<code>T&</code>,但是<a href="../1.DeducingTypes/item1.html">Item1</a>解释了在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。基于这样的规则,考虑它会对下面用户的代码有哪些影响:</p>
|
||||
<pre><code class="language-cpp">std::deque<int> d;
|
||||
…
|
||||
authAndAccess(d, 5) = 10; //认证用户,返回d[5],
|
||||
@ -230,12 +230,12 @@ decltype(auto) authAndAccess(Container& c, Index i);
|
||||
//从makeStringDeque中获得第五个元素的拷贝并返回
|
||||
auto s = authAndAccess(makeStringDeque(), 5);
|
||||
</code></pre>
|
||||
<p>要想支持这样使用<code>authAndAccess</code>我们就得修改一下当前的声明使得它支持左值和右值。重载是一个不错的选择(一个函数重载声明为左值引用,另一个声明为右值引用),但是我们就不得不维护两个重载函数。另一个方法是使<code>authAndAccess</code>的引用可以绑定左值和右值,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>解释了那正是通用引用能做的,所以我们这里可以使用通用引用进行声明:</p>
|
||||
<p>要想支持这样使用<code>authAndAccess</code>我们就得修改一下当前的声明使得它支持左值和右值。重载是一个不错的选择(一个函数重载声明为左值引用,另一个声明为右值引用),但是我们就不得不维护两个重载函数。另一个方法是使<code>authAndAccess</code>的引用可以绑定左值和右值,<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>解释了那正是通用引用能做的,所以我们这里可以使用通用引用进行声明:</p>
|
||||
<pre><code class="language-cpp">template<typename Containter, typename Index> //现在c是通用引用
|
||||
decltype(auto) authAndAccess(Container&& c, Index i);
|
||||
</code></pre>
|
||||
<p>在这个模板中,我们不知道我们操纵的容器的类型是什么,那意味着我们同样不知道它使用的索引对象(index objects)的类型,对一个未知类型的对象使用传值通常会造成不必要的拷贝,对程序的性能有极大的影响,还会造成对象切片行为(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md">item41</a>),以及给同事落下笑柄。但是就容器索引来说,我们遵照标准模板库对于索引的处理是有理由的(比如<code>std::string</code>,<code>std::vector</code>和<code>std::deque</code>的<code>operator[]</code>),所以我们坚持传值调用。</p>
|
||||
<p>然而,我们还需要更新一下模板的实现,让它能听从<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>的告诫应用<code>std::forward</code>实现通用引用:</p>
|
||||
<p>在这个模板中,我们不知道我们操纵的容器的类型是什么,那意味着我们同样不知道它使用的索引对象(index objects)的类型,对一个未知类型的对象使用传值通常会造成不必要的拷贝,对程序的性能有极大的影响,还会造成对象切片行为(参见<a href="../8.Tweaks/item41.html">item41</a>),以及给同事落下笑柄。但是就容器索引来说,我们遵照标准模板库对于索引的处理是有理由的(比如<code>std::string</code>,<code>std::vector</code>和<code>std::deque</code>的<code>operator[]</code>),所以我们坚持传值调用。</p>
|
||||
<p>然而,我们还需要更新一下模板的实现,让它能听从<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>的告诫应用<code>std::forward</code>实现通用引用:</p>
|
||||
<pre><code class="language-cpp">template<typename Container, typename Index> //最终的C++14版本
|
||||
decltype(auto)
|
||||
authAndAccess(Container&& c, Index i)
|
||||
@ -276,7 +276,7 @@ decltype(auto) f2()
|
||||
}
|
||||
</code></pre>
|
||||
<p>注意不仅<code>f2</code>的返回类型不同于<code>f1</code>,而且它还引用了一个局部变量!这样的代码将会把你送上未定义行为的特快列车,一辆你绝对不想上第二次的车。</p>
|
||||
<p>当使用<code>decltype(auto)</code>的时候一定要加倍的小心,在表达式中看起来无足轻重的细节将会影响到<code>decltype(auto)</code>的推导结果。为了确认类型推导是否产出了你想要的结果,请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item4.md">Item4</a>描述的那些技术。</p>
|
||||
<p>当使用<code>decltype(auto)</code>的时候一定要加倍的小心,在表达式中看起来无足轻重的细节将会影响到<code>decltype(auto)</code>的推导结果。为了确认类型推导是否产出了你想要的结果,请参见<a href="../1.DeducingTypes/item4.html">Item4</a>描述的那些技术。</p>
|
||||
<p>同时你也不应该忽略<code>decltype</code>这块大蛋糕。没错,<code>decltype</code>(单独使用或者与<code>auto</code>一起用)可能会偶尔产生一些令人惊讶的结果,但那毕竟是少数情况。通常,<code>decltype</code>都会产生你想要的结果,尤其是当你对一个名字使用<code>decltype</code>时,因为在这种情况下,<code>decltype</code>只是做一件本分之事:它产出名字的声明类型。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
|
@ -215,7 +215,7 @@ param = PK6Widget
|
||||
param = class Widget const *
|
||||
</code></pre>
|
||||
<p>这三个独立的编译器产生了相同的信息并表示信息非常准确,当然看起来不是那么准确。在模板<code>f</code>中,<code>param</code>的声明类型是<code>const T&</code>。难道你们不觉得<code>T</code>和<code>param</code>类型相同很奇怪吗?比如<code>T</code>是<code>int</code>,<code>param</code>的类型应该是<code>const int&</code>而不是相同类型才对吧。</p>
|
||||
<p>遗憾的是,事实就是这样,<code>std::type_info::name</code>的结果并不总是可信的,就像上面一样,三个编译器对<code>param</code>的报告都是错误的。因为它们本质上可以不正确,因为<code>std::type_info::name</code>规范批准像传值形参一样来对待这些类型。正如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>提到的,如果传递的是一个引用,那么引用部分(reference-ness)将被忽略,如果忽略后还具有<code>const</code>或者<code>volatile</code>,那么常量性<code>const</code>ness或者易变性<code>volatile</code>ness也会被忽略。那就是为什么<code>param</code>的类型<code>const Widget * const &</code>会输出为<code>const Widget *</code>,首先引用被忽略,然后这个指针自身的常量性<code>const</code>ness被忽略,剩下的就是指针指向一个常量对象。</p>
|
||||
<p>遗憾的是,事实就是这样,<code>std::type_info::name</code>的结果并不总是可信的,就像上面一样,三个编译器对<code>param</code>的报告都是错误的。因为它们本质上可以不正确,因为<code>std::type_info::name</code>规范批准像传值形参一样来对待这些类型。正如<a href="../1.DeducingTypes/item1.html">Item1</a>提到的,如果传递的是一个引用,那么引用部分(reference-ness)将被忽略,如果忽略后还具有<code>const</code>或者<code>volatile</code>,那么常量性<code>const</code>ness或者易变性<code>volatile</code>ness也会被忽略。那就是为什么<code>param</code>的类型<code>const Widget * const &</code>会输出为<code>const Widget *</code>,首先引用被忽略,然后这个指针自身的常量性<code>const</code>ness被忽略,剩下的就是指针指向一个常量对象。</p>
|
||||
<p>同样遗憾的是,IDE编辑器显示的类型信息也不总是可靠的,或者说不总是有用的。还是一样的例子,一个IDE编辑器可能会把<code>T</code>的类型显示为(我没有胡编乱造):</p>
|
||||
<pre><code class="language-cpp">const
|
||||
std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
|
||||
@ -263,7 +263,7 @@ param = Widget const * const&
|
||||
<pre><code class="language-cpp">T = class Widget const *
|
||||
param = class Widget const * const &
|
||||
</code></pre>
|
||||
<p>这样近乎一致的结果是很不错的,但是请记住IDE,编译器错误诊断或者像Boost.TypeIndex这样的库只是用来帮助你理解编译器推导的类型是什么。它们是有用的,但是作为本章结束语我想说它们根本不能替代你对<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>-<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">3</a>提到的类型推导的理解。</p>
|
||||
<p>这样近乎一致的结果是很不错的,但是请记住IDE,编译器错误诊断或者像Boost.TypeIndex这样的库只是用来帮助你理解编译器推导的类型是什么。它们是有用的,但是作为本章结束语我想说它们根本不能替代你对<a href="../1.DeducingTypes/item1.html">Item1</a>-<a href="../1.DeducingTypes/item3.html">3</a>提到的类型推导的理解。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出</li>
|
||||
|
@ -180,7 +180,7 @@ void dwim(It b,It e)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>因为使用<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>所述的<code>auto</code>类型推导技术,它甚至能表示一些只有编译器才知道的类型:</p>
|
||||
<p>因为使用<a href="../1.DeducingTypes/item2.html">Item2</a>所述的<code>auto</code>类型推导技术,它甚至能表示一些只有编译器才知道的类型:</p>
|
||||
<pre><code class="language-cpp">auto derefUPLess =
|
||||
[](const std::unique_ptr<Widget> &p1, //用于std::unique_ptr
|
||||
const std::unique_ptr<Widget> &p2) //指向的Widget类型的
|
||||
@ -209,7 +209,7 @@ derefUPLess = [](const std::unique_ptr<Widget> &p1,
|
||||
const std::unique_ptr<Widget> &p2)
|
||||
{ return *p1 < *p2; };
|
||||
</code></pre>
|
||||
<p>语法冗长不说,还需要重复写很多形参类型,使用<code>std::function</code>还不如使用<code>auto</code>。用<code>auto</code>声明的变量保存一个和闭包一样类型的(新)闭包,因此使用了与闭包相同大小存储空间。实例化<code>std::function</code>并声明一个对象这个对象将会有固定的大小。这个大小可能不足以存储一个闭包,这个时候<code>std::function</code>的构造函数将会在堆上面分配内存来存储,这就造成了使用<code>std::function</code>比<code>auto</code>声明变量会消耗更多的内存。并且通过具体实现我们得知通过<code>std::function</code>调用一个闭包几乎无疑比<code>auto</code>声明的对象调用要慢。换句话说,<code>std::function</code>方法比<code>auto</code>方法要更耗空间且更慢,还可能有<em>out-of-memory</em>异常。并且正如上面的例子,比起写<code>std::function</code>实例化的类型来,使用<code>auto</code>要方便得多。在这场存储闭包的比赛中,<code>auto</code>无疑取得了胜利(也可以使用<code>std::bind</code>来生成一个闭包,但在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item34.md">Item34</a>我会尽我最大努力说服你使用<em>lambda</em>表达式代替<code>std::bind</code>)</p>
|
||||
<p>语法冗长不说,还需要重复写很多形参类型,使用<code>std::function</code>还不如使用<code>auto</code>。用<code>auto</code>声明的变量保存一个和闭包一样类型的(新)闭包,因此使用了与闭包相同大小存储空间。实例化<code>std::function</code>并声明一个对象这个对象将会有固定的大小。这个大小可能不足以存储一个闭包,这个时候<code>std::function</code>的构造函数将会在堆上面分配内存来存储,这就造成了使用<code>std::function</code>比<code>auto</code>声明变量会消耗更多的内存。并且通过具体实现我们得知通过<code>std::function</code>调用一个闭包几乎无疑比<code>auto</code>声明的对象调用要慢。换句话说,<code>std::function</code>方法比<code>auto</code>方法要更耗空间且更慢,还可能有<em>out-of-memory</em>异常。并且正如上面的例子,比起写<code>std::function</code>实例化的类型来,使用<code>auto</code>要方便得多。在这场存储闭包的比赛中,<code>auto</code>无疑取得了胜利(也可以使用<code>std::bind</code>来生成一个闭包,但在<a href="../6.LambdaExpressions/item34.html">Item34</a>我会尽我最大努力说服你使用<em>lambda</em>表达式代替<code>std::bind</code>)</p>
|
||||
<p>使用<code>auto</code>除了可以避免未初始化的无效变量,省略冗长的声明类型,直接保存闭包外,它还有一个好处是可以避免一个问题,我称之为与类型快捷方式(type shortcuts)有关的问题。你将看到这样的代码——甚至你会这么写:</p>
|
||||
<pre><code class="language-cpp">std::vector<int> v;
|
||||
…
|
||||
@ -238,14 +238,14 @@ for(const std::pair<std::string, int>& p : m)
|
||||
</code></pre>
|
||||
<p>这样无疑更具效率,且更容易书写。而且,这个代码有一个非常吸引人的特性,如果你获取<code>p</code>的地址,你确实会得到一个指向<code>m</code>中元素的指针。在没有<code>auto</code>的版本中<code>p</code>会指向一个临时变量,这个临时变量在每次迭代完成时会被销毁。</p>
|
||||
<p>后面这两个例子——应当写<code>std::vector<int>::size_type</code>时写了<code>unsigned</code>,应当写<code>std::pair<const std::string, int></code>时写了<code>std::pair<std::string, int></code>——说明了显式的指定类型可能会导致你不像看到的类型转换。如果你使用<code>auto</code>声明目标变量你就不必担心这个问题。</p>
|
||||
<p>基于这些原因我建议你优先考虑<code>auto</code>而非显式类型声明。然而<code>auto</code>也不是完美的。每个<code>auto</code>变量都从初始化表达式中推导类型,有一些表达式的类型和我们期望的大相径庭。关于在哪些情况下会发生这些问题,以及你可以怎么解决这些问题我们在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>和<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/2.Auto/item6.md">6</a>讨论,所以这里我不再赘述。我想把注意力放到你可能关心的另一点:使用auto代替传统类型声明对源码可读性的影响。</p>
|
||||
<p>基于这些原因我建议你优先考虑<code>auto</code>而非显式类型声明。然而<code>auto</code>也不是完美的。每个<code>auto</code>变量都从初始化表达式中推导类型,有一些表达式的类型和我们期望的大相径庭。关于在哪些情况下会发生这些问题,以及你可以怎么解决这些问题我们在<a href="../1.DeducingTypes/item2.html">Item2</a>和<a href="../2.Auto/item6.html">6</a>讨论,所以这里我不再赘述。我想把注意力放到你可能关心的另一点:使用auto代替传统类型声明对源码可读性的影响。</p>
|
||||
<p>首先,深呼吸,放松,<code>auto</code>是<strong>可选项</strong>,不是<strong>命令</strong>,在某些情况下如果你的专业判断告诉你使用显式类型声明比<code>auto</code>要更清晰更易维护,那你就不必再坚持使用<code>auto</code>。但是要牢记,C++没有在其他众所周知的语言所拥有的类型推导(<em>type inference</em>)上开辟新土地。其他静态类型的过程式语言(如C#、D、Sacla、Visual Basic)或多或少都有等价的特性,更不必提那些静态类型的函数式语言了(如ML、Haskell、OCaml、F#等)。在某种程度上,这是因为动态类型语言,如Perl、Python、Ruby等的成功;在这些语言中,几乎没有显式的类型声明。软件开发社区对于类型推导有丰富的经验,他们展示了在维护大型工业强度的代码上使用这种技术没有任何争议。</p>
|
||||
<p>一些开发者也担心使用<code>auto</code>就不能瞥一眼源代码便知道对象的类型,然而,IDE扛起了部分担子(也考虑到了<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item4.md">Item4</a>中提到的IDE类型显示问题),在很多情况下,少量显示一个对象的类型对于知道对象的确切类型是有帮助的,这通常已经足够了。举个例子,要想知道一个对象是容器还是计数器还是智能指针,不需要知道它的确切类型。一个适当的变量名称就能告诉我们大量的抽象类型信息。</p>
|
||||
<p>一些开发者也担心使用<code>auto</code>就不能瞥一眼源代码便知道对象的类型,然而,IDE扛起了部分担子(也考虑到了<a href="../1.DeducingTypes/item4.html">Item4</a>中提到的IDE类型显示问题),在很多情况下,少量显示一个对象的类型对于知道对象的确切类型是有帮助的,这通常已经足够了。举个例子,要想知道一个对象是容器还是计数器还是智能指针,不需要知道它的确切类型。一个适当的变量名称就能告诉我们大量的抽象类型信息。</p>
|
||||
<p>真正的问题是显式指定类型可以避免一些微妙的错误,以及更具效率和正确性,而且,如果初始化表达式的类型改变,则<code>auto</code>推导出的类型也会改变,这意味着使用<code>auto</code>可以帮助我们完成一些重构工作。举个例子,如果一个函数返回类型被声明为<code>int</code>,但是后来你认为将它声明为<code>long</code>会更好,调用它作为初始化表达式的变量会自动改变类型,但是如果你不使用<code>auto</code>你就不得不在源代码中挨个找到调用地点然后修改它们。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li><code>auto</code>变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。</li>
|
||||
<li>正如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>和<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/2.Auto/item6.md">6</a>讨论的,<code>auto</code>类型的变量可能会踩到一些陷阱。</li>
|
||||
<li>正如<a href="../1.DeducingTypes/item2.html">Item2</a>和<a href="../2.Auto/item6.html">6</a>讨论的,<code>auto</code>类型的变量可能会踩到一些陷阱。</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
|
@ -141,7 +141,7 @@
|
||||
<main>
|
||||
<h2 id="条款六auto推导若非己愿使用显式类型初始化惯用法"><a class="header" href="#条款六auto推导若非己愿使用显式类型初始化惯用法">条款六:<code>auto</code>推导若非己愿,使用显式类型初始化惯用法</a></h2>
|
||||
<p><strong>Item 6: Use the explicitly typed initializer idiom when <code>auto</code> deduces undesired types</strong></p>
|
||||
<p>在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/2.Auto/item5.md">Item5</a>中解释了比起显式指定类型使用<code>auto</code>声明变量有若干技术优势,但是有时当你想向左转<code>auto</code>却向右转。举个例子,假如我有一个函数,参数为<code>Widget</code>,返回一个<code>std::vector<bool></code>,这里的<code>bool</code>表示<code>Widget</code>是否提供一个独有的特性。</p>
|
||||
<p>在<a href="../2.Auto/item5.html">Item5</a>中解释了比起显式指定类型使用<code>auto</code>声明变量有若干技术优势,但是有时当你想向左转<code>auto</code>却向右转。举个例子,假如我有一个函数,参数为<code>Widget</code>,返回一个<code>std::vector<bool></code>,这里的<code>bool</code>表示<code>Widget</code>是否提供一个独有的特性。</p>
|
||||
<pre><code class="language-cpp">std::vector<bool> features(const Widget& w);
|
||||
</code></pre>
|
||||
<p>更进一步假设第5个<em>bit</em>表示<code>Widget</code>是否具有高优先级,我们可以写这样的代码:</p>
|
||||
@ -172,7 +172,7 @@ processWidget(w, highPriority); //根据它的优先级处理w
|
||||
<pre><code class="language-cpp">processWidget(w, highPriority); //未定义行为!
|
||||
//highPriority包含一个悬置指针!
|
||||
</code></pre>
|
||||
<p><code>std::vector<bool>::reference</code>是一个代理类(<em>proxy class</em>)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,<code>std::vector<bool>::reference</code>展示了对<code>std::vector<bool></code>使用<code>operator[]</code>来实现引用<em>bit</em>这样的行为。另外,C++标准模板库中的智能指针(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">第4章</a>)也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。事实上,“Proxy”设计模式是软件设计这座万神庙中一直都存在的高级会员。</p>
|
||||
<p><code>std::vector<bool>::reference</code>是一个代理类(<em>proxy class</em>)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,<code>std::vector<bool>::reference</code>展示了对<code>std::vector<bool></code>使用<code>operator[]</code>来实现引用<em>bit</em>这样的行为。另外,C++标准模板库中的智能指针(见<a href="../4.SmartPointers/item18.html">第4章</a>)也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。事实上,“Proxy”设计模式是软件设计这座万神庙中一直都存在的高级会员。</p>
|
||||
<p>一些代理类被设计于用以对客户可见。比如<code>std::shared_ptr</code>和<code>std::unique_ptr</code>。其他的代理类则或多或少不可见,比如<code>std::vector<bool>::reference</code>就是不可见代理类的一个例子,还有它在<code>std::bitset</code>的胞弟<code>std::bitset::reference</code>。</p>
|
||||
<p>在后者的阵营(注:指不可见代理类)里一些C++库也是用了表达式模板(<em>expression templates</em>)的黑科技。这些库通常被用于提高数值运算的效率。给出一个矩阵类<code>Matrix</code>和矩阵对象<code>m1</code>,<code>m2</code>,<code>m3</code>,<code>m4</code>,举个例子,这个表达式</p>
|
||||
<pre><code class="language-cpp">Matrix sum = m1 + m2 + m3 + m4;
|
||||
@ -182,7 +182,7 @@ processWidget(w, highPriority); //根据它的优先级处理w
|
||||
<p>因此你想避开这种形式的代码:</p>
|
||||
<pre><code class="language-cpp">auto someVar = expression of "invisible" proxy class type;
|
||||
</code></pre>
|
||||
<p>但是你怎么能意识到你正在使用代理类?应用他们的软件不可能宣告它们的存在。它们被设计为<strong>不可见</strong>,至少概念上说是这样!每当你发现它们,你真的应该舍弃<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/2.Auto/item5.md">Item5</a>演示的<code>auto</code>所具有的诸多好处吗?</p>
|
||||
<p>但是你怎么能意识到你正在使用代理类?应用他们的软件不可能宣告它们的存在。它们被设计为<strong>不可见</strong>,至少概念上说是这样!每当你发现它们,你真的应该舍弃<a href="../2.Auto/item5.html">Item5</a>演示的<code>auto</code>所具有的诸多好处吗?</p>
|
||||
<p>让我们首先回到如何找到它们的问题上。虽然“不可见”代理类都在程序员日常使用的雷达下方飞行,但是很多库都证明它们可以上方飞行。当你越熟悉你使用的库的基本设计理念,你的思维就会越活跃,不至于思维僵化认为代理类只能在这些库中使用。</p>
|
||||
<p>当缺少文档的时候,可以去看看头文件。很少会出现源代码全都用代理对象,它们通常用于一些函数的返回类型,所以通常能从函数签名中看出它们的存在。这里有一份<code>std::vector<bool>::operator[]</code>的说明书:</p>
|
||||
<pre><code class="language-cpp">namespace std{ //来自于C++标准库
|
||||
|
@ -285,8 +285,8 @@ auto val =
|
||||
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>
|
||||
(uInfo);
|
||||
</code></pre>
|
||||
<p>为避免这种冗长的表示,我们可以写一个函数传入枚举名并返回对应的<code>std::size_t</code>值,但这有一点技巧性。<code>std::get</code>是一个模板(函数),需要你给出一个<code>std::size_t</code>值的模板实参(注意使用<code><></code>而不是<code>()</code>),因此将枚举名变换为<code>std::size_t</code>值的函数必须<strong>在编译期</strong>产生这个结果。如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item15.md">Item15</a>提到的,那必须是一个<code>constexpr</code>函数。</p>
|
||||
<p>事实上,它也的确该是一个<code>constexpr</code>函数模板,因为它应该能用于任何<code>enum</code>。如果我们想让它更一般化,我们还要泛化它的返回类型。较之于返回<code>std::size_t</code>,我们更应该返回枚举的底层类型。这可以通过<code>std::underlying_type</code>这个<em>type trait</em>获得。(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>关于<em>type trait</em>的内容)。最终我们还要再加上<code>noexcept</code>修饰(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md">Item14</a>),因为我们知道它肯定不会产生异常。根据上述分析最终得到的<code>toUType</code>函数模板在编译期接受任意枚举名并返回它的值:</p>
|
||||
<p>为避免这种冗长的表示,我们可以写一个函数传入枚举名并返回对应的<code>std::size_t</code>值,但这有一点技巧性。<code>std::get</code>是一个模板(函数),需要你给出一个<code>std::size_t</code>值的模板实参(注意使用<code><></code>而不是<code>()</code>),因此将枚举名变换为<code>std::size_t</code>值的函数必须<strong>在编译期</strong>产生这个结果。如<a href="../3.MovingToModernCpp/item15.html">Item15</a>提到的,那必须是一个<code>constexpr</code>函数。</p>
|
||||
<p>事实上,它也的确该是一个<code>constexpr</code>函数模板,因为它应该能用于任何<code>enum</code>。如果我们想让它更一般化,我们还要泛化它的返回类型。较之于返回<code>std::size_t</code>,我们更应该返回枚举的底层类型。这可以通过<code>std::underlying_type</code>这个<em>type trait</em>获得。(参见<a href="../3.MovingToModernCpp/item9.html">Item9</a>关于<em>type trait</em>的内容)。最终我们还要再加上<code>noexcept</code>修饰(参见<a href="../3.MovingToModernCpp/item14.html">Item14</a>),因为我们知道它肯定不会产生异常。根据上述分析最终得到的<code>toUType</code>函数模板在编译期接受任意枚举名并返回它的值:</p>
|
||||
<pre><code class="language-cpp">template<typename E>
|
||||
constexpr typename std::underlying_type<E>::type
|
||||
toUType(E enumerator) noexcept
|
||||
@ -296,7 +296,7 @@ constexpr typename std::underlying_type<E>::type
|
||||
std::underlying_type<E>::type>(enumerator);
|
||||
}
|
||||
</code></pre>
|
||||
<p>在C++14中,<code>toUType</code>还可以进一步用<code>std::underlying_type_t</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>)代替<code>typename std::underlying_type<E>::type</code>打磨:</p>
|
||||
<p>在C++14中,<code>toUType</code>还可以进一步用<code>std::underlying_type_t</code>(参见<a href="../3.MovingToModernCpp/item9.html">Item9</a>)代替<code>typename std::underlying_type<E>::type</code>打磨:</p>
|
||||
<pre><code class="language-cpp">template<typename E> //C++14
|
||||
constexpr std::underlying_type_t<E>
|
||||
toUType(E enumerator) noexcept
|
||||
@ -304,7 +304,7 @@ constexpr std::underlying_type_t<E>
|
||||
return static_cast<std::underlying_type_t<E>>(enumerator);
|
||||
}
|
||||
</code></pre>
|
||||
<p>还可以再用C++14 <code>auto</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>)打磨一下代码:</p>
|
||||
<p>还可以再用C++14 <code>auto</code>(参见<a href="../1.DeducingTypes/item3.html">Item3</a>)打磨一下代码:</p>
|
||||
<pre><code class="language-cpp">template<typename E> //C++14
|
||||
constexpr auto
|
||||
toUType(E enumerator) noexcept
|
||||
|
@ -142,7 +142,7 @@
|
||||
<h2 id="条款十一优先考虑使用deleted函数而非使用未定义的私有声明"><a class="header" href="#条款十一优先考虑使用deleted函数而非使用未定义的私有声明">条款十一:优先考虑使用<em>deleted</em>函数而非使用未定义的私有声明</a></h2>
|
||||
<p><strong>Item 11: Prefer deleted functions to private undefined ones.</strong></p>
|
||||
<p>如果你写的代码要被其他人使用,你不想让他们调用某个特殊的函数,你通常不会声明这个函数。无声明,不函数。简简单单!但有时C++会给你自动声明一些函数,如果你想防止客户调用这些函数,事情就不那么简单了。</p>
|
||||
<p>上述场景见于特殊的成员函数,即当有必要时C++自动生成的那些函数。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>详细讨论了这些函数,但是现在,我们只关心拷贝构造函数和拷贝赋值运算符重载。本节主要致力于讨论C++98中那些被C++11所取代的最佳实践,而且在C++98中,你想要禁止使用的成员函数,几乎总是拷贝构造函数或者赋值运算符,或者两者都是。</p>
|
||||
<p>上述场景见于特殊的成员函数,即当有必要时C++自动生成的那些函数。<a href="../3.MovingToModernCpp/item17.html">Item17</a>详细讨论了这些函数,但是现在,我们只关心拷贝构造函数和拷贝赋值运算符重载。本节主要致力于讨论C++98中那些被C++11所取代的最佳实践,而且在C++98中,你想要禁止使用的成员函数,几乎总是拷贝构造函数或者赋值运算符,或者两者都是。</p>
|
||||
<p>在C++98中防止调用这些函数的方法是将它们声明为私有(<code>private</code>)成员函数并且不定义。举个例子,在C++ 标准库<em>iostream</em>继承链的顶部是模板类<code>basic_ios</code>。所有<em>istream</em>和<em>ostream</em>类都继承此类(直接或者间接)。拷贝<em>istream</em>和<em>ostream</em>是不合适的,因为这些操作应该怎么做是模棱两可的。比如一个<code>istream</code>对象,代表一个输入值的流,流中有一些已经被读取,有一些可能马上要被读取。如果一个<em>istream</em>被拷贝,需要拷贝将要被读取的值和已经被读取的值吗?解决这个问题最好的方法是不定义这个操作。直接禁止拷贝流。</p>
|
||||
<p>要使这些<em>istream</em>和<em>ostream</em>类不可拷贝,<code>basic_ios</code>在C++98中是这样声明的(包括注释):</p>
|
||||
<pre><code class="language-cpp">template <class charT, class traits = char_traits<charT> >
|
||||
@ -190,7 +190,7 @@ bool isLucky(double) = delete; //拒绝float和double
|
||||
if (isLucky(true)) … //错误!
|
||||
if (isLucky(3.5f)) … //错误!
|
||||
</code></pre>
|
||||
<p>另一个<em>deleted</em>函数用武之地(<code>private</code>成员函数做不到的地方)是禁止一些模板的实例化。假如你要求一个模板仅支持原生指针(尽管<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">第四章</a>建议使用智能指针代替原生指针):</p>
|
||||
<p>另一个<em>deleted</em>函数用武之地(<code>private</code>成员函数做不到的地方)是禁止一些模板的实例化。假如你要求一个模板仅支持原生指针(尽管<a href="../4.SmartPointers/item18.html">第四章</a>建议使用智能指针代替原生指针):</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void processPointer(T* ptr);
|
||||
</code></pre>
|
||||
|
@ -163,7 +163,7 @@ ConstIterT ci =
|
||||
values.insert(static_cast<IterT>(ci), 1998); //可能无法通过编译,
|
||||
//原因见下
|
||||
</code></pre>
|
||||
<p><code>typedef</code>不是强制的,但是可以让代码中的<em>cast</em>更好写。(你可能想知道为什么我使用<code>typedef</code>而不是<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>提到的别名声明,因为这段代码在演示C++98做法,别名声明是C++11加入的特性)</p>
|
||||
<p><code>typedef</code>不是强制的,但是可以让代码中的<em>cast</em>更好写。(你可能想知道为什么我使用<code>typedef</code>而不是<a href="../3.MovingToModernCpp/item9.html">Item9</a>提到的别名声明,因为这段代码在演示C++98做法,别名声明是C++11加入的特性)</p>
|
||||
<p>之所以<code>std::find</code>的调用会出现类型转换是因为在C++98中<code>values</code>是non-<code>const</code>容器,没办法简简单单的从non-<code>const</code>容器中获取<code>const_iterator</code>。严格来说类型转换不是必须的,因为用其他方法获取<code>const_iterator</code>也是可以的(比如你可以把<code>values</code>绑定到reference-to-<code>const</code>变量上,然后再用这个变量代替<code>values</code>),但不管怎么说,从non-<code>const</code>容器中获取<code>const_iterator</code>的做法都有点别扭。</p>
|
||||
<p>当你费劲地获得了<code>const_iterator</code>,事情可能会变得更糟,因为C++98中,插入操作(以及删除操作)的位置只能由<code>iterator</code>指定,<code>const_iterator</code>是不被接受的。这也是我在上面的代码中,将<code>const_iterator</code>(我那么小心地从<code>std::find</code>搞出来的东西)转换为<code>iterator</code>的原因,因为向<code>insert</code>传入<code>const_iterator</code>不能通过编译。</p>
|
||||
<p>老实说,上面的代码也可能无法编译,因为没有一个可移植的从<code>const_iterator</code>到<code>iterator</code>的方法,即使使用<code>static_cast</code>也不行。甚至传说中的牛刀<code>reinterpret_cast</code>也杀不了这条鸡。(它不是C++98的限制,也不是C++11的限制,只是<code>const_iterator</code>就是不能转换为<code>iterator</code>,不管看起来对它们施以转换是有多么合理。)不过有办法生成一个<code>iterator</code>,使其指向和<code>const_iterator</code>指向相同,但是看起来不明显,也没有广泛应用,在这本书也不值得讨论。除此之外,我希望目前我陈述的观点是清晰的:<code>const_iterator</code>在C++98中会有很多问题,不如它的兄弟(译注:指<code>iterator</code>)有用。最终,开发者们不再相信能加<code>const</code>就加它的教条,而是只在实用的地方加它,C++98的<code>const_iterator</code>不是那么实用。</p>
|
||||
|
@ -163,11 +163,11 @@ Widget w;
|
||||
vw.push_back(w); //把w添加进vw
|
||||
…
|
||||
</code></pre>
|
||||
<p>假设这个代码能正常工作,你也无意修改为C++11风格。但是你确实想要C++11移动语义带来的性能优势,毕竟这里的类型是可以移动的(move-enabled types)。因此你需要确保<code>Widget</code>有移动操作,可以手写代码也可以让编译器自动生成,当然前提是能满足自动生成的条件(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>)。</p>
|
||||
<p>假设这个代码能正常工作,你也无意修改为C++11风格。但是你确实想要C++11移动语义带来的性能优势,毕竟这里的类型是可以移动的(move-enabled types)。因此你需要确保<code>Widget</code>有移动操作,可以手写代码也可以让编译器自动生成,当然前提是能满足自动生成的条件(参见<a href="../3.MovingToModernCpp/item17.html">Item17</a>)。</p>
|
||||
<p>当新元素添加到<code>std::vector</code>,<code>std::vector</code>可能没地方放它,换句话说,<code>std::vector</code>的大小(size)等于它的容量(capacity)。这时候,<code>std::vector</code>会分配一个新的更大块的内存用于存放其中元素,然后将元素从老内存区移动到新内存区,然后析构老内存区里的对象。在C++98中,移动是通过复制老内存区的每一个元素到新内存区完成的,然后老内存区的每个元素发生析构。这种方法使得<code>push_back</code>可以提供很强的异常安全保证:如果在复制元素期间抛出异常,<code>std::vector</code>状态保持不变,因为老内存元素析构必须建立在它们已经成功复制到新内存的前提下。</p>
|
||||
<p>在C++11中,一个很自然的优化就是将上述复制操作替换为移动操作。但是很不幸运,这会破坏<code>push_back</code>的异常安全保证。如果<strong>n</strong>个元素已经从老内存移动到了新内存区,但异常在移动第<strong>n+1</strong>个元素时抛出,那么<code>push_back</code>操作就不能完成。但是原始的<code>std::vector</code>已经被修改:有<strong>n</strong>个元素已经移动走了。恢复<code>std::vector</code>至原始状态也不太可能,因为从新内存移动到老内存本身又可能引发异常。</p>
|
||||
<p>这是个很严重的问题,因为老代码可能依赖于<code>push_back</code>提供的强烈的异常安全保证。因此,C++11版本的实现不能简单的将<code>push_back</code>里面的复制操作替换为移动操作,除非知晓移动操作绝不抛异常,这时复制替换为移动就是安全的,唯一的副作用就是性能得到提升。</p>
|
||||
<p><code>std::vector::push_back</code>受益于“如果可以就移动,如果必要则复制”策略,并且它不是标准库中唯一采取该策略的函数。C++98中还有一些函数(如<code>std::vector::reverse</code>,<code>std::deque::insert</code>等)也受益于这种强异常保证。对于这个函数只有在知晓移动不抛异常的情况下用C++11的移动操作替换C++98的复制操作才是安全的。但是如何知道一个函数中的移动操作是否产生异常?答案很明显:它检查这个操作是否被声明为<code>noexcept</code>。(这个检查非常弯弯绕。像是<code>std::vector::push_back</code>之类的函数调用<code>std::move_if_noexcept</code>,这是个<code>std::move</code>的变体,根据其中类型的移动构造函数是否为<code>noexcept</code>的,视情况转换为右值或保持左值(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>)。反过来,<code>std::move_if_noexcept</code>查阅<code>std::is_nothrow_move_constructible</code>这个<em>type trait</em>,基于移动构造函数是否有<code>noexcept</code>(或者<code>throw()</code>)的设计,编译器设置这个<em>type trait</em>的值。)</p>
|
||||
<p><code>std::vector::push_back</code>受益于“如果可以就移动,如果必要则复制”策略,并且它不是标准库中唯一采取该策略的函数。C++98中还有一些函数(如<code>std::vector::reverse</code>,<code>std::deque::insert</code>等)也受益于这种强异常保证。对于这个函数只有在知晓移动不抛异常的情况下用C++11的移动操作替换C++98的复制操作才是安全的。但是如何知道一个函数中的移动操作是否产生异常?答案很明显:它检查这个操作是否被声明为<code>noexcept</code>。(这个检查非常弯弯绕。像是<code>std::vector::push_back</code>之类的函数调用<code>std::move_if_noexcept</code>,这是个<code>std::move</code>的变体,根据其中类型的移动构造函数是否为<code>noexcept</code>的,视情况转换为右值或保持左值(参见<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>)。反过来,<code>std::move_if_noexcept</code>查阅<code>std::is_nothrow_move_constructible</code>这个<em>type trait</em>,基于移动构造函数是否有<code>noexcept</code>(或者<code>throw()</code>)的设计,编译器设置这个<em>type trait</em>的值。)</p>
|
||||
<p><code>swap</code>函数是<code>noexcept</code>的另一个绝佳用地。<code>swap</code>是STL算法实现的一个关键组件,它也常用于拷贝运算符重载中。它的广泛使用意味着对其施加不抛异常的优化是非常有价值的。有趣的是,标准库的<code>swap</code>是否<code>noexcept</code>有时依赖于用户定义的<code>swap</code>是否<code>noexcept</code>。比如,数组和<code>std::pair</code>的<code>swap</code>声明如下:</p>
|
||||
<pre><code class="language-cpp">template <class T, size_t N>
|
||||
void swap(T (&a)[N],
|
||||
|
@ -207,7 +207,7 @@ private:
|
||||
</code></pre>
|
||||
<p><code>std::mutex m</code>被声明为<code>mutable</code>,因为锁定和解锁它的都是non-<code>const</code>成员函数。在<code>roots</code>(<code>const</code>成员函数)中,<code>m</code>却被视为<code>const</code>对象。</p>
|
||||
<p>值得注意的是,因为<code>std::mutex</code>是一种只可移动类型(<em>move-only type</em>,一种可以移动但不能复制的类型),所以将<code>m</code>添加进<code>Polynomial</code>中的副作用是使<code>Polynomial</code>失去了被复制的能力。不过,它仍然可以移动。</p>
|
||||
<p>在某些情况下,互斥量的副作用显会得过大。例如,如果你所做的只是计算成员函数被调用了多少次,使用<code>std::atomic</code> 修饰的计数器(保证其他线程视它的操作为不可分割的整体,参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md">item40</a>)通常会是一个开销更小的方法。(然而它是否轻量取决于你使用的硬件和标准库中互斥量的实现。)以下是如何使用<code>std::atomic</code>来统计调用次数。</p>
|
||||
<p>在某些情况下,互斥量的副作用显会得过大。例如,如果你所做的只是计算成员函数被调用了多少次,使用<code>std::atomic</code> 修饰的计数器(保证其他线程视它的操作为不可分割的整体,参见<a href="../7.TheConcurrencyAPI/item40.html">item40</a>)通常会是一个开销更小的方法。(然而它是否轻量取决于你使用的硬件和标准库中互斥量的实现。)以下是如何使用<code>std::atomic</code>来统计调用次数。</p>
|
||||
<pre><code class="language-c++">class Point { //2D点
|
||||
public:
|
||||
…
|
||||
|
@ -153,12 +153,12 @@ public:
|
||||
};
|
||||
</code></pre>
|
||||
<p>掌控它们生成和行为的规则类似于拷贝系列。移动操作仅在需要的时候生成,如果生成了,就会对类的non-static数据成员执行逐成员的移动。那意味着移动构造函数根据<code>rhs</code>参数里面对应的成员移动构造出新non-static部分,移动赋值运算符根据参数里面对应的non-static成员移动赋值。移动构造函数也移动构造基类部分(如果有的话),移动赋值运算符也是移动赋值基类部分。</p>
|
||||
<p>现在,当我对一个数据成员或者基类使用移动构造或者移动赋值时,没有任何保证移动一定会真的发生。逐成员移动,实际上,更像是逐成员移动<strong>请求</strong>,因为对<strong>不可移动类型</strong>(即对移动操作没有特殊支持的类型,比如大部分C++98传统类)使用“移动”操作实际上执行的是拷贝操作。逐成员移动的核心是对对象使用<code>std::move</code>,然后函数决议时会选择执行移动还是拷贝操作。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>包括了这个操作的细节。本条款中,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。</p>
|
||||
<p>现在,当我对一个数据成员或者基类使用移动构造或者移动赋值时,没有任何保证移动一定会真的发生。逐成员移动,实际上,更像是逐成员移动<strong>请求</strong>,因为对<strong>不可移动类型</strong>(即对移动操作没有特殊支持的类型,比如大部分C++98传统类)使用“移动”操作实际上执行的是拷贝操作。逐成员移动的核心是对对象使用<code>std::move</code>,然后函数决议时会选择执行移动还是拷贝操作。<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>包括了这个操作的细节。本条款中,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。</p>
|
||||
<p>像拷贝操作情况一样,如果你自己声明了移动操作,编译器就不会生成。然而它们生成的精确条件与拷贝操作的条件有点不同。</p>
|
||||
<p>两个拷贝操作是独立的:声明一个不会限制编译器生成另一个。所以如果你声明一个拷贝构造函数,但是没有声明拷贝赋值运算符,如果写的代码用到了拷贝赋值,编译器会帮助你生成拷贝赋值运算符。同样的,如果你声明拷贝赋值运算符但是没有拷贝构造函数,代码用到拷贝构造函数时编译器就会生成它。上述规则在C++98和C++11中都成立。</p>
|
||||
<p>两个移动操作不是相互独立的。如果你声明了其中一个,编译器就不再生成另一个。如果你给类声明了,比如,一个移动构造函数,就表明对于移动操作应怎样实现,与编译器应生成的默认逐成员移动有些区别。如果逐成员移动构造有些问题,那么逐成员移动赋值同样也可能有问题。所以声明移动构造函数阻止移动赋值运算符的生成,声明移动赋值运算符同样阻止编译器生成移动构造函数。</p>
|
||||
<p>再进一步,如果一个类显式声明了拷贝操作,编译器就不会生成移动操作。这种限制的解释是如果声明拷贝操作(构造或者赋值)就暗示着平常拷贝对象的方法(逐成员拷贝)不适用于该类,编译器会明白如果逐成员拷贝对拷贝操作来说不合适,逐成员移动也可能对移动操作来说不合适。</p>
|
||||
<p>这是另一个方向。声明移动操作(构造或赋值)使得编译器禁用拷贝操作。(编译器通过给拷贝操作加上<em>delete</em>来保证,参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item11.md">Item11</a>。)(译注:禁用的是自动生成的拷贝操作,对于用户声明的拷贝操作不受影响)毕竟,如果逐成员移动对该类来说不合适,也没有理由指望逐成员拷贝操作是合适的。听起来会破坏C++98的某些代码,因为C++11中拷贝操作可用的条件比C++98更受限,但事实并非如此。C++98的代码没有移动操作,因为C++98中没有移动对象这种概念。只有一种方法能让老代码使用用户声明的移动操作,那就是使用C++11标准然后添加这些操作,使用了移动语义的类必须接受C++11特殊成员函数生成规则的限制。</p>
|
||||
<p>这是另一个方向。声明移动操作(构造或赋值)使得编译器禁用拷贝操作。(编译器通过给拷贝操作加上<em>delete</em>来保证,参见<a href="../3.MovingToModernCpp/item11.html">Item11</a>。)(译注:禁用的是自动生成的拷贝操作,对于用户声明的拷贝操作不受影响)毕竟,如果逐成员移动对该类来说不合适,也没有理由指望逐成员拷贝操作是合适的。听起来会破坏C++98的某些代码,因为C++11中拷贝操作可用的条件比C++98更受限,但事实并非如此。C++98的代码没有移动操作,因为C++98中没有移动对象这种概念。只有一种方法能让老代码使用用户声明的移动操作,那就是使用C++11标准然后添加这些操作,使用了移动语义的类必须接受C++11特殊成员函数生成规则的限制。</p>
|
||||
<p>也许你早已听过_Rule of Three_规则。这个规则告诉我们如果你声明了拷贝构造函数,拷贝赋值运算符,或者析构函数三者之一,你应该也声明其余两个。它来源于长期的观察,即用户接管拷贝操作的需求几乎都是因为该类会做其他资源的管理,这也几乎意味着(1)无论哪种资源管理如果在一个拷贝操作内完成,也应该在另一个拷贝操作内完成(2)类的析构函数也需要参与资源的管理(通常是释放)。通常要管理的资源是内存,这也是为什么标准库里面那些管理内存的类(如会动态内存管理的STL容器)都声明了“<em>the big three</em>”:拷贝构造,拷贝赋值和析构。</p>
|
||||
<p><em>Rule of Three</em>带来的后果就是只要出现用户定义的析构函数就意味着简单的逐成员拷贝操作不适用于该类。那意味着如果一个类声明了析构,拷贝操作可能不应该自动生成,因为它们做的事情可能是错误的。在C++98提出的时候,上述推理没有得倒足够的重视,所以C++98用户声明析构函数不会左右编译器生成拷贝操作的意愿。C++11中情况仍然如此,但仅仅是因为限制拷贝操作生成的条件会破坏老代码。</p>
|
||||
<p><em>Rule of Three</em>规则背后的解释依然有效,再加上对声明拷贝操作阻止移动操作隐式生成的观察,使得C++11不会为那些有用户定义的析构函数的类生成移动操作。</p>
|
||||
@ -222,7 +222,7 @@ private:
|
||||
<p>C++11对于特殊成员函数处理的规则如下:</p>
|
||||
<ul>
|
||||
<li><strong>默认构造函数</strong>:和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成。</li>
|
||||
<li><strong>析构函数</strong>:基本上和C++98相同;稍微不同的是现在析构默认<code>noexcept</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md">Item14</a>)。和C++98一样,仅当基类析构为虚函数时该类析构才为虚函数。</li>
|
||||
<li><strong>析构函数</strong>:基本上和C++98相同;稍微不同的是现在析构默认<code>noexcept</code>(参见<a href="../3.MovingToModernCpp/item14.html">Item14</a>)。和C++98一样,仅当基类析构为虚函数时该类析构才为虚函数。</li>
|
||||
<li><strong>拷贝构造函数</strong>:和C++98运行时行为一样:逐成员拷贝non-static数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是<em>delete</em>的。当用户声明了拷贝赋值或者析构,该函数自动生成已被废弃。</li>
|
||||
<li><strong>拷贝赋值运算符</strong>:和C++98运行时行为一样:逐成员拷贝赋值non-static数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是<em>delete</em>的。当用户声明了拷贝构造或者析构,该函数自动生成已被废弃。</li>
|
||||
<li><strong>移动构造函数</strong>和<strong>移动赋值运算符</strong>:都对非static数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。</li>
|
||||
@ -238,7 +238,7 @@ private:
|
||||
…
|
||||
};
|
||||
</code></pre>
|
||||
<p>编译器仍会生成移动和拷贝操作(假设正常生成它们的条件满足),即使可以模板实例化产出拷贝构造和拷贝赋值运算符的函数签名。(当<code>T</code>为<code>Widget</code>时。)很可能你会觉得这是一个不值得承认的边缘情况,但是我提到它是有道理的,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>将会详细讨论它可能带来的后果。</p>
|
||||
<p>编译器仍会生成移动和拷贝操作(假设正常生成它们的条件满足),即使可以模板实例化产出拷贝构造和拷贝赋值运算符的函数签名。(当<code>T</code>为<code>Widget</code>时。)很可能你会觉得这是一个不值得承认的边缘情况,但是我提到它是有道理的,<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>将会详细讨论它可能带来的后果。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>特殊成员函数是编译器可能自动生成的函数:默认构造函数,析构函数,拷贝操作,移动操作。</li>
|
||||
|
@ -178,7 +178,7 @@ private:
|
||||
int z(0); //错误!
|
||||
}
|
||||
</code></pre>
|
||||
<p>另一方面,不可拷贝的对象(例如<code>std::atomic</code>——见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md">Item40</a>)可以使用花括号初始化或者小括号初始化,但是不能使用"="初始化:</p>
|
||||
<p>另一方面,不可拷贝的对象(例如<code>std::atomic</code>——见<a href="../7.TheConcurrencyAPI/item40.html">Item40</a>)可以使用花括号初始化或者小括号初始化,但是不能使用"="初始化:</p>
|
||||
<pre><code class="language-cpp">std::atomic<int> ai1{ 0 }; //没问题
|
||||
std::atomic<int> ai2(0); //没问题
|
||||
std::atomic<int> ai3 = 0; //错误!
|
||||
@ -204,7 +204,7 @@ int sum3 = x + y + z; //同上
|
||||
<pre><code class="language-cpp">Widget w3{}; //调用没有参数的构造函数构造对象
|
||||
</code></pre>
|
||||
<p>关于括号初始化还有很多要说的。它的语法能用于各种不同的上下文,它防止了隐式的变窄转换,而且对于C++最令人头疼的解析也天生免疫。既然好到这个程度那为什么这个条款不叫“Prefer braced initialization syntax”呢?</p>
|
||||
<p>括号初始化的缺点是有时它有一些令人惊讶的行为。这些行为使得括号初始化、<code>std::initializer_list</code>和构造函数重载决议本来就不清不楚的暧昧关系进一步混乱。把它们放到一起会让看起来应该左转的代码右转。举个例子,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>解释了当<code>auto</code>声明的变量使用花括号初始化,变量类型就会被推导为<code>std::initializer_list</code>,尽管使用相同内容的其他初始化方式会产生正常的结果。所以,你越喜欢用<code>auto</code>,你就越不能用括号初始化。</p>
|
||||
<p>括号初始化的缺点是有时它有一些令人惊讶的行为。这些行为使得括号初始化、<code>std::initializer_list</code>和构造函数重载决议本来就不清不楚的暧昧关系进一步混乱。把它们放到一起会让看起来应该左转的代码右转。举个例子,<a href="../1.DeducingTypes/item2.html">Item2</a>解释了当<code>auto</code>声明的变量使用花括号初始化,变量类型就会被推导为<code>std::initializer_list</code>,尽管使用相同内容的其他初始化方式会产生正常的结果。所以,你越喜欢用<code>auto</code>,你就越不能用括号初始化。</p>
|
||||
<p>在构造函数调用中,只要不包含<code>std::initializer_list</code>形参,那么花括号初始化和小括号初始化都会产生一样的结果:</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
public:
|
||||
@ -326,7 +326,7 @@ void doSomeWork(Ts&&... params)
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>在现实中我们有两种方式实现这个伪代码(关于<code>std::forward</code>请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>):</p>
|
||||
<p>在现实中我们有两种方式实现这个伪代码(关于<code>std::forward</code>请参见<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>):</p>
|
||||
<pre><code class="language-cpp">T localObject(std::forward<Ts>(params)...); //使用小括号
|
||||
T localObject{std::forward<Ts>(params)...}; //使用花括号
|
||||
</code></pre>
|
||||
@ -336,7 +336,7 @@ T localObject{std::forward<Ts>(params)...}; //使用花括号
|
||||
doSomeWork<std::vector<int>>(10, 20);
|
||||
</code></pre>
|
||||
<p>如果<code>doSomeWork</code>创建<code>localObject</code>时使用的是小括号,<code>std::vector</code>就会包含10个元素。如果<code>doSomeWork</code>创建<code>localObject</code>时使用的是花括号,<code>std::vector</code>就会包含2个元素。哪个是正确的?<code>doSomeWork</code>的作者不知道,只有调用者知道。</p>
|
||||
<p>这正是标准库函数<code>std::make_unique</code>和<code>std::make_shared</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>)面对的问题。它们的解决方案是使用小括号,并被记录在文档中作为接口的一部分。(注:更灵活的设计——允许调用者决定从模板来的函数应该使用小括号还是花括号——是有可能的。详情参见<a href="http://akrzemi1.wordpress.com/">Andrzej’s C++ blog</a>在2013年6月5日的文章,“<a href="http://akrzemi1.wordpress.com/2013/06/05/intuitive-interface-part-i/">Intuitive interface — Part I.</a>”)</p>
|
||||
<p>这正是标准库函数<code>std::make_unique</code>和<code>std::make_shared</code>(参见<a href="../4.SmartPointers/item21.html">Item21</a>)面对的问题。它们的解决方案是使用小括号,并被记录在文档中作为接口的一部分。(注:更灵活的设计——允许调用者决定从模板来的函数应该使用小括号还是花括号——是有可能的。详情参见<a href="http://akrzemi1.wordpress.com/">Andrzej’s C++ blog</a>在2013年6月5日的文章,“<a href="http://akrzemi1.wordpress.com/2013/06/05/intuitive-interface-part-i/">Intuitive interface — Part I.</a>”)</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于C++最令人头疼的解析有天生的免疫性</li>
|
||||
|
@ -211,7 +211,7 @@ auto lockAndCall(FuncType func,
|
||||
return func(ptr);
|
||||
}
|
||||
</code></pre>
|
||||
<p>如果你对函数返回类型(<code>auto ... -> decltype(func(ptr))</code>)感到困惑不解,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>可以帮助你。在C++14中代码的返回类型还可以被简化为<code>decltype(auto)</code>:</p>
|
||||
<p>如果你对函数返回类型(<code>auto ... -> decltype(func(ptr))</code>)感到困惑不解,<a href="../1.DeducingTypes/item3.html">Item3</a>可以帮助你。在C++14中代码的返回类型还可以被简化为<code>decltype(auto)</code>:</p>
|
||||
<pre><code class="language-cpp">template<typename FuncType,
|
||||
typename MuxType,
|
||||
typename PtrType>
|
||||
|
@ -141,7 +141,7 @@
|
||||
<main>
|
||||
<h2 id="条款九优先考虑别名声明而非typedefs"><a class="header" href="#条款九优先考虑别名声明而非typedefs">条款九:优先考虑别名声明而非<code>typedef</code>s</a></h2>
|
||||
<p><strong>Item 9: Prefer alias declarations to <code>typedef</code>s</strong></p>
|
||||
<p>我相信每个人都同意使用STL容器是个好主意,并且我希望<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>能说服你让你觉得使用<code>std:unique_ptr</code>也是个好主意,但我猜没有人喜欢写上几次 <code>std::unique_ptr<std::unordered_map<std::string, std::string>></code>这样的类型,它可能会让你患上腕管综合征的风险大大增加。</p>
|
||||
<p>我相信每个人都同意使用STL容器是个好主意,并且我希望<a href="../4.SmartPointers/item18.html">Item18</a>能说服你让你觉得使用<code>std:unique_ptr</code>也是个好主意,但我猜没有人喜欢写上几次 <code>std::unique_ptr<std::unordered_map<std::string, std::string>></code>这样的类型,它可能会让你患上腕管综合征的风险大大增加。</p>
|
||||
<p>避免上述医疗悲剧也很简单,引入<code>typedef</code>即可:</p>
|
||||
<pre><code class="language-cpp">typedef
|
||||
std::unique_ptr<std::unordered_map<std::string, std::string>>
|
||||
@ -212,7 +212,7 @@ private:
|
||||
};
|
||||
</code></pre>
|
||||
<p>就像你看到的,<code>MyAllocList<Wine>::type</code>不是一个类型。如果<code>Widget</code>使用<code>Wine</code>实例化,在<code>Widget</code>模板中的<code>MyAllocList<Wine>::type</code>将会是一个数据成员,不是一个类型。在<code>Widget</code>模板内,<code>MyAllocList<T>::type</code>是否表示一个类型取决于<code>T</code>是什么,这就是为什么编译器会坚持要求你在前面加上<code>typename</code>。</p>
|
||||
<p>如果你尝试过模板元编程(<em>template metaprogramming</em>,TMP), 你一定会碰到取模板类型参数然后基于它创建另一种类型的情况。举个例子,给一个类型<code>T</code>,如果你想去掉<code>T</code>的常量修饰和引用修饰(<code>const</code>- or reference qualifiers),比如你想把<code>const std::string&</code>变成<code>std::string</code>。又或者你想给一个类型加上<code>const</code>或变为左值引用,比如把<code>Widget</code>变成<code>const Widget</code>或<code>Widget&</code>。(如果你没有用过模板元编程,太遗憾了,因为如果你真的想成为一个高效C++程序员,你需要至少熟悉C++在这方面的基本知识。你可以看看在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item27.md">27</a>里的TMP的应用实例,包括我提到的类型转换)。</p>
|
||||
<p>如果你尝试过模板元编程(<em>template metaprogramming</em>,TMP), 你一定会碰到取模板类型参数然后基于它创建另一种类型的情况。举个例子,给一个类型<code>T</code>,如果你想去掉<code>T</code>的常量修饰和引用修饰(<code>const</code>- or reference qualifiers),比如你想把<code>const std::string&</code>变成<code>std::string</code>。又或者你想给一个类型加上<code>const</code>或变为左值引用,比如把<code>Widget</code>变成<code>const Widget</code>或<code>Widget&</code>。(如果你没有用过模板元编程,太遗憾了,因为如果你真的想成为一个高效C++程序员,你需要至少熟悉C++在这方面的基本知识。你可以看看在<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>,<a href="../5.RRefMovSemPerfForw/item27.html">27</a>里的TMP的应用实例,包括我提到的类型转换)。</p>
|
||||
<p>C++11在<em>type traits</em>(类型特性)中给了你一系列工具去实现类型转换,如果要使用这些模板请包含头文件<code><type_traits></code>。里面有许许多多<em>type traits</em>,也不全是类型转换的工具,也包含一些可预测接口的工具。给一个你想施加转换的类型<code>T</code>,结果类型就是<code>std::</code>transformation<code><T>::type</code>,比如:</p>
|
||||
<pre><code class="language-cpp">std::remove_const<T>::type //从const T中产出T
|
||||
std::remove_reference<T>::type //从T&和T&&中产出T
|
||||
|
@ -180,7 +180,7 @@ makeInvestment(Ts&&... params);
|
||||
…
|
||||
} //销毁 *pInvestment
|
||||
</code></pre>
|
||||
<p>但是也可以在所有权转移的场景中使用它,比如将工厂返回的<code>std::unique_ptr</code>移入容器中,然后将容器元素移入一个对象的数据成员中,然后对象过后被销毁。发生这种情况时,这个对象的<code>std::unique_ptr</code>数据成员也被销毁,并且智能指针数据成员的析构将导致从工厂返回的资源被销毁。如果所有权链由于异常或者其他非典型控制流出现中断(比如提前从函数return或者循环中的<code>break</code>),则拥有托管资源的<code>std::unique_ptr</code>将保证指向内容的析构函数被调用,销毁对应资源。(这个规则也有些例外。大多数情况发生于不正常的程序终止。如果一个异常传播到线程的基本函数(比如程序初始线程的<code>main</code>函数)外,或者违反<code>noexcept</code>说明(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md">Item14</a>),局部变量可能不会被销毁;如果<code>std::abort</code>或者退出函数(如<code>std::_Exit</code>,<code>std::exit</code>,或<code>std::quick_exit</code>)被调用,局部变量一定没被销毁。)</p>
|
||||
<p>但是也可以在所有权转移的场景中使用它,比如将工厂返回的<code>std::unique_ptr</code>移入容器中,然后将容器元素移入一个对象的数据成员中,然后对象过后被销毁。发生这种情况时,这个对象的<code>std::unique_ptr</code>数据成员也被销毁,并且智能指针数据成员的析构将导致从工厂返回的资源被销毁。如果所有权链由于异常或者其他非典型控制流出现中断(比如提前从函数return或者循环中的<code>break</code>),则拥有托管资源的<code>std::unique_ptr</code>将保证指向内容的析构函数被调用,销毁对应资源。(这个规则也有些例外。大多数情况发生于不正常的程序终止。如果一个异常传播到线程的基本函数(比如程序初始线程的<code>main</code>函数)外,或者违反<code>noexcept</code>说明(见<a href="../3.MovingToModernCpp/item14.html">Item14</a>),局部变量可能不会被销毁;如果<code>std::abort</code>或者退出函数(如<code>std::_Exit</code>,<code>std::exit</code>,或<code>std::quick_exit</code>)被调用,局部变量一定没被销毁。)</p>
|
||||
<p>默认情况下,销毁将通过<code>delete</code>进行,但是在构造过程中,<code>std::unique_ptr</code>对象可以被设置为使用(对资源的)<strong>自定义删除器</strong>:当资源需要销毁时可调用的任意函数(或者函数对象,包括<em>lambda</em>表达式)。如果通过<code>makeInvestment</code>创建的对象不应仅仅被<code>delete</code>,而应该先写一条日志,<code>makeInvestment</code>可以以如下方式实现。(代码后有说明,别担心有些东西的动机不那么明显。)</p>
|
||||
<pre><code class="language-cpp">auto delInvmt = [](Investment* pInvestment) //自定义删除器
|
||||
{ //(lambda表达式)
|
||||
@ -216,7 +216,7 @@ makeInvestment(Ts&&... params)
|
||||
<p><code>delInvmt</code>是从<code>makeInvestment</code>返回的对象的自定义的删除器。所有的自定义的删除行为接受要销毁对象的原始指针,然后执行所有必要行为实现销毁操作。在上面情况中,操作包括调用<code>makeLogEntry</code>然后应用<code>delete</code>。使用<em>lambda</em>创建<code>delInvmt</code>是方便的,而且,正如稍后看到的,比编写常规的函数更有效。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>当使用自定义删除器时,删除器类型必须作为第二个类型实参传给<code>std::unique_ptr</code>。在上面情况中,就是<code>delInvmt</code>的类型,这就是为什么<code>makeInvestment</code>返回类型是<code>std::unique_ptr<Investment, decltype(delInvmt)></code>。(对于<code>decltype</code>,更多信息查看<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>)</p>
|
||||
<p>当使用自定义删除器时,删除器类型必须作为第二个类型实参传给<code>std::unique_ptr</code>。在上面情况中,就是<code>delInvmt</code>的类型,这就是为什么<code>makeInvestment</code>返回类型是<code>std::unique_ptr<Investment, decltype(delInvmt)></code>。(对于<code>decltype</code>,更多信息查看<a href="../1.DeducingTypes/item3.html">Item3</a>)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>makeInvestment</code>的基本策略是创建一个空的<code>std::unique_ptr</code>,然后指向一个合适类型的对象,然后返回。为了将自定义删除器<code>delInvmt</code>与<code>pInv</code>关联,我们把<code>delInvmt</code>作为<code>pInv</code>构造函数的第二个实参。</p>
|
||||
@ -225,7 +225,7 @@ makeInvestment(Ts&&... params)
|
||||
<p>尝试将原始指针(比如<code>new</code>创建)赋值给<code>std::unique_ptr</code>通不过编译,因为是一种从原始指针到智能指针的隐式转换。这种隐式转换会出问题,所以C++11的智能指针禁止这个行为。这就是通过<code>reset</code>来让<code>pInv</code>接管通过<code>new</code>创建的对象的所有权的原因。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>使用<code>new</code>时,我们使用<code>std::forward</code>把传给<code>makeInvestment</code>的实参完美转发出去(查看<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>)。这使调用者提供的所有信息可用于正在创建的对象的构造函数。</p>
|
||||
<p>使用<code>new</code>时,我们使用<code>std::forward</code>把传给<code>makeInvestment</code>的实参完美转发出去(查看<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>)。这使调用者提供的所有信息可用于正在创建的对象的构造函数。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>自定义删除器的一个形参,类型是<code>Investment*</code>,不管在<code>makeInvestment</code>内部创建的对象的真实类型(如<code>Stock</code>,<code>Bond</code>,或<code>RealEstate</code>)是什么,它最终在<em>lambda</em>表达式中,作为<code>Investment*</code>对象被删除。这意味着我们通过基类指针删除派生类实例,为此,基类<code>Investment</code>必须有虚析构函数:</p>
|
||||
@ -238,7 +238,7 @@ public:
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
<p>在C++14中,函数的返回类型推导存在(参阅<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>),意味着<code>makeInvestment</code>可以以更简单,更封装的方式实现:</p>
|
||||
<p>在C++14中,函数的返回类型推导存在(参阅<a href="../1.DeducingTypes/item3.html">Item3</a>),意味着<code>makeInvestment</code>可以以更简单,更封装的方式实现:</p>
|
||||
<pre><code class="language-cpp">template<typename... Ts>
|
||||
auto makeInvestment(Ts&&... params) //C++14
|
||||
{
|
||||
@ -286,14 +286,14 @@ std::unique_ptr<Investment, void (*)(Investment*)> //Investment*的指针
|
||||
makeInvestment(Ts&&... params); //加至少一个函数指针的大小
|
||||
</code></pre>
|
||||
<p>具有很多状态的自定义删除器会产生大尺寸<code>std::unique_ptr</code>对象。如果你发现自定义删除器使得你的<code>std::unique_ptr</code>变得过大,你需要审视修改你的设计。</p>
|
||||
<p>工厂函数不是<code>std::unique_ptr</code>的唯一常见用法。作为实现<strong>Pimpl Idiom</strong>(译注:<em>pointer to implementation</em>,一种隐藏实际实现而减弱编译依赖性的设计思想,《Effective C++》条款31对此有过叙述)的一种机制,它更为流行。代码并不复杂,但是在某些情况下并不直观,所以这安排在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item22.md">Item22</a>的专门主题中。</p>
|
||||
<p>工厂函数不是<code>std::unique_ptr</code>的唯一常见用法。作为实现<strong>Pimpl Idiom</strong>(译注:<em>pointer to implementation</em>,一种隐藏实际实现而减弱编译依赖性的设计思想,《Effective C++》条款31对此有过叙述)的一种机制,它更为流行。代码并不复杂,但是在某些情况下并不直观,所以这安排在<a href="../4.SmartPointers/item22.html">Item22</a>的专门主题中。</p>
|
||||
<p><code>std::unique_ptr</code>有两种形式,一种用于单个对象(<code>std::unique_ptr<T></code>),一种用于数组(<code>std::unique_ptr<T[]></code>)。结果就是,指向哪种形式没有歧义。<code>std::unique_ptr</code>的API设计会自动匹配你的用法,比如<code>operator[]</code>就是数组对象,解引用操作符(<code>operator*</code>和<code>operator-></code>)就是单个对象专有。</p>
|
||||
<p>你应该对数组的<code>std::unique_ptr</code>的存在兴趣泛泛,因为<code>std::array</code>,<code>std::vector</code>,<code>std::string</code>这些更好用的数据容器应该取代原始数组。<code>std::unique_ptr<T[]></code>有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。</p>
|
||||
<p><code>std::unique_ptr</code>是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为<code>std::shared_ptr</code>:</p>
|
||||
<pre><code class="language-cpp">std::shared_ptr<Investment> sp = //将std::unique_ptr
|
||||
makeInvestment(arguments); //转为std::shared_ptr
|
||||
</code></pre>
|
||||
<p>这就是<code>std::unique_ptr</code>非常适合用作工厂函数返回类型的原因的关键部分。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义,或者共享所有权(即<code>std::shared_ptr</code>)是否更合适。 通过返回<code>std::unique_ptr</code>,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用其更灵活的兄弟替换它。(有关<code>std::shared_ptr</code>的信息,请转到<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item19</a>。)</p>
|
||||
<p>这就是<code>std::unique_ptr</code>非常适合用作工厂函数返回类型的原因的关键部分。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义,或者共享所有权(即<code>std::shared_ptr</code>)是否更合适。 通过返回<code>std::unique_ptr</code>,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用其更灵活的兄弟替换它。(有关<code>std::shared_ptr</code>的信息,请转到<a href="../4.SmartPointers/item19.html">Item19</a>。)</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li><code>std::unique_ptr</code>是轻量级、快速的、只可移动(<em>move-only</em>)的管理专有所有权语义资源的智能指针</li>
|
||||
|
@ -147,12 +147,12 @@
|
||||
<p>引用计数暗示着性能问题:</p>
|
||||
<ul>
|
||||
<li><strong><code>std::shared_ptr</code>大小是原始指针的两倍</strong>,因为它内部包含一个指向资源的原始指针,还包含一个指向资源的引用计数值的原始指针。(这种实现法并不是标准要求的,但是我(指原书作者Scott Meyers)熟悉的所有标准库都这样实现。)</li>
|
||||
<li><strong>引用计数的内存必须动态分配</strong>。 概念上,引用计数与所指对象关联起来,但是实际上被指向的对象不知道这件事情(译注:不知道有一个关联到自己的计数值)。因此它们没有办法存放一个引用计数值。(一个好消息是任何对象——甚至是内置类型的——都可以由<code>std::shared_ptr</code>管理。)<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>会解释使用<code>std::make_shared</code>创建<code>std::shared_ptr</code>可以避免引用计数的动态分配,但是还存在一些<code>std::make_shared</code>不能使用的场景,这时候引用计数就会动态分配。</li>
|
||||
<li><strong>引用计数的内存必须动态分配</strong>。 概念上,引用计数与所指对象关联起来,但是实际上被指向的对象不知道这件事情(译注:不知道有一个关联到自己的计数值)。因此它们没有办法存放一个引用计数值。(一个好消息是任何对象——甚至是内置类型的——都可以由<code>std::shared_ptr</code>管理。)<a href="../4.SmartPointers/item21.html">Item21</a>会解释使用<code>std::make_shared</code>创建<code>std::shared_ptr</code>可以避免引用计数的动态分配,但是还存在一些<code>std::make_shared</code>不能使用的场景,这时候引用计数就会动态分配。</li>
|
||||
<li><strong>递增递减引用计数必须是原子性的</strong>,因为多个reader、writer可能在不同的线程。比如,指向某种资源的<code>std::shared_ptr</code>可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,<code>std::shared_ptr</code>指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个<em>word</em>大小,你也应该假定读写它们是存在开销的。</li>
|
||||
</ul>
|
||||
<p>我写道<code>std::shared_ptr</code>构造函数只是“通常”递增指向对象的引用计数会不会让你有点好奇?创建一个指向对象的<code>std::shared_ptr</code>就产生了又一个指向那个对象的<code>std::shared_ptr</code>,为什么我没说<strong>总是</strong>增加引用计数值?</p>
|
||||
<p>原因是移动构造函数的存在。从另一个<code>std::shared_ptr</code>移动构造新<code>std::shared_ptr</code>会将原来的<code>std::shared_ptr</code>设置为null,那意味着老的<code>std::shared_ptr</code>不再指向资源,同时新的<code>std::shared_ptr</code>指向资源。这样的结果就是不需要修改引用计数值。因此移动<code>std::shared_ptr</code>会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动构造比拷贝构造快,移动赋值运算符也比拷贝赋值运算符快。</p>
|
||||
<p>类似<code>std::unique_ptr</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>),<code>std::shared_ptr</code>使用<code>delete</code>作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于<code>std::unique_ptr</code>。对于<code>std::unique_ptr</code>来说,删除器类型是智能指针类型的一部分。对于<code>std::shared_ptr</code>则不是:</p>
|
||||
<p>类似<code>std::unique_ptr</code>(参见<a href="../4.SmartPointers/item18.html">Item18</a>),<code>std::shared_ptr</code>使用<code>delete</code>作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于<code>std::unique_ptr</code>。对于<code>std::unique_ptr</code>来说,删除器类型是智能指针类型的一部分。对于<code>std::shared_ptr</code>则不是:</p>
|
||||
<pre><code class="language-CPP">auto loggingDel = [](Widget *pw) //自定义删除器
|
||||
{ //(和条款18一样)
|
||||
makeLogEntry(pw);
|
||||
@ -176,13 +176,13 @@ std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
|
||||
</code></pre>
|
||||
<p>它们也能相互赋值,也可以传入一个形参为<code>std::shared_ptr<Widget></code>的函数。但是自定义删除器类型不同的<code>std::unique_ptr</code>就不行,因为<code>std::unique_ptr</code>把删除器视作类型的一部分。</p>
|
||||
<p>另一个不同于<code>std::unique_ptr</code>的地方是,指定自定义删除器不会改变<code>std::shared_ptr</code>对象的大小。不管删除器是什么,一个<code>std::shared_ptr</code>对象都是两个指针大小。这是个好消息,但是它应该让你隐隐约约不安。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。它意味着函数对象是任意大的。<code>std::shared_ptr</code>怎么能引用一个任意大的删除器而不使用更多的内存?</p>
|
||||
<p>它不能。它必须使用更多的内存。然而,那部分内存不是<code>std::shared_ptr</code>对象的一部分。那部分在堆上面,或者<code>std::shared_ptr</code>创建者利用<code>std::shared_ptr</code>对自定义分配器的支持能力,那部分内存随便在哪都行。我前面提到了<code>std::shared_ptr</code>对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做<strong>控制块</strong>(<em>control block</em>)。每个<code>std::shared_ptr</code>管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>提到的,一个次级引用计数<em>weak count</em>,但是目前我们先忽略它。我们可以想象<code>std::shared_ptr</code>对象在内存中是这样:</p>
|
||||
<p>它不能。它必须使用更多的内存。然而,那部分内存不是<code>std::shared_ptr</code>对象的一部分。那部分在堆上面,或者<code>std::shared_ptr</code>创建者利用<code>std::shared_ptr</code>对自定义分配器的支持能力,那部分内存随便在哪都行。我前面提到了<code>std::shared_ptr</code>对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做<strong>控制块</strong>(<em>control block</em>)。每个<code>std::shared_ptr</code>管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如<a href="../4.SmartPointers/item21.html">Item21</a>提到的,一个次级引用计数<em>weak count</em>,但是目前我们先忽略它。我们可以想象<code>std::shared_ptr</code>对象在内存中是这样:</p>
|
||||
<p><img src="media/item19_fig1.png" alt="item19_fig1" /></p>
|
||||
<p>当指向对象的<code>std::shared_ptr</code>一创建,对象的控制块就建立了。至少我们期望是如此。通常,对于一个创建指向对象的<code>std::shared_ptr</code>的函数来说不可能知道是否有其他<code>std::shared_ptr</code>早已指向那个对象,所以控制块的创建会遵循下面几条规则:</p>
|
||||
<ul>
|
||||
<li><strong><code>std::make_shared</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>)总是创建一个控制块</strong>。它创建一个要指向的新对象,所以可以肯定<code>std::make_shared</code>调用时对象不存在其他控制块。</li>
|
||||
<li><strong><code>std::make_shared</code>(参见<a href="../4.SmartPointers/item21.html">Item21</a>)总是创建一个控制块</strong>。它创建一个要指向的新对象,所以可以肯定<code>std::make_shared</code>调用时对象不存在其他控制块。</li>
|
||||
<li><strong>当从独占指针(即<code>std::unique_ptr</code>或者<code>std::auto_ptr</code>)上构造出<code>std::shared_ptr</code>时会创建控制块</strong>。独占指针没有使用控制块,所以指针指向的对象没有关联控制块。(作为构造的一部分,<code>std::shared_ptr</code>侵占独占指针所指向的对象的独占权,所以独占指针被设置为null)</li>
|
||||
<li><strong>当从原始指针上构造出<code>std::shared_ptr</code>时会创建控制块</strong>。如果你想从一个早已存在控制块的对象上创建<code>std::shared_ptr</code>,你将假定传递一个<code>std::shared_ptr</code>或者<code>std::weak_ptr</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md">Item20</a>)作为构造函数实参,而不是原始指针。用<code>std::shared_ptr</code>或者<code>std::weak_ptr</code>作为构造函数实参创建<code>std::shared_ptr</code>不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。</li>
|
||||
<li><strong>当从原始指针上构造出<code>std::shared_ptr</code>时会创建控制块</strong>。如果你想从一个早已存在控制块的对象上创建<code>std::shared_ptr</code>,你将假定传递一个<code>std::shared_ptr</code>或者<code>std::weak_ptr</code>(参见<a href="../4.SmartPointers/item20.html">Item20</a>)作为构造函数实参,而不是原始指针。用<code>std::shared_ptr</code>或者<code>std::weak_ptr</code>作为构造函数实参创建<code>std::shared_ptr</code>不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。</li>
|
||||
</ul>
|
||||
<p>这些规则造成的后果就是从原始指针上构造超过一个<code>std::shared_ptr</code>就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。那意味着像下面的代码是有问题的,很有问题,问题很大:</p>
|
||||
<pre><code class="language-cpp">auto pw = new Widget; //pw是原始指针
|
||||
@ -191,9 +191,9 @@ std::shared_ptr<Widget> spw1(pw, loggingDel); //为*pw创建控制块
|
||||
…
|
||||
std::shared_ptr<Widget> spw2(pw, loggingDel); //为*pw创建第二个控制块
|
||||
</code></pre>
|
||||
<p>创建原始指针<code>pw</code>指向动态分配的对象很糟糕,因为它完全背离了这章的建议:倾向于使用智能指针而不是原始指针。(如果你忘记了该建议的动机,请翻到<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">本章开头</a>)。撇开那个不说,创建<code>pw</code>那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。</p>
|
||||
<p>创建原始指针<code>pw</code>指向动态分配的对象很糟糕,因为它完全背离了这章的建议:倾向于使用智能指针而不是原始指针。(如果你忘记了该建议的动机,请翻到<a href="../4.SmartPointers/item18.html">本章开头</a>)。撇开那个不说,创建<code>pw</code>那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。</p>
|
||||
<p>现在,传给<code>spw1</code>的构造函数一个原始指针,它会为指向的对象创建一个控制块(因此有个引用计数值)。这种情况下,指向的对象是<code>*pw</code>(即<code>pw</code>指向的对象)。就其本身而言没什么问题,但是将同样的原始指针传递给<code>spw2</code>的构造函数会再次为<code>*pw</code>创建一个控制块(所以也有个引用计数值)。因此<code>*pw</code>有两个引用计数值,每一个最后都会变成零,然后最终导致<code>*pw</code>销毁两次。第二个销毁会产生未定义行为。</p>
|
||||
<p><code>std::shared_ptr</code>给我们上了两堂课。第一,避免传给<code>std::shared_ptr</code>构造函数原始指针。通常替代方案是使用<code>std::make_shared</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>),不过上面例子中,我们使用了自定义删除器,用<code>std::make_shared</code>就没办法做到。第二,如果你必须传给<code>std::shared_ptr</code>构造函数原始指针,直接传<code>new</code>出来的结果,不要传指针变量。如果上面代码第一部分这样重写:</p>
|
||||
<p><code>std::shared_ptr</code>给我们上了两堂课。第一,避免传给<code>std::shared_ptr</code>构造函数原始指针。通常替代方案是使用<code>std::make_shared</code>(参见<a href="../4.SmartPointers/item21.html">Item21</a>),不过上面例子中,我们使用了自定义删除器,用<code>std::make_shared</code>就没办法做到。第二,如果你必须传给<code>std::shared_ptr</code>构造函数原始指针,直接传<code>new</code>出来的结果,不要传指针变量。如果上面代码第一部分这样重写:</p>
|
||||
<pre><code class="language-cpp">std::shared_ptr<Widget> spw1(new Widget, //直接使用new的结果
|
||||
loggingDel);
|
||||
</code></pre>
|
||||
@ -218,7 +218,7 @@ public:
|
||||
processedWidgets.emplace_back(this); //然后将它加到已处理过的Widget
|
||||
} //的列表中,这是错的!
|
||||
</code></pre>
|
||||
<p>注释已经说了这是错的——或者至少大部分是错的。(错误的部分是传递<code>this</code>,而不是使用了<code>emplace_back</code>。如果你不熟悉<code>emplace_back</code>,参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item42.md">Item42</a>)。上面的代码可以通过编译,但是向<code>std::shared_ptr</code>的容器传递一个原始指针(<code>this</code>),<code>std::shared_ptr</code>会由此为指向的<code>Widget</code>(<code>*this</code>)创建一个控制块。那看起来没什么问题,直到你意识到如果成员函数外面早已存在指向那个<code>Widget</code>对象的指针,它是未定义行为的Game, Set, and Match(译注:一部关于网球的电影,但是译者没看过。句子本意“压倒性胜利;比赛结束”)。</p>
|
||||
<p>注释已经说了这是错的——或者至少大部分是错的。(错误的部分是传递<code>this</code>,而不是使用了<code>emplace_back</code>。如果你不熟悉<code>emplace_back</code>,参见<a href="../8.Tweaks/item42.html">Item42</a>)。上面的代码可以通过编译,但是向<code>std::shared_ptr</code>的容器传递一个原始指针(<code>this</code>),<code>std::shared_ptr</code>会由此为指向的<code>Widget</code>(<code>*this</code>)创建一个控制块。那看起来没什么问题,直到你意识到如果成员函数外面早已存在指向那个<code>Widget</code>对象的指针,它是未定义行为的Game, Set, and Match(译注:一部关于网球的电影,但是译者没看过。句子本意“压倒性胜利;比赛结束”)。</p>
|
||||
<p><code>std::shared_ptr</code>API已有处理这种情况的设施。它的名字可能是C++标准库中最奇怪的一个:<code>std::enable_shared_from_this</code>。如果你想创建一个用<code>std::shared_ptr</code>管理的类,这个类能够用<code>this</code>指针安全地创建一个<code>std::shared_ptr</code>,<code>std::enable_shared_from_this</code>就可作为基类的模板类。在我们的例子中,<code>Widget</code>将会继承自<code>std::enable_shared_from_this</code>:</p>
|
||||
<pre><code class="language-cpp">class Widget: public std::enable_shared_from_this<Widget> {
|
||||
public:
|
||||
@ -253,7 +253,7 @@ private:
|
||||
</code></pre>
|
||||
<p>现在,你可能隐约记得我们讨论控制块的动机是想了解有关<code>std::shared_ptr</code>的成本。既然我们已经知道了怎么避免创建过多控制块,就让我们回到原来的主题。</p>
|
||||
<p>控制块通常只占几个<em>word</em>大小,自定义删除器和分配器可能会让它变大一点。通常控制块的实现比你想的更复杂一些。它使用继承,甚至里面还有一个虚函数(用来确保指向的对象被正确销毁)。这意味着使用<code>std::shared_ptr</code>还会招致控制块使用虚函数带来的成本。</p>
|
||||
<p>了解了动态分配控制块,任意大小的删除器和分配器,虚函数机制,原子性的引用计数修改,你对于<code>std::shared_ptr</code>的热情可能有点消退。可以理解,对每个资源管理问题来说都没有最佳的解决方案。但就它提供的功能来说,<code>std::shared_ptr</code>的开销是非常合理的。在通常情况下,使用默认删除器和默认分配器,使用<code>std::make_shared</code>创建<code>std::shared_ptr</code>,产生的控制块只需三个word大小。它的分配基本上是无开销的。(开销被并入了指向的对象的分配成本里。细节参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>)。对<code>std::shared_ptr</code>解引用的开销不会比原始指针高。执行需要原子引用计数修改的操作需要承担一两个原子操作开销,这些操作通常都会一一映射到机器指令上,所以即使对比非原子指令来说,原子指令开销较大,但是它们仍然只是单个指令上的。对于每个被<code>std::shared_ptr</code>指向的对象来说,控制块中的虚函数机制产生的开销通常只需要承受一次,即对象销毁的时候。</p>
|
||||
<p>了解了动态分配控制块,任意大小的删除器和分配器,虚函数机制,原子性的引用计数修改,你对于<code>std::shared_ptr</code>的热情可能有点消退。可以理解,对每个资源管理问题来说都没有最佳的解决方案。但就它提供的功能来说,<code>std::shared_ptr</code>的开销是非常合理的。在通常情况下,使用默认删除器和默认分配器,使用<code>std::make_shared</code>创建<code>std::shared_ptr</code>,产生的控制块只需三个word大小。它的分配基本上是无开销的。(开销被并入了指向的对象的分配成本里。细节参见<a href="../4.SmartPointers/item21.html">Item21</a>)。对<code>std::shared_ptr</code>解引用的开销不会比原始指针高。执行需要原子引用计数修改的操作需要承担一两个原子操作开销,这些操作通常都会一一映射到机器指令上,所以即使对比非原子指令来说,原子指令开销较大,但是它们仍然只是单个指令上的。对于每个被<code>std::shared_ptr</code>指向的对象来说,控制块中的虚函数机制产生的开销通常只需要承受一次,即对象销毁的时候。</p>
|
||||
<p>作为这些轻微开销的交换,你得到了动态分配的资源的生命周期自动管理的好处。大多数时候,比起手动管理,使用<code>std::shared_ptr</code>管理共享性资源都是非常合适的。如果你还在犹豫是否能承受<code>std::shared_ptr</code>带来的开销,那就再想想你是否需要共享所有权。如果独占资源可行或者<strong>可能</strong>可行,用<code>std::unique_ptr</code>是一个更好的选择。它的性能表现更接近于原始指针,并且从<code>std::unique_ptr</code>升级到<code>std::shared_ptr</code>也很容易,因为<code>std::shared_ptr</code>可以从<code>std::unique_ptr</code>上创建。</p>
|
||||
<p>反之不行。当你的资源由<code>std::shared_ptr</code>管理,现在又想修改资源生命周期管理方式是没有办法的。即使引用计数为一,你也不能重新修改资源所有权,改用<code>std::unique_ptr</code>管理它。资源和指向它的<code>std::shared_ptr</code>的签订的所有权协议是“除非死亡否则永不分开”。不能分离,不能废除,没有特许。</p>
|
||||
<p><code>std::shared_ptr</code>不能处理的另一个东西是数组。和<code>std::unique_ptr</code>不同的是,<code>std::shared_ptr</code>的API设计之初就是针对单个对象的,没有办法<code>std::shared_ptr<T[]></code>。一次又一次,“聪明”的程序员踌躇于是否该使用<code>std::shared_ptr<T></code>指向数组,然后传入自定义删除器来删除数组(即<code>delete []</code>)。这可以通过编译,但是是一个糟糕的主意。一方面,<code>std::shared_ptr</code>没有提供<code>operator[]</code>,所以数组索引操作需要借助怪异的指针算术。另一方面,<code>std::shared_ptr</code>支持转换为指向基类的指针,这对于单个对象来说有效,但是当用于数组类型时相当于在类型系统上开洞。(出于这个原因,<code>std::unique_ptr<T[]></code> API禁止这种转换。)更重要的是,C++11已经提供了很多内置数组的候选方案(比如<code>std::array</code>,<code>std::vector</code>,<code>std::string</code>)。声明一个指向傻瓜数组的智能指针(译注:也是”聪明的指针“之意)几乎总是表示着糟糕的设计。</p>
|
||||
|
@ -141,7 +141,7 @@
|
||||
<main>
|
||||
<h2 id="条款二十当stdshared_ptr可能悬空时使用stdweak_ptr"><a class="header" href="#条款二十当stdshared_ptr可能悬空时使用stdweak_ptr">条款二十:当<code>std::shared_ptr</code>可能悬空时使用<code>std::weak_ptr</code></a></h2>
|
||||
<p><strong>Item 20: Use <code>std::weak_ptr</code> for <code>std::shared_ptr</code>-like pointers that can dangle</strong></p>
|
||||
<p>自相矛盾的是,如果有一个像<code>std::shared_ptr</code>(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item19</a>)的但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似<code>std::shared_ptr</code>但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个<code>std::shared_ptr</code>不存在的问题:可能指向已经销毁的对象。一个真正的智能指针应该跟踪所指对象,在悬空时知晓,悬空(<em>dangle</em>)就是指针指向的对象不再存在。这就是对<code>std::weak_ptr</code>最精确的描述。</p>
|
||||
<p>自相矛盾的是,如果有一个像<code>std::shared_ptr</code>(见<a href="../4.SmartPointers/item19.html">Item19</a>)的但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似<code>std::shared_ptr</code>但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个<code>std::shared_ptr</code>不存在的问题:可能指向已经销毁的对象。一个真正的智能指针应该跟踪所指对象,在悬空时知晓,悬空(<em>dangle</em>)就是指针指向的对象不再存在。这就是对<code>std::weak_ptr</code>最精确的描述。</p>
|
||||
<p>你可能想知道什么时候该用<code>std::weak_ptr</code>。你可能想知道关于<code>std::weak_ptr</code> API的更多。它什么都好除了不太智能。<code>std::weak_ptr</code>不能解引用,也不能测试是否为空值。因为<code>std::weak_ptr</code>不是一个独立的智能指针。它是<code>std::shared_ptr</code>的增强。</p>
|
||||
<p>这种关系在它创建之时就建立了。<code>std::weak_ptr</code>通常从<code>std::shared_ptr</code>上创建。当从<code>std::shared_ptr</code>上创建<code>std::weak_ptr</code>时两者指向相同的对象,但是<code>std::weak_ptr</code>不会影响所指对象的引用计数:</p>
|
||||
<pre><code class="language-cpp">auto spw = //spw创建之后,指向的Widget的
|
||||
@ -165,7 +165,7 @@ auto spw2 = wpw.lock(); //同上,但是使用auto
|
||||
<p>另一种形式是以<code>std::weak_ptr</code>为实参构造<code>std::shared_ptr</code>。这种情况中,如果<code>std::weak_ptr</code>过期,会抛出一个异常:</p>
|
||||
<pre><code class="language-cpp">std::shared_ptr<Widget> spw3(wpw); //如果wpw过期,抛出std::bad_weak_ptr异常
|
||||
</code></pre>
|
||||
<p>但是你可能还想知道为什么<code>std::weak_ptr</code>就有用了。考虑一个工厂函数,它基于一个唯一ID从只读对象上产出智能指针。根据<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item18</a>的描述,工厂函数会返回一个该对象类型的<code>std::unique_ptr</code>:</p>
|
||||
<p>但是你可能还想知道为什么<code>std::weak_ptr</code>就有用了。考虑一个工厂函数,它基于一个唯一ID从只读对象上产出智能指针。根据<a href="../4.SmartPointers/item19.html">Item18</a>的描述,工厂函数会返回一个该对象类型的<code>std::unique_ptr</code>:</p>
|
||||
<pre><code class="language-cpp">std::unique_ptr<const Widget> loadWidget(WidgetID id);
|
||||
</code></pre>
|
||||
<p>如果调用<code>loadWidget</code>是一个昂贵的操作(比如它操作文件或者数据库I/O)并且重复使用ID很常见,一个合理的优化是再写一个函数除了完成<code>loadWidget</code>做的事情之外再缓存它的结果。当每个请求获取的<code>Widget</code>阻塞了缓存也会导致本身性能问题,所以另一个合理的优化可以是当<code>Widget</code>不再使用的时候销毁它的缓存。</p>
|
||||
@ -201,7 +201,7 @@ auto spw2 = wpw.lock(); //同上,但是使用auto
|
||||
</ul>
|
||||
<p>使用<code>std::weak_ptr</code>显然是这些选择中最好的。但是,需要注意使用<code>std::weak_ptr</code>打破<code>std::shared_ptr</code>循环并不常见。在严格分层的数据结构比如树中,子节点只被父节点持有。当父节点被销毁时,子节点就被销毁。从父到子的链接关系可以使用<code>std::unique_ptr</code>很好的表征。从子到父的反向连接可以使用原始指针安全实现,因为子节点的生命周期肯定短于父节点。因此没有子节点解引用一个悬垂的父节点指针这样的风险。</p>
|
||||
<p>当然,不是所有的使用指针的数据结构都是严格分层的,所以当发生这种情况时,比如上面所述缓存和观察者列表的实现之类的,知道<code>std::weak_ptr</code>随时待命也是不错的。</p>
|
||||
<p>从效率角度来看,<code>std::weak_ptr</code>与<code>std::shared_ptr</code>基本相同。两者的大小是相同的,使用相同的控制块(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item19</a>),构造、析构、赋值操作涉及引用计数的原子操作。这可能让你感到惊讶,因为本条款开篇就提到<code>std::weak_ptr</code>不影响引用计数。我写的是<code>std::weak_ptr</code>不参与对象的<strong>共享所有权</strong>,因此不影响<strong>指向对象的引用计数</strong>。实际上在控制块中还是有第二个引用计数,<code>std::weak_ptr</code>操作的是第二个引用计数。想了解细节的话,继续看<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>吧。</p>
|
||||
<p>从效率角度来看,<code>std::weak_ptr</code>与<code>std::shared_ptr</code>基本相同。两者的大小是相同的,使用相同的控制块(参见<a href="../4.SmartPointers/item19.html">Item19</a>),构造、析构、赋值操作涉及引用计数的原子操作。这可能让你感到惊讶,因为本条款开篇就提到<code>std::weak_ptr</code>不影响引用计数。我写的是<code>std::weak_ptr</code>不参与对象的<strong>共享所有权</strong>,因此不影响<strong>指向对象的引用计数</strong>。实际上在控制块中还是有第二个引用计数,<code>std::weak_ptr</code>操作的是第二个引用计数。想了解细节的话,继续看<a href="../4.SmartPointers/item21.html">Item21</a>吧。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>用<code>std::weak_ptr</code>替代可能会悬空的<code>std::shared_ptr</code>。</li>
|
||||
|
@ -148,7 +148,7 @@ std::unique_ptr<T> make_unique(Ts&&... params)
|
||||
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
|
||||
}
|
||||
</code></pre>
|
||||
<p>正如你看到的,<code>make_unique</code>只是将它的参数完美转发到所要创建的对象的构造函数,从<code>new</code>产生的原始指针里面构造出<code>std::unique_ptr</code>,并返回这个<code>std::unique_ptr</code>。这种形式的函数不支持数组和自定义析构(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>),但它给出了一个示范:只需一点努力就能写出你想要的<code>make_unique</code>函数。(要想实现一个特性完备的<code>make_unique</code>,就去找提供这个的标准化文件吧,然后拷贝那个实现。你想要的这个文件是N3656,是Stephan T. Lavavej写于2013-04-18的文档。)需要记住的是,不要把它放到<code>std</code>命名空间中,因为你可能并不希望看到升级C++14标准库的时候你放进<code>std</code>命名空间的内容和编译器供应商提供的<code>std</code>命名空间的内容发生冲突。</p>
|
||||
<p>正如你看到的,<code>make_unique</code>只是将它的参数完美转发到所要创建的对象的构造函数,从<code>new</code>产生的原始指针里面构造出<code>std::unique_ptr</code>,并返回这个<code>std::unique_ptr</code>。这种形式的函数不支持数组和自定义析构(见<a href="../4.SmartPointers/item18.html">Item18</a>),但它给出了一个示范:只需一点努力就能写出你想要的<code>make_unique</code>函数。(要想实现一个特性完备的<code>make_unique</code>,就去找提供这个的标准化文件吧,然后拷贝那个实现。你想要的这个文件是N3656,是Stephan T. Lavavej写于2013-04-18的文档。)需要记住的是,不要把它放到<code>std</code>命名空间中,因为你可能并不希望看到升级C++14标准库的时候你放进<code>std</code>命名空间的内容和编译器供应商提供的<code>std</code>命名空间的内容发生冲突。</p>
|
||||
<p><code>std::make_unique</code>和<code>std::make_shared</code>是三个<strong>make函数</strong> 中的两个:接收任意的多参数集合,完美转发到构造函数去动态分配一个对象,然后返回这个指向这个对象的指针。第三个<code>make</code>函数是<code>std::allocate_shared</code>。它行为和<code>std::make_shared</code>一样,只不过第一个参数是用来动态分配内存的<em>allocator</em>对象。</p>
|
||||
<p>即使通过用和不用<code>make</code>函数来创建智能指针的一个小小比较,也揭示了为何使用<code>make</code>函数更好的第一个原因。例如:</p>
|
||||
<pre><code class="language-c++">auto upw1(std::make_unique<Widget>()); //使用make函数
|
||||
@ -160,7 +160,7 @@ std::shared_ptr<Widget> spw2(new Widget); //不使用make函数
|
||||
<p>第二个使用<code>make</code>函数的原因和异常安全有关。假设我们有个函数按照某种优先级处理<code>Widget</code>:</p>
|
||||
<pre><code class="language-c++">void processWidget(std::shared_ptr<Widget> spw, int priority);
|
||||
</code></pre>
|
||||
<p>值传递<code>std::shared_ptr</code>可能看起来很可疑,但是<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md">Item41</a>解释了,如果<code>processWidget</code>总是复制<code>std::shared_ptr</code>(例如,通过将其存储在已处理的<code>Widget</code>的一个数据结构中),那么这可能是一个合理的设计选择。</p>
|
||||
<p>值传递<code>std::shared_ptr</code>可能看起来很可疑,但是<a href="../8.Tweaks/item41.html">Item41</a>解释了,如果<code>processWidget</code>总是复制<code>std::shared_ptr</code>(例如,通过将其存储在已处理的<code>Widget</code>的一个数据结构中),那么这可能是一个合理的设计选择。</p>
|
||||
<p>现在假设我们有一个函数来计算相关的优先级,</p>
|
||||
<pre><code class="language-c++">int computePriority();
|
||||
</code></pre>
|
||||
@ -191,14 +191,14 @@ std::shared_ptr<Widget> spw2(new Widget); //不使用make函数
|
||||
<p><code>std::make_shared</code>的一个特性(与直接使用<code>new</code>相比)是效率提升。使用<code>std::make_shared</code>允许编译器生成更小,更快的代码,并使用更简洁的数据结构。考虑以下对new的直接使用:</p>
|
||||
<pre><code class="language-c++">std::shared_ptr<Widget> spw(new Widget);
|
||||
</code></pre>
|
||||
<p>显然,这段代码需要进行内存分配,但它实际上执行了两次。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item19</a>解释了每个<code>std::shared_ptr</code>指向一个控制块,其中包含被指向对象的引用计数,还有其他东西。这个控制块的内存在<code>std::shared_ptr</code>构造函数中分配。因此,直接使用<code>new</code>需要为<code>Widget</code>进行一次内存分配,为控制块再进行一次内存分配。</p>
|
||||
<p>显然,这段代码需要进行内存分配,但它实际上执行了两次。<a href="../4.SmartPointers/item19.html">Item19</a>解释了每个<code>std::shared_ptr</code>指向一个控制块,其中包含被指向对象的引用计数,还有其他东西。这个控制块的内存在<code>std::shared_ptr</code>构造函数中分配。因此,直接使用<code>new</code>需要为<code>Widget</code>进行一次内存分配,为控制块再进行一次内存分配。</p>
|
||||
<p>如果使用<code>std::make_shared</code>代替:</p>
|
||||
<pre><code class="language-c++">auto spw = std::make_shared<Widget>();
|
||||
</code></pre>
|
||||
<p>一次分配足矣。这是因为<code>std::make_shared</code>分配一块内存,同时容纳了<code>Widget</code>对象和控制块。这种优化减少了程序的静态大小,因为代码只包含一个内存分配调用,并且它提高了可执行代码的速度,因为内存只分配一次。此外,使用<code>std::make_shared</code>避免了对控制块中的某些簿记信息的需要,潜在地减少了程序的总内存占用。</p>
|
||||
<p>对于<code>std::make_shared</code>的效率分析同样适用于<code>std::allocate_shared</code>,因此<code>std::make_shared</code>的性能优势也扩展到了该函数。</p>
|
||||
<p>更倾向于使用<code>make</code>函数而不是直接使用<code>new</code>的争论非常激烈。尽管它们在软件工程、异常安全和效率方面具有优势,但本条款的建议是,更<strong>倾向于</strong>使用<code>make</code>函数,而不是完全依赖于它们。这是因为有些情况下它们不能或不应该被使用。</p>
|
||||
<p>例如,<code>make</code>函数都不允许指定自定义删除器(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>和<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">19</a>),但是<code>std::unique_ptr</code>和<code>std::shared_ptr</code>有构造函数这么做。有个<code>Widget</code>的自定义删除器:</p>
|
||||
<p>例如,<code>make</code>函数都不允许指定自定义删除器(见<a href="../4.SmartPointers/item18.html">Item18</a>和<a href="../4.SmartPointers/item19.html">19</a>),但是<code>std::unique_ptr</code>和<code>std::shared_ptr</code>有构造函数这么做。有个<code>Widget</code>的自定义删除器:</p>
|
||||
<pre><code class="language-cpp">auto widgetDeleter = [](Widget* pw) { … };
|
||||
</code></pre>
|
||||
<p>创建一个使用它的智能指针只能直接使用<code>new</code>:</p>
|
||||
@ -208,12 +208,12 @@ std::shared_ptr<Widget> spw2(new Widget); //不使用make函数
|
||||
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
|
||||
</code></pre>
|
||||
<p>对于<code>make</code>函数,没有办法做同样的事情。</p>
|
||||
<p><code>make</code>函数第二个限制来自于其实现中的语法细节。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item7.md">Item7</a>解释了,当构造函数重载,有使用<code>std::initializer_list</code>作为参数的重载形式和不用其作为参数的的重载形式,用花括号创建的对象更倾向于使用<code>std::initializer_list</code>作为形参的重载形式,而用小括号创建对象将调用不用<code>std::initializer_list</code>作为参数的的重载形式。<code>make</code>函数会将它们的参数完美转发给对象构造函数,但是它们是使用小括号还是花括号?对某些类型,问题的答案会很不相同。例如,在这些调用中,</p>
|
||||
<p><code>make</code>函数第二个限制来自于其实现中的语法细节。<a href="../3.MovingToModernCpp/item7.html">Item7</a>解释了,当构造函数重载,有使用<code>std::initializer_list</code>作为参数的重载形式和不用其作为参数的的重载形式,用花括号创建的对象更倾向于使用<code>std::initializer_list</code>作为形参的重载形式,而用小括号创建对象将调用不用<code>std::initializer_list</code>作为参数的的重载形式。<code>make</code>函数会将它们的参数完美转发给对象构造函数,但是它们是使用小括号还是花括号?对某些类型,问题的答案会很不相同。例如,在这些调用中,</p>
|
||||
<pre><code class="language-cpp">auto upv = std::make_unique<std::vector<int>>(10, 20);
|
||||
auto spv = std::make_shared<std::vector<int>>(10, 20);
|
||||
</code></pre>
|
||||
<p>生成的智能指针指向带有10个元素的<code>std::vector</code>,每个元素值为20,还是指向带有两个元素的<code>std::vector</code>,其中一个元素值10,另一个为20?或者结果是不确定的?</p>
|
||||
<p>好消息是这并非不确定:两种调用都创建了10个元素,每个值为20的<code>std::vector</code>。这意味着在<code>make</code>函数中,完美转发使用小括号,而不是花括号。坏消息是如果你想用花括号初始化指向的对象,你必须直接使用<code>new</code>。使用<code>make</code>函数会需要能够完美转发花括号初始化的能力,但是,正如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>所说,花括号初始化无法完美转发。但是,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>介绍了一个变通的方法:使用<code>auto</code>类型推导从花括号初始化创建<code>std::initializer_list</code>对象(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>),然后将<code>auto</code>创建的对象传递给<code>make</code>函数。</p>
|
||||
<p>好消息是这并非不确定:两种调用都创建了10个元素,每个值为20的<code>std::vector</code>。这意味着在<code>make</code>函数中,完美转发使用小括号,而不是花括号。坏消息是如果你想用花括号初始化指向的对象,你必须直接使用<code>new</code>。使用<code>make</code>函数会需要能够完美转发花括号初始化的能力,但是,正如<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>所说,花括号初始化无法完美转发。但是,<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>介绍了一个变通的方法:使用<code>auto</code>类型推导从花括号初始化创建<code>std::initializer_list</code>对象(见<a href="../1.DeducingTypes/item2.html">Item2</a>),然后将<code>auto</code>创建的对象传递给<code>make</code>函数。</p>
|
||||
<pre><code class="language-cpp">//创建std::initializer_list
|
||||
auto initList = { 10, 20 };
|
||||
//使用std::initializer_list为形参的构造函数创建std::vector
|
||||
@ -222,7 +222,7 @@ auto spv = std::make_shared<std::vector<int>>(initList);
|
||||
<p>对于<code>std::unique_ptr</code>,只有这两种情景(自定义删除器和花括号初始化)使用<code>make</code>函数有点问题。对于<code>std::shared_ptr</code>和它的<code>make</code>函数,还有2个问题。都属于边缘情况,但是一些开发者常碰到,你也可能是其中之一。</p>
|
||||
<p>一些类重载了<code>operator new</code>和<code>operator delete</code>。这些函数的存在意味着对这些类型的对象的全局内存分配和释放是不合常规的。设计这种定制操作往往只会精确的分配、释放对象大小的内存。例如,<code>Widget</code>类的<code>operator new</code>和<code>operator delete</code>只会处理<code>sizeof(Widget)</code>大小的内存块的分配和释放。这种系列行为不太适用于<code>std::shared_ptr</code>对自定义分配(通过<code>std::allocate_shared</code>)和释放(通过自定义删除器)的支持,因为<code>std::allocate_shared</code>需要的内存总大小不等于动态分配的对象大小,还需要<strong>再加上</strong>控制块大小。因此,使用<code>make</code>函数去创建重载了<code>operator new</code>和<code>operator delete</code>类的对象是个典型的糟糕想法。</p>
|
||||
<p>与直接使用<code>new</code>相比,<code>std::make_shared</code>在大小和速度上的优势源于<code>std::shared_ptr</code>的控制块与指向的对象放在同一块内存中。当对象的引用计数降为0,对象被销毁(即析构函数被调用)。但是,因为控制块和对象被放在同一块分配的内存块中,直到控制块的内存也被销毁,对象占用的内存才被释放。</p>
|
||||
<p>正如我说,控制块除了引用计数,还包含簿记信息。引用计数追踪有多少<code>std::shared_ptr</code>s指向控制块,但控制块还有第二个计数,记录多少个<code>std::weak_ptr</code>s指向控制块。第二个引用计数就是<em>weak count</em>。(实际上,<em>weak count</em>的值不总是等于指向控制块的<code>std::weak_ptr</code>的数目,因为库的实现者找到一些方法在<em>weak count</em>中添加附加信息,促进更好的代码产生。为了本条款的目的,我们会忽略这一点,假定<em>weak count</em>的值等于指向控制块的<code>std::weak_ptr</code>的数目。)当一个<code>std::weak_ptr</code>检测它是否过期时(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item19</a>),它会检测指向的控制块中的引用计数(而不是<em>weak count</em>)。如果引用计数是0(即对象没有<code>std::shared_ptr</code>再指向它,已经被销毁了),<code>std::weak_ptr</code>就已经过期。否则就没过期。</p>
|
||||
<p>正如我说,控制块除了引用计数,还包含簿记信息。引用计数追踪有多少<code>std::shared_ptr</code>s指向控制块,但控制块还有第二个计数,记录多少个<code>std::weak_ptr</code>s指向控制块。第二个引用计数就是<em>weak count</em>。(实际上,<em>weak count</em>的值不总是等于指向控制块的<code>std::weak_ptr</code>的数目,因为库的实现者找到一些方法在<em>weak count</em>中添加附加信息,促进更好的代码产生。为了本条款的目的,我们会忽略这一点,假定<em>weak count</em>的值等于指向控制块的<code>std::weak_ptr</code>的数目。)当一个<code>std::weak_ptr</code>检测它是否过期时(见<a href="../4.SmartPointers/item19.html">Item19</a>),它会检测指向的控制块中的引用计数(而不是<em>weak count</em>)。如果引用计数是0(即对象没有<code>std::shared_ptr</code>再指向它,已经被销毁了),<code>std::weak_ptr</code>就已经过期。否则就没过期。</p>
|
||||
<p>只要<code>std::weak_ptr</code>s引用一个控制块(即<em>weak count</em>大于零),该控制块必须继续存在。只要控制块存在,包含它的内存就必须保持分配。通过<code>std::shared_ptr</code>的<code>make</code>函数分配的内存,直到最后一个<code>std::shared_ptr</code>和最后一个指向它的<code>std::weak_ptr</code>已被销毁,才会释放。</p>
|
||||
<p>如果对象类型非常大,而且销毁最后一个<code>std::shared_ptr</code>和销毁最后一个<code>std::weak_ptr</code>之间的时间很长,那么在销毁对象和释放它所占用的内存之间可能会出现延迟。</p>
|
||||
<pre><code class="language-c++">class ReallyBigType { … };
|
||||
@ -286,7 +286,7 @@ processWidget(spw, computePriority()); // 正确,但是没优化,见下
|
||||
<p>但是在异常安全调用中,我们传递了左值:</p>
|
||||
<pre><code class="language-c++">processWidget(spw, computePriority()); //实参是左值
|
||||
</code></pre>
|
||||
<p>因为<code>processWidget</code>的<code>std::shared_ptr</code>形参是传值,从右值构造只需要移动,而传递左值构造需要拷贝。对<code>std::shared_ptr</code>而言,这种区别是有意义的,因为拷贝<code>std::shared_ptr</code>需要对引用计数原子递增,移动则不需要对引用计数有操作。为了使异常安全代码达到非异常安全代码的性能水平,我们需要用<code>std::move</code>将<code>spw</code>转换为右值(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>):</p>
|
||||
<p>因为<code>processWidget</code>的<code>std::shared_ptr</code>形参是传值,从右值构造只需要移动,而传递左值构造需要拷贝。对<code>std::shared_ptr</code>而言,这种区别是有意义的,因为拷贝<code>std::shared_ptr</code>需要对引用计数原子递增,移动则不需要对引用计数有操作。为了使异常安全代码达到非异常安全代码的性能水平,我们需要用<code>std::move</code>将<code>spw</code>转换为右值(见<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>):</p>
|
||||
<pre><code class="language-c++">processWidget(std::move(spw), computePriority()); //高效且异常安全
|
||||
</code></pre>
|
||||
<p>这很有趣,也值得了解,但通常是无关紧要的,因为您很少有理由不使用<code>make</code>函数。除非你有令人信服的理由这样做,否则你应该使用<code>make</code>函数。</p>
|
||||
|
@ -188,7 +188,7 @@ Widget::~Widget() //销毁数据成员
|
||||
{ delete pImpl; }
|
||||
</code></pre>
|
||||
<p>在这里我把<code>#include</code>命令写出来是为了明确一点,对于<code>std::string</code>,<code>std::vector</code>和<code>Gadget</code>的头文件的整体依赖依然存在。 然而,这些依赖从头文件<code>widget.h</code>(它被所有<code>Widget</code>类的使用者包含,并且对他们可见)移动到了<code>widget.cpp</code>(该文件只被<code>Widget</code>类的实现者包含,并只对他可见)。 我高亮了其中动态分配和回收<code>Impl</code>对象的部分(译者注:markdown高亮不了,实际高亮的是<code>new Impl</code>和<code>delete pImpl;</code>两个语句)。这就是为什么我们需要<code>Widget</code>的析构函数——我们需要<code>Widget</code>被销毁时回收该对象。</p>
|
||||
<p>但是,我展示给你们看的是一段C++98的代码,散发着一股已经过去了几千年的腐朽气息。 它使用了原始指针,原始的<code>new</code>和原始的<code>delete</code>,一切都让它如此的...原始。这一章建立在“智能指针比原始指针更好”的主题上,并且,如果我们想要的只是在类<code>Widget</code>的构造函数动态分配<code>Widget::impl</code>对象,在<code>Widget</code>对象销毁时一并销毁它, <code>std::unique_ptr</code>(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>)是最合适的工具。在头文件中用<code>std::unique_ptr</code>替代原始指针,就有了头文件中如下代码:</p>
|
||||
<p>但是,我展示给你们看的是一段C++98的代码,散发着一股已经过去了几千年的腐朽气息。 它使用了原始指针,原始的<code>new</code>和原始的<code>delete</code>,一切都让它如此的...原始。这一章建立在“智能指针比原始指针更好”的主题上,并且,如果我们想要的只是在类<code>Widget</code>的构造函数动态分配<code>Widget::impl</code>对象,在<code>Widget</code>对象销毁时一并销毁它, <code>std::unique_ptr</code>(见<a href="../4.SmartPointers/item18.html">Item18</a>)是最合适的工具。在头文件中用<code>std::unique_ptr</code>替代原始指针,就有了头文件中如下代码:</p>
|
||||
<pre><code class="language-cpp">class Widget { //在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
@ -223,7 +223,7 @@ Widget w; //错误!
|
||||
</code></pre>
|
||||
<p>你所看到的错误信息根据编译器不同会有所不同,但是其文本一般会提到一些有关于“把<code>sizeof</code>或<code>delete</code>应用到未完成类型上”的信息。对于未完成类型,使用以上操作是禁止的。</p>
|
||||
<p>在Pimpl惯用法中使用<code>std::unique_ptr</code>会抛出错误,有点惊悚,因为第一<code>std::unique_ptr</code>宣称它支持未完成类型,第二Pimpl惯用法是<code>std::unique_ptr</code>的最常见的使用情况之一。 幸运的是,让这段代码能正常运行很简单。 只需要对上面出现的问题的原因有一个基础的认识就可以了。</p>
|
||||
<p>在对象<code>w</code>被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了<code>std::unique_ptr</code>,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的特殊成员函数的规则(见 <a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>),编译器会自动为我们生成一个析构函数。 在这个析构函数里,编译器会插入一些代码来调用类<code>Widget</code>的数据成员<code>pImpl</code>的析构函数。 <code>pImpl</code>是一个<code>std::unique_ptr<Widget::Impl></code>,也就是说,一个使用默认删除器的<code>std::unique_ptr</code>。 默认删除器是一个函数,它使用<code>delete</code>来销毁内置于<code>std::unique_ptr</code>的原始指针。然而,在使用<code>delete</code>之前,通常会使默认删除器使用C++11的特性<code>static_assert</code>来确保原始指针指向的类型不是一个未完成类型。 当编译器为<code>Widget w</code>的析构生成代码时,它会遇到<code>static_assert</code>检查并且失败,这通常是错误信息的来源。 这些错误信息只在对象<code>w</code>销毁的地方出现,因为类<code>Widget</code>的析构函数,正如其他的编译器生成的特殊成员函数一样,是暗含<code>inline</code>属性的。 错误信息自身往往指向对象<code>w</code>被创建的那行,因为这行代码明确地构造了这个对象,导致了后面潜在的析构。</p>
|
||||
<p>在对象<code>w</code>被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了<code>std::unique_ptr</code>,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的特殊成员函数的规则(见 <a href="../3.MovingToModernCpp/item17.html">Item17</a>),编译器会自动为我们生成一个析构函数。 在这个析构函数里,编译器会插入一些代码来调用类<code>Widget</code>的数据成员<code>pImpl</code>的析构函数。 <code>pImpl</code>是一个<code>std::unique_ptr<Widget::Impl></code>,也就是说,一个使用默认删除器的<code>std::unique_ptr</code>。 默认删除器是一个函数,它使用<code>delete</code>来销毁内置于<code>std::unique_ptr</code>的原始指针。然而,在使用<code>delete</code>之前,通常会使默认删除器使用C++11的特性<code>static_assert</code>来确保原始指针指向的类型不是一个未完成类型。 当编译器为<code>Widget w</code>的析构生成代码时,它会遇到<code>static_assert</code>检查并且失败,这通常是错误信息的来源。 这些错误信息只在对象<code>w</code>销毁的地方出现,因为类<code>Widget</code>的析构函数,正如其他的编译器生成的特殊成员函数一样,是暗含<code>inline</code>属性的。 错误信息自身往往指向对象<code>w</code>被创建的那行,因为这行代码明确地构造了这个对象,导致了后面潜在的析构。</p>
|
||||
<p>为了解决这个问题,你只需要确保在编译器生成销毁<code>std::unique_ptr<Widget::Impl></code>的代码之前, <code>Widget::Impl</code>已经是一个完成类型(<em>complete type</em>)。 当编译器“看到”它的定义的时候,该类型就成为完成类型了。 但是 <code>Widget::Impl</code>的定义在<code>widget.cpp</code>里。成功编译的关键,就是在<code>widget.cpp</code>文件内,让编译器在“看到” <code>Widget</code>的析构函数实现之前(也即编译器插入的,用来销毁<code>std::unique_ptr</code>这个数据成员的代码的,那个位置),先定义<code>Widget::Impl</code>。</p>
|
||||
<p>做出这样的调整很容易。只需要先在<code>widget.h</code>里,只声明类<code>Widget</code>的析构函数,但不要在这里定义它:</p>
|
||||
<pre><code class="language-cpp">class Widget { //跟之前一样,在“widget.h”中
|
||||
@ -259,7 +259,7 @@ Widget::~Widget() //析构函数的定义(译者注:这里
|
||||
<p>这样就可以了,并且这样增加的代码也最少,你声明<code>Widget</code>析构函数只是为了在 Widget 的实现文件中(译者注:指<code>widget.cpp</code>)写出它的定义,但是如果你想强调编译器自动生成的析构函数会做和你一样正确的事情,你可以直接使用“<code>= default</code>”定义析构函数体</p>
|
||||
<pre><code class="language-cpp">Widget::~Widget() = default; //同上述代码效果一致
|
||||
</code></pre>
|
||||
<p>使用了Pimpl惯用法的类自然适合支持移动操作,因为编译器自动生成的移动操作正合我们所意:对其中的<code>std::unique_ptr</code>进行移动。 正如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>所解释的那样,声明一个类<code>Widget</code>的析构函数会阻止编译器生成移动操作,所以如果你想要支持移动操作,你必须自己声明相关的函数。考虑到编译器自动生成的版本会正常运行,你可能会很想按如下方式实现它们:</p>
|
||||
<p>使用了Pimpl惯用法的类自然适合支持移动操作,因为编译器自动生成的移动操作正合我们所意:对其中的<code>std::unique_ptr</code>进行移动。 正如<a href="../3.MovingToModernCpp/item17.html">Item17</a>所解释的那样,声明一个类<code>Widget</code>的析构函数会阻止编译器生成移动操作,所以如果你想要支持移动操作,你必须自己声明相关的函数。考虑到编译器自动生成的版本会正常运行,你可能会很想按如下方式实现它们:</p>
|
||||
<pre><code class="language-cpp">class Widget { //仍然在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
@ -335,7 +335,7 @@ Widget& Widget::operator=(const Widget& rhs) //拷贝operator=
|
||||
return *this;
|
||||
}
|
||||
</code></pre>
|
||||
<p>两个函数的实现都比较中规中矩。 在每个情况中,我们都只从源对象(<code>rhs</code>)中,复制了结构体<code>Impl</code>的内容到目标对象中(<code>*this</code>)。我们利用了编译器会为我们自动生成结构体<code>Impl</code>的复制操作函数的机制,而不是逐一复制结构体<code>Impl</code>的成员,自动生成的复制操作能自动复制每一个成员。 因此我们通过调用编译器生成的<code>Widget::Impl</code>的复制操作函数来实现了类<code>Widget</code>的复制操作。 在复制构造函数中,注意,我们仍然遵从了<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>的建议,使用<code>std::make_unique</code>而非直接使用<code>new</code>。</p>
|
||||
<p>两个函数的实现都比较中规中矩。 在每个情况中,我们都只从源对象(<code>rhs</code>)中,复制了结构体<code>Impl</code>的内容到目标对象中(<code>*this</code>)。我们利用了编译器会为我们自动生成结构体<code>Impl</code>的复制操作函数的机制,而不是逐一复制结构体<code>Impl</code>的成员,自动生成的复制操作能自动复制每一个成员。 因此我们通过调用编译器生成的<code>Widget::Impl</code>的复制操作函数来实现了类<code>Widget</code>的复制操作。 在复制构造函数中,注意,我们仍然遵从了<a href="../4.SmartPointers/item21.html">Item21</a>的建议,使用<code>std::make_unique</code>而非直接使用<code>new</code>。</p>
|
||||
<p>为了实现Pimpl惯用法,<code>std::unique_ptr</code>是我们使用的智能指针,因为位于对象内部的<code>pImpl</code>指针(例如,在类<code>Widget</code>内部),对所指向的对应实现的对象的享有独占所有权。然而,有趣的是,如果我们使用<code>std::shared_ptr</code>而不是<code>std::unique_ptr</code>来做<code>pImpl</code>指针, 我们会发现本条款的建议不再适用。 我们不需要在类<code>Widget</code>里声明析构函数,没有了用户定义析构函数,编译器将会愉快地生成移动操作,并且将会如我们所期望般工作。<code>widget.h</code>里的代码如下,</p>
|
||||
<pre><code class="language-cpp">class Widget { //在“widget.h”中
|
||||
public:
|
||||
|
@ -172,9 +172,9 @@ move(T&& param)
|
||||
return static_cast<ReturnType>(param);
|
||||
}
|
||||
</code></pre>
|
||||
<p>我为你们高亮了这段代码的两部分(译者注:高亮的部分为函数名<code>move</code>和<code>static_cast<ReturnType>(param)</code>)。一个是函数名字,因为函数的返回值非常具有干扰性,而且我不想你们被它搞得晕头转向。另外一个高亮的部分是包含这段函数的本质的转换。正如你所见,<code>std::move</code>接受一个对象的引用(准确的说,一个通用引用(universal reference),见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>),返回一个指向同对象的引用。</p>
|
||||
<p>该函数返回类型的<code>&&</code>部分表明<code>std::move</code>函数返回的是一个右值引用,但是,正如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>所解释的那样,如果类型<code>T</code>恰好是一个左值引用,那么<code>T&&</code>将会成为一个左值引用。为了避免如此,<em>type trait</em>(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>)<code>std::remove_reference</code>应用到了类型<code>T</code>上,因此确保了<code>&&</code>被正确的应用到了一个不是引用的类型上。这保证了<code>std::move</code>返回的真的是右值引用,这很重要,因为函数返回的右值引用是右值。因此,<code>std::move</code>将它的实参转换为一个右值,这就是它的全部作用。</p>
|
||||
<p>此外,<code>std::move</code>在C++14中可以被更简单地实现。多亏了函数返回值类型推导(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>)和标准库的模板别名<code>std::remove_reference_t</code>(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>),<code>std::move</code>可以这样写:</p>
|
||||
<p>我为你们高亮了这段代码的两部分(译者注:高亮的部分为函数名<code>move</code>和<code>static_cast<ReturnType>(param)</code>)。一个是函数名字,因为函数的返回值非常具有干扰性,而且我不想你们被它搞得晕头转向。另外一个高亮的部分是包含这段函数的本质的转换。正如你所见,<code>std::move</code>接受一个对象的引用(准确的说,一个通用引用(universal reference),见<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>),返回一个指向同对象的引用。</p>
|
||||
<p>该函数返回类型的<code>&&</code>部分表明<code>std::move</code>函数返回的是一个右值引用,但是,正如<a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>所解释的那样,如果类型<code>T</code>恰好是一个左值引用,那么<code>T&&</code>将会成为一个左值引用。为了避免如此,<em>type trait</em>(见<a href="../3.MovingToModernCpp/item9.html">Item9</a>)<code>std::remove_reference</code>应用到了类型<code>T</code>上,因此确保了<code>&&</code>被正确的应用到了一个不是引用的类型上。这保证了<code>std::move</code>返回的真的是右值引用,这很重要,因为函数返回的右值引用是右值。因此,<code>std::move</code>将它的实参转换为一个右值,这就是它的全部作用。</p>
|
||||
<p>此外,<code>std::move</code>在C++14中可以被更简单地实现。多亏了函数返回值类型推导(见<a href="../1.DeducingTypes/item3.html">Item3</a>)和标准库的模板别名<code>std::remove_reference_t</code>(见<a href="../3.MovingToModernCpp/item9.html">Item9</a>),<code>std::move</code>可以这样写:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
decltype(auto) move(T&& param) //C++14,仍然在std命名空间
|
||||
{
|
||||
@ -185,7 +185,7 @@ decltype(auto) move(T&& param) //C++14,仍然在std命名空
|
||||
<p>看起来更简单,不是吗?</p>
|
||||
<p>因为<code>std::move</code>除了转换它的实参到右值以外什么也不做,有一些提议说它的名字叫<code>rvalue_cast</code>之类可能会更好。虽然可能确实是这样,但是它的名字已经是<code>std::move</code>,所以记住<code>std::move</code>做什么和不做什么很重要。它只进行转换,不移动任何东西。</p>
|
||||
<p>当然,右值本来就是移动操作的候选者,所以对一个对象使用<code>std::move</code>就是告诉编译器,这个对象很适合被移动。所以这就是为什么<code>std::move</code>叫现在的名字:更容易指定可以被移动的对象。</p>
|
||||
<p>事实上,右值只不过<strong>经常</strong>是移动操作的候选者。假设你有一个类,它用来表示一段注解。这个类的构造函数接受一个包含有注解的<code>std::string</code>作为形参,然后它复制该形参到数据成员。假设你了解<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md">Item41</a>,你声明一个值传递的形参:</p>
|
||||
<p>事实上,右值只不过<strong>经常</strong>是移动操作的候选者。假设你有一个类,它用来表示一段注解。这个类的构造函数接受一个包含有注解的<code>std::string</code>作为形参,然后它复制该形参到数据成员。假设你了解<a href="../8.Tweaks/item41.html">Item41</a>,你声明一个值传递的形参:</p>
|
||||
<pre><code class="language-cpp">class Annotation {
|
||||
public:
|
||||
explicit Annotation(std::string text); //将会被复制的形参,
|
||||
@ -199,7 +199,7 @@ public:
|
||||
…
|
||||
};
|
||||
</code></pre>
|
||||
<p>当复制<code>text</code>到一个数据成员的时候,为了避免一次复制操作的代价,你仍然记得来自<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md">Item41</a>的建议,把<code>std::move</code>应用到<code>text</code>上,因此产生一个右值:</p>
|
||||
<p>当复制<code>text</code>到一个数据成员的时候,为了避免一次复制操作的代价,你仍然记得来自<a href="../8.Tweaks/item41.html">Item41</a>的建议,把<code>std::move</code>应用到<code>text</code>上,因此产生一个右值:</p>
|
||||
<pre><code class="language-cpp">class Annotation {
|
||||
public:
|
||||
explicit Annotation(const std::string text)
|
||||
@ -246,7 +246,7 @@ logAndProcess(std::move(w)); //用右值调用
|
||||
</code></pre>
|
||||
<p>在<code>logAndProcess</code>函数的内部,形参<code>param</code>被传递给函数<code>process</code>。函数<code>process</code>分别对左值和右值做了重载。当我们使用左值来调用<code>logAndProcess</code>时,自然我们期望该左值被当作左值转发给<code>process</code>函数,而当我们使用右值来调用<code>logAndProcess</code>函数时,我们期望<code>process</code>函数的右值重载版本被调用。</p>
|
||||
<p>但是<code>param</code>,正如所有的其他函数形参一样,是一个左值。每次在函数<code>logAndProcess</code>内部对函数<code>process</code>的调用,都会因此调用函数<code>process</code>的左值重载版本。为防如此,我们需要一种机制:当且仅当传递给函数<code>logAndProcess</code>的用以初始化<code>param</code>的实参是一个右值时,<code>param</code>会被转换为一个右值。这就是<code>std::forward</code>做的事情。这就是为什么<code>std::forward</code>是一个<strong>有条件</strong>的转换:它的实参用右值初始化时,转换为一个右值。</p>
|
||||
<p>你也许会想知道<code>std::forward</code>是怎么知道它的实参是否是被一个右值初始化的。举个例子,在上述代码中,<code>std::forward</code>是怎么分辨<code>param</code>是被一个左值还是右值初始化的? 简短的说,该信息藏在函数<code>logAndProcess</code>的模板参数<code>T</code>中。该参数被传递给了函数<code>std::forward</code>,它解开了含在其中的信息。该机制工作的细节可以查询<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>。</p>
|
||||
<p>你也许会想知道<code>std::forward</code>是怎么知道它的实参是否是被一个右值初始化的。举个例子,在上述代码中,<code>std::forward</code>是怎么分辨<code>param</code>是被一个左值还是右值初始化的? 简短的说,该信息藏在函数<code>logAndProcess</code>的模板参数<code>T</code>中。该参数被传递给了函数<code>std::forward</code>,它解开了含在其中的信息。该机制工作的细节可以查询<a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>。</p>
|
||||
<p>考虑到<code>std::move</code>和<code>std::forward</code>都可以归结于转换,它们唯一的区别就是<code>std::move</code>总是执行转换,而<code>std::forward</code>偶尔为之。你可能会问是否我们可以免于使用<code>std::move</code>而在任何地方只使用<code>std::forward</code>。 从纯技术的角度,答案是yes:<code>std::forward</code>是可以完全胜任,<code>std::move</code>并非必须。当然,其实两者中没有哪一个函数是<strong>真的必须</strong>的,因为我们可以到处直接写转换代码,但是我希望我们能同意:这将相当的,嗯,让人恶心。</p>
|
||||
<p><code>std::move</code>的吸引力在于它的便利性:减少了出错的可能性,增加了代码的清晰程度。考虑一个类,我们希望统计有多少次移动构造函数被调用了。我们只需要一个<code>static</code>的计数器,它会在移动构造的时候自增。假设在这个类中,唯一一个非静态的数据成员是<code>std::string</code>,一种经典的移动构造函数(即,使用<code>std::move</code>)可以被实现如下:</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
@ -273,7 +273,7 @@ public:
|
||||
|
||||
}
|
||||
</code></pre>
|
||||
<p>注意,第一,<code>std::move</code>只需要一个函数实参(<code>rhs.s</code>),而<code>std::forward</code>不但需要一个函数实参(<code>rhs.s</code>),还需要一个模板类型实参<code>std::string</code>。其次,我们传递给<code>std::forward</code>的类型应当是一个non-reference,因为惯例是传递的实参应该是一个右值(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>)。同样,这意味着<code>std::move</code>比起<code>std::forward</code>来说需要打更少的字,并且免去了传递一个表示我们正在传递一个右值的类型实参。同样,它根绝了我们传递错误类型的可能性(例如,<code>std::string&</code>可能导致数据成员<code>s</code>被复制而不是被移动构造)。</p>
|
||||
<p>注意,第一,<code>std::move</code>只需要一个函数实参(<code>rhs.s</code>),而<code>std::forward</code>不但需要一个函数实参(<code>rhs.s</code>),还需要一个模板类型实参<code>std::string</code>。其次,我们传递给<code>std::forward</code>的类型应当是一个non-reference,因为惯例是传递的实参应该是一个右值(见<a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>)。同样,这意味着<code>std::move</code>比起<code>std::forward</code>来说需要打更少的字,并且免去了传递一个表示我们正在传递一个右值的类型实参。同样,它根绝了我们传递错误类型的可能性(例如,<code>std::string&</code>可能导致数据成员<code>s</code>被复制而不是被移动构造)。</p>
|
||||
<p>更重要的是,<code>std::move</code>的使用代表着无条件向右值的转换,而使用<code>std::forward</code>只对绑定了右值的引用进行到右值转换。这是两种完全不同的动作。前者是典型地为了移动操作,而后者只是传递(亦为转发)一个对象到另外一个函数,保留它原有的左值属性或右值属性。因为这些动作实在是差异太大,所以我们拥有两个不同的函数(以及函数名)来区分这些动作。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
|
@ -154,7 +154,7 @@ template<typename T>
|
||||
void f(T&& param); //不是右值引用
|
||||
</code></pre>
|
||||
<p>事实上,“<code>T&&</code>”有两种不同的意思。第一种,当然是右值引用。这种引用表现得正如你所期待的那样:它们只绑定到右值上,并且它们主要的存在原因就是为了识别可以移动操作的对象。</p>
|
||||
<p>“<code>T&&</code>”的另一种意思是,它既可以是右值引用,也可以是左值引用。这种引用在源码里看起来像右值引用(即“<code>T&&</code>”),但是它们可以表现得像是左值引用(即“<code>T&</code>”)。它们的二重性使它们既可以绑定到右值上(就像右值引用),也可以绑定到左值上(就像左值引用)。 此外,它们还可以绑定到<code>const</code>或者non-<code>const</code>的对象上,也可以绑定到<code>volatile</code>或者non-<code>volatile</code>的对象上,甚至可以绑定到既<code>const</code>又<code>volatile</code>的对象上。它们可以绑定到几乎任何东西。这种空前灵活的引用值得拥有自己的名字。我把它叫做<strong>通用引用</strong>(<em>universal references</em>)。(<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>解释了<code>std::forward</code>几乎总是可以应用到通用引用上,并且在这本书即将出版之际,一些C++社区的成员已经开始将这种通用引用称之为<strong>转发引用</strong>(<em>forwarding references</em>))。</p>
|
||||
<p>“<code>T&&</code>”的另一种意思是,它既可以是右值引用,也可以是左值引用。这种引用在源码里看起来像右值引用(即“<code>T&&</code>”),但是它们可以表现得像是左值引用(即“<code>T&</code>”)。它们的二重性使它们既可以绑定到右值上(就像右值引用),也可以绑定到左值上(就像左值引用)。 此外,它们还可以绑定到<code>const</code>或者non-<code>const</code>的对象上,也可以绑定到<code>volatile</code>或者non-<code>volatile</code>的对象上,甚至可以绑定到既<code>const</code>又<code>volatile</code>的对象上。它们可以绑定到几乎任何东西。这种空前灵活的引用值得拥有自己的名字。我把它叫做<strong>通用引用</strong>(<em>universal references</em>)。(<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>解释了<code>std::forward</code>几乎总是可以应用到通用引用上,并且在这本书即将出版之际,一些C++社区的成员已经开始将这种通用引用称之为<strong>转发引用</strong>(<em>forwarding references</em>))。</p>
|
||||
<p>在两种情况下会出现通用引用。最常见的一种是函数模板形参,正如在之前的示例代码中所出现的例子:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void f(T&& param); //param是一个通用引用
|
||||
@ -236,8 +236,8 @@ void someFunc(MyTemplateType&& param);
|
||||
stop timer and record elapsed time;
|
||||
};
|
||||
</code></pre>
|
||||
<p>如果你对<em>lambda</em>里的代码“<code>std::forward<decltype(blah blah blah)></code>”反应是“这是什么鬼...?!”,只能说你可能还没有读<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item33.md">Item33</a>。别担心。在本条款,重要的事是<em>lambda</em>表达式中声明的<code>auto&&</code>类型的形参。<code>func</code>是一个通用引用,可以被绑定到任何可调用对象,无论左值还是右值。<code>args</code>是0个或者多个通用引用(即它是个通用引用<em>parameter pack</em>),它可以绑定到任意数目、任意类型的对象上。多亏了<code>auto</code>类型的通用引用,函数<code>timeFuncInvocation</code>可以对<strong>近乎任意</strong>(pretty much any)函数进行计时。(如果你想知道任意(any)和近乎任意(pretty much any)的区别,往后翻到<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>)。</p>
|
||||
<p>牢记整个本条款——通用引用的基础——是一个谎言,噢不,是一个“抽象”。其底层真相被称为<strong>引用折叠</strong>(<em>reference collapsing</em>),<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>的专题将致力于讨论它。但是这个真相并不降低该抽象的有用程度。区分右值引用和通用引用将会帮助你更准确地阅读代码(“究竟我眼前的这个<code>T&&</code>是只绑定到右值还是可以绑定任意对象呢?”),并且,当你在和你的合作者交流时,它会帮助你避免歧义(“在这里我在用一个通用引用,而非右值引用”)。它也可以帮助你弄懂<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>和<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">26</a>,它们依赖于右值引用和通用引用的区别。所以,拥抱这份抽象,陶醉于它吧。就像牛顿的力学定律(本质上不正确),比起爱因斯坦的广义相对论(这是真相)而言,往往更简单,更易用。所以通用引用的概念,相较于穷究引用折叠的细节而言,是更合意之选。</p>
|
||||
<p>如果你对<em>lambda</em>里的代码“<code>std::forward<decltype(blah blah blah)></code>”反应是“这是什么鬼...?!”,只能说你可能还没有读<a href="../6.LambdaExpressions/item33.html">Item33</a>。别担心。在本条款,重要的事是<em>lambda</em>表达式中声明的<code>auto&&</code>类型的形参。<code>func</code>是一个通用引用,可以被绑定到任何可调用对象,无论左值还是右值。<code>args</code>是0个或者多个通用引用(即它是个通用引用<em>parameter pack</em>),它可以绑定到任意数目、任意类型的对象上。多亏了<code>auto</code>类型的通用引用,函数<code>timeFuncInvocation</code>可以对<strong>近乎任意</strong>(pretty much any)函数进行计时。(如果你想知道任意(any)和近乎任意(pretty much any)的区别,往后翻到<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>)。</p>
|
||||
<p>牢记整个本条款——通用引用的基础——是一个谎言,噢不,是一个“抽象”。其底层真相被称为<strong>引用折叠</strong>(<em>reference collapsing</em>),<a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>的专题将致力于讨论它。但是这个真相并不降低该抽象的有用程度。区分右值引用和通用引用将会帮助你更准确地阅读代码(“究竟我眼前的这个<code>T&&</code>是只绑定到右值还是可以绑定任意对象呢?”),并且,当你在和你的合作者交流时,它会帮助你避免歧义(“在这里我在用一个通用引用,而非右值引用”)。它也可以帮助你弄懂<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>和<a href="../5.RRefMovSemPerfForw/item26.html">26</a>,它们依赖于右值引用和通用引用的区别。所以,拥抱这份抽象,陶醉于它吧。就像牛顿的力学定律(本质上不正确),比起爱因斯坦的广义相对论(这是真相)而言,往往更简单,更易用。所以通用引用的概念,相较于穷究引用折叠的细节而言,是更合意之选。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>如果一个函数模板形参的类型为<code>T&&</code>,并且<code>T</code>需要被推导得知,或者如果一个对象被声明为<code>auto&&</code>,这个形参或者对象就是一个通用引用。</li>
|
||||
|
@ -147,7 +147,7 @@
|
||||
…
|
||||
};
|
||||
</code></pre>
|
||||
<p>这是个例子,你将希望传递这样的对象给其他函数,允许那些函数利用对象的右值性(rvalueness)。这样做的方法是将绑定到此类对象的形参转换为右值。如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>中所述,这不仅是<code>std::move</code>所做,而且它的创建就是为了这个目的:</p>
|
||||
<p>这是个例子,你将希望传递这样的对象给其他函数,允许那些函数利用对象的右值性(rvalueness)。这样做的方法是将绑定到此类对象的形参转换为右值。如<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>中所述,这不仅是<code>std::move</code>所做,而且它的创建就是为了这个目的:</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
public:
|
||||
Widget(Widget&& rhs) //rhs是右值引用
|
||||
@ -160,7 +160,7 @@ private:
|
||||
std::shared_ptr<SomeDataStructure> p;
|
||||
};
|
||||
</code></pre>
|
||||
<p>另一方面(查看<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>),通用引用<strong>可能</strong>绑定到有资格移动的对象上。通用引用使用右值初始化时,才将其强制转换为右值。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>阐释了这正是<code>std::forward</code>所做的:</p>
|
||||
<p>另一方面(查看<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>),通用引用<strong>可能</strong>绑定到有资格移动的对象上。通用引用使用右值初始化时,才将其强制转换为右值。<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>阐释了这正是<code>std::forward</code>所做的:</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
public:
|
||||
template<typename T>
|
||||
@ -171,7 +171,7 @@ public:
|
||||
};
|
||||
</code></pre>
|
||||
<p>总而言之,当把右值引用转发给其他函数时,右值引用应该被<strong>无条件转换</strong>为右值(通过<code>std::move</code>),因为它们<strong>总是</strong>绑定到右值;当转发通用引用时,通用引用应该<strong>有条件地转换</strong>为右值(通过<code>std::forward</code>),因为它们只是<strong>有时</strong>绑定到右值。</p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>解释说,可以在右值引用上使用<code>std::forward</code>表现出适当的行为,但是代码较长,容易出错,所以应该避免在右值引用上使用<code>std::forward</code>。更糟的是在通用引用上使用<code>std::move</code>,这可能会意外改变左值(比如局部变量):</p>
|
||||
<p><a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>解释说,可以在右值引用上使用<code>std::forward</code>表现出适当的行为,但是代码较长,容易出错,所以应该避免在右值引用上使用<code>std::forward</code>。更糟的是在通用引用上使用<code>std::move</code>,这可能会意外改变左值(比如局部变量):</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
public:
|
||||
template<typename T>
|
||||
@ -195,7 +195,7 @@ w.setName(n); //把n移动进w!
|
||||
… //现在n的值未知
|
||||
</code></pre>
|
||||
<p>上面的例子,局部变量<code>n</code>被传递给<code>w.setName</code>,调用方可能认为这是对<code>n</code>的只读操作——这一点倒是可以被原谅。但是因为<code>setName</code>内部使用<code>std::move</code>无条件将传递的引用形参转换为右值,<code>n</code>的值被移动进<code>w.name</code>,调用<code>setName</code>返回时<code>n</code>最终变为未定义的值。这种行为使得调用者蒙圈了——还有可能变得狂躁。</p>
|
||||
<p>你可能争辩说<code>setName</code>不应该将其形参声明为通用引用。此类引用不能使用<code>const</code>(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>),但是<code>setName</code>肯定不应该修改其形参。你可能会指出,如果为<code>const</code>左值和为右值分别重载<code>setName</code>可以避免整个问题,比如这样:</p>
|
||||
<p>你可能争辩说<code>setName</code>不应该将其形参声明为通用引用。此类引用不能使用<code>const</code>(见<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>),但是<code>setName</code>肯定不应该修改其形参。你可能会指出,如果为<code>const</code>左值和为右值分别重载<code>setName</code>可以避免整个问题,比如这样:</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
public:
|
||||
void setName(const std::string& newName) //用const左值设置
|
||||
@ -210,8 +210,8 @@ public:
|
||||
<p>这样的话,当然可以工作,但是有缺点。首先编写和维护的代码更多(两个函数而不是单个模板);其次,效率下降。比如,考虑如下场景:</p>
|
||||
<pre><code class="language-cpp">w.setName("Adela Novak");
|
||||
</code></pre>
|
||||
<p>使用通用引用的版本的<code>setName</code>,字面字符串“<code>Adela Novak</code>”可以被传递给<code>setName</code>,再传给<code>w</code>内部<code>std::string</code>的赋值运算符。<code>w</code>的<code>name</code>的数据成员通过字面字符串直接赋值,没有临时<code>std::string</code>对象被创建。但是,<code>setName</code>重载版本,会有一个临时<code>std::string</code>对象被创建,<code>setName</code>形参绑定到这个对象,然后这个临时<code>std::string</code>移动到<code>w</code>的数据成员中。一次<code>setName</code>的调用会包括<code>std::string</code>构造函数调用(创建中间对象),<code>std::string</code>赋值运算符调用(移动<code>newName</code>到<code>w.name</code>),<code>std::string</code>析构函数调用(析构中间对象)。这比调用接受<code>const char*</code>指针的<code>std::string</code>赋值运算符开销昂贵许多。增加的开销根据实现不同而不同,这些开销是否值得担心也跟应用和库的不同而有所不同,但是事实上,将通用引用模板替换成对左值引用和右值引用的一对函数重载在某些情况下会导致运行时的开销。如果把例子泛化,<code>Widget</code>数据成员是任意类型(而不是知道是个<code>std::string</code>),性能差距可能会变得更大,因为不是所有类型的移动操作都像<code>std::string</code>开销较小(参看<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item29.md">Item29</a>)。</p>
|
||||
<p>但是,关于对左值和右值的重载函数最重要的问题不是源代码的数量,也不是代码的运行时性能。而是设计的可扩展性差。<code>Widget::setName</code>有一个形参,因此需要两种重载实现,但是对于有更多形参的函数,每个都可能是左值或右值,重载函数的数量几何式增长:n个参数的话,就要实现2<sup>n</sup>种重载。这还不是最坏的。有的函数——实际上是函数模板——接受<strong>无限制</strong>个数的参数,每个参数都可以是左值或者右值。此类函数的典型代表是<code>std::make_shared</code>,还有对于C++14的<code>std::make_unique</code>(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>)。查看他们的的重载声明:</p>
|
||||
<p>使用通用引用的版本的<code>setName</code>,字面字符串“<code>Adela Novak</code>”可以被传递给<code>setName</code>,再传给<code>w</code>内部<code>std::string</code>的赋值运算符。<code>w</code>的<code>name</code>的数据成员通过字面字符串直接赋值,没有临时<code>std::string</code>对象被创建。但是,<code>setName</code>重载版本,会有一个临时<code>std::string</code>对象被创建,<code>setName</code>形参绑定到这个对象,然后这个临时<code>std::string</code>移动到<code>w</code>的数据成员中。一次<code>setName</code>的调用会包括<code>std::string</code>构造函数调用(创建中间对象),<code>std::string</code>赋值运算符调用(移动<code>newName</code>到<code>w.name</code>),<code>std::string</code>析构函数调用(析构中间对象)。这比调用接受<code>const char*</code>指针的<code>std::string</code>赋值运算符开销昂贵许多。增加的开销根据实现不同而不同,这些开销是否值得担心也跟应用和库的不同而有所不同,但是事实上,将通用引用模板替换成对左值引用和右值引用的一对函数重载在某些情况下会导致运行时的开销。如果把例子泛化,<code>Widget</code>数据成员是任意类型(而不是知道是个<code>std::string</code>),性能差距可能会变得更大,因为不是所有类型的移动操作都像<code>std::string</code>开销较小(参看<a href="../5.RRefMovSemPerfForw/item29.html">Item29</a>)。</p>
|
||||
<p>但是,关于对左值和右值的重载函数最重要的问题不是源代码的数量,也不是代码的运行时性能。而是设计的可扩展性差。<code>Widget::setName</code>有一个形参,因此需要两种重载实现,但是对于有更多形参的函数,每个都可能是左值或右值,重载函数的数量几何式增长:n个参数的话,就要实现2<sup>n</sup>种重载。这还不是最坏的。有的函数——实际上是函数模板——接受<strong>无限制</strong>个数的参数,每个参数都可以是左值或者右值。此类函数的典型代表是<code>std::make_shared</code>,还有对于C++14的<code>std::make_unique</code>(见<a href="../4.SmartPointers/item21.html">Item21</a>)。查看他们的的重载声明:</p>
|
||||
<pre><code class="language-cpp">template<class T, class... Args> //来自C++11标准
|
||||
shared_ptr<T> make_shared(Args&&... args);
|
||||
|
||||
@ -233,7 +233,7 @@ void setSignText(T&& text) //text是通用引用
|
||||
}
|
||||
</code></pre>
|
||||
<p>这里,我们想要确保<code>text</code>的值不会被<code>sign.setText</code>改变,因为我们想要在<code>signHistory.add</code>中继续使用。因此<code>std::forward</code>只在最后使用。</p>
|
||||
<p>对于<code>std::move</code>,同样的思路(即最后一次用右值引用的时候再调用<code>std::move</code>),但是需要注意,在有些稀少的情况下,你需要调用<code>std::move_if_noexcept</code>代替<code>std::move</code>。要了解何时以及为什么,参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md">Item14</a>。</p>
|
||||
<p>对于<code>std::move</code>,同样的思路(即最后一次用右值引用的时候再调用<code>std::move</code>),但是需要注意,在有些稀少的情况下,你需要调用<code>std::move_if_noexcept</code>代替<code>std::move</code>。要了解何时以及为什么,参考<a href="../3.MovingToModernCpp/item14.html">Item14</a>。</p>
|
||||
<p>如果你在<strong>按值</strong>返回的函数中,返回值绑定到右值引用或者通用引用上,需要对返回的引用使用<code>std::move</code>或者<code>std::forward</code>。要了解原因,考虑两个矩阵相加的<code>operator+</code>函数,左侧的矩阵为右值(可以被用来保存求值之后的和):</p>
|
||||
<pre><code class="language-cpp">Matrix //按值返回
|
||||
operator+(Matrix&& lhs, const Matrix& rhs)
|
||||
@ -251,7 +251,7 @@ operator+(Matrix&& lhs, const Matrix& rhs)
|
||||
}
|
||||
</code></pre>
|
||||
<p><code>lhs</code>是个左值的事实,会强制编译器拷贝它到返回值的内存空间。假定<code>Matrix</code>支持移动操作,并且比拷贝操作效率更高,在<code>return</code>语句中使用<code>std::move</code>的代码效率更高。</p>
|
||||
<p>如果<code>Matrix</code>不支持移动操作,将其转换为右值不会变差,因为右值可以直接被<code>Matrix</code>的拷贝构造函数拷贝(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>)。如果<code>Matrix</code>随后支持了移动操作,<code>operator+</code>将在下一次编译时受益。就是这种情况,通过将<code>std::move</code>应用到按值返回的函数中要返回的右值引用上,不会损失什么(还可能获得收益)。</p>
|
||||
<p>如果<code>Matrix</code>不支持移动操作,将其转换为右值不会变差,因为右值可以直接被<code>Matrix</code>的拷贝构造函数拷贝(见<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>)。如果<code>Matrix</code>随后支持了移动操作,<code>operator+</code>将在下一次编译时受益。就是这种情况,通过将<code>std::move</code>应用到按值返回的函数中要返回的右值引用上,不会损失什么(还可能获得收益)。</p>
|
||||
<p>使用通用引用和<code>std::forward</code>的情况类似。考虑函数模板<code>reduceAndCopy</code>收到一个未规约(unreduced)对象<code>Fraction</code>,将其规约,并返回一个规约后的副本。如果原始对象是右值,可以将其移动到返回值中(避免拷贝开销),但是如果原始对象是左值,必须创建副本,因此如下代码:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
Fraction //按值返回
|
||||
|
@ -160,7 +160,7 @@ logAndAdd("Patty Dog"); //传递字符串字面值
|
||||
<p>在第一个调用中,<code>logAndAdd</code>的形参<code>name</code>绑定到变量<code>petName</code>。在<code>logAndAdd</code>中<code>name</code>最终传给<code>names.emplace</code>。因为<code>name</code>是左值,会拷贝到<code>names</code>中。没有方法避免拷贝,因为是左值(<code>petName</code>)传递给<code>logAndAdd</code>的。</p>
|
||||
<p>在第二个调用中,形参<code>name</code>绑定到右值(显式从“<code>Persephone</code>”创建的临时<code>std::string</code>)。<code>name</code>本身是个左值,所以它被拷贝到<code>names</code>中,但是我们意识到,原则上,它的值可以被移动到<code>names</code>中。本次调用中,我们有个拷贝代价,但是我们应该能用移动勉强应付。</p>
|
||||
<p>在第三个调用中,形参<code>name</code>也绑定一个右值,但是这次是通过“<code>Patty Dog</code>”隐式创建的临时<code>std::string</code>变量。就像第二个调用中,<code>name</code>被拷贝到<code>names</code>,但是这里,传递给<code>logAndAdd</code>的实参是一个字符串字面量。如果直接将字符串字面量传递给<code>emplace</code>,就不会创建<code>std::string</code>的临时变量,而是直接在<code>std::multiset</code>中通过字面量构建<code>std::string</code>。在第三个调用中,我们有个<code>std::string</code>拷贝开销,但是我们连移动开销都不想要,更别说拷贝的。</p>
|
||||
<p>我们可以通过使用通用引用(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>)重写<code>logAndAdd</code>来使第二个和第三个调用效率提升,按照<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>的说法,<code>std::forward</code>转发这个引用到<code>emplace</code>。代码如下:</p>
|
||||
<p>我们可以通过使用通用引用(参见<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>)重写<code>logAndAdd</code>来使第二个和第三个调用效率提升,按照<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>的说法,<code>std::forward</code>转发这个引用到<code>emplace</code>。代码如下:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void logAndAdd(T&& name)
|
||||
{
|
||||
@ -203,7 +203,7 @@ logAndAdd(nameIdx); //错误!
|
||||
<p>最后一行的注释并不清楚明白,下面让我来说明发生了什么。</p>
|
||||
<p>有两个重载的<code>logAndAdd</code>。使用通用引用的那个推导出<code>T</code>的类型是<code>short</code>,因此可以精确匹配。对于<code>int</code>类型参数的重载也可以在<code>short</code>类型提升后匹配成功。根据正常的重载解决规则,精确匹配优先于类型提升的匹配,所以被调用的是通用引用的重载。</p>
|
||||
<p>在通用引用那个重载中,<code>name</code>形参绑定到要传入的<code>short</code>上,然后<code>name</code>被<code>std::forward</code>给<code>names</code>(一个<code>std::multiset<std::string></code>)的<code>emplace</code>成员函数,然后又被转发给<code>std::string</code>构造函数。<code>std::string</code>没有接受<code>short</code>的构造函数,所以<code>logAndAdd</code>调用里的<code>multiset::emplace</code>调用里的<code>std::string</code>构造函数调用失败。(译者注:这句话比较绕,实际上就是调用链。)所有这一切的原因就是对于<code>short</code>类型通用引用重载优先于<code>int</code>类型的重载。</p>
|
||||
<p>使用通用引用的函数在C++中是最贪婪的函数。它们几乎可以精确匹配任何类型的实参(极少不适用的实参在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>中介绍)。这也是把重载和通用引用组合在一块是糟糕主意的原因:通用引用的实现会匹配比开发者预期要多得多的实参类型。</p>
|
||||
<p>使用通用引用的函数在C++中是最贪婪的函数。它们几乎可以精确匹配任何类型的实参(极少不适用的实参在<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>中介绍)。这也是把重载和通用引用组合在一块是糟糕主意的原因:通用引用的实现会匹配比开发者预期要多得多的实参类型。</p>
|
||||
<p>一个更容易掉入这种陷阱的例子是写一个完美转发构造函数。简单对<code>logAndAdd</code>例子进行改造就可以说明这个问题。不用写接受<code>std::string</code>或者用索引查找<code>std::string</code>的自由函数,只是想一个构造函数有着相同操作的<code>Person</code>类:</p>
|
||||
<pre><code class="language-cpp">class Person {
|
||||
public:
|
||||
@ -219,7 +219,7 @@ private:
|
||||
std::string name;
|
||||
};
|
||||
</code></pre>
|
||||
<p>就像在<code>logAndAdd</code>的例子中,传递一个不是<code>int</code>的整型变量(比如<code>std::size_t</code>,<code>short</code>,<code>long</code>等)会调用通用引用的构造函数而不是<code>int</code>的构造函数,这会导致编译错误。这里这个问题甚至更糟糕,因为<code>Person</code>中存在的重载比肉眼看到的更多。在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>中说明,在适当的条件下,C++会生成拷贝和移动构造函数,即使类包含了模板化的构造函数,模板函数能实例化产生与拷贝和移动构造函数一样的签名,也在合适的条件范围内。如果拷贝和移动构造被生成,<code>Person</code>类看起来就像这样:</p>
|
||||
<p>就像在<code>logAndAdd</code>的例子中,传递一个不是<code>int</code>的整型变量(比如<code>std::size_t</code>,<code>short</code>,<code>long</code>等)会调用通用引用的构造函数而不是<code>int</code>的构造函数,这会导致编译错误。这里这个问题甚至更糟糕,因为<code>Person</code>中存在的重载比肉眼看到的更多。在<a href="../3.MovingToModernCpp/item17.html">Item17</a>中说明,在适当的条件下,C++会生成拷贝和移动构造函数,即使类包含了模板化的构造函数,模板函数能实例化产生与拷贝和移动构造函数一样的签名,也在合适的条件范围内。如果拷贝和移动构造被生成,<code>Person</code>类看起来就像这样:</p>
|
||||
<pre><code class="language-cpp">class Person {
|
||||
public:
|
||||
template<typename T> //完美转发的构造函数
|
||||
@ -269,7 +269,7 @@ public:
|
||||
};
|
||||
</code></pre>
|
||||
<p>但是没啥影响,因为重载规则规定当模板实例化函数和非模板函数(或者称为“正常”函数)匹配优先级相当时,优先使用“正常”函数。拷贝构造函数(正常函数)因此胜过具有相同签名的模板实例化函数。</p>
|
||||
<p>(如果你想知道为什么编译器在生成一个拷贝构造函数时还会模板实例化一个相同签名的函数,参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>。)</p>
|
||||
<p>(如果你想知道为什么编译器在生成一个拷贝构造函数时还会模板实例化一个相同签名的函数,参考<a href="../3.MovingToModernCpp/item17.html">Item17</a>。)</p>
|
||||
<p>当继承纳入考虑范围时,完美转发的构造函数与编译器生成的拷贝、移动操作之间的交互会更加复杂。尤其是,派生类的拷贝和移动操作的传统实现会表现得非常奇怪。来看一下:</p>
|
||||
<pre><code class="language-cpp">class SpecialPerson: public Person {
|
||||
public:
|
||||
@ -283,7 +283,7 @@ public:
|
||||
};
|
||||
</code></pre>
|
||||
<p>如同注释表示的,派生类的拷贝和移动构造函数没有调用基类的拷贝和移动构造函数,而是调用了基类的完美转发构造函数!为了理解原因,要知道派生类将<code>SpecialPerson</code>类型的实参传递给其基类,然后通过模板实例化和重载解析规则作用于基类<code>Person</code>。最终,代码无法编译,因为<code>std::string</code>没有接受一个<code>SpecialPerson</code>的构造函数。</p>
|
||||
<p>我希望到目前为止,已经说服了你,如果可能的话,避免对通用引用形参的函数进行重载。但是,如果在通用引用上重载是糟糕的主意,那么如果需要可转发大多数实参类型的函数,但是对于某些实参类型又要特殊处理应该怎么办?存在多种办法。实际上,下一个条款,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item27.md">Item27</a>专门来讨论这个问题,敬请阅读。</p>
|
||||
<p>我希望到目前为止,已经说服了你,如果可能的话,避免对通用引用形参的函数进行重载。但是,如果在通用引用上重载是糟糕的主意,那么如果需要可转发大多数实参类型的函数,但是对于某些实参类型又要特殊处理应该怎么办?存在多种办法。实际上,下一个条款,<a href="../5.RRefMovSemPerfForw/item27.html">Item27</a>专门来讨论这个问题,敬请阅读。</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>对通用引用形参的函数进行重载,通用引用函数的调用机会几乎总会比你期望的多得多。</li>
|
||||
|
@ -141,14 +141,14 @@
|
||||
<main>
|
||||
<h2 id="条款二十七熟悉通用引用重载的替代方法"><a class="header" href="#条款二十七熟悉通用引用重载的替代方法">条款二十七:熟悉通用引用重载的替代方法</a></h2>
|
||||
<p><strong>Item 27: Familiarize yourself with alternatives to overloading on universal references</strong></p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中说明了对使用通用引用形参的函数,无论是独立函数还是成员函数(尤其是构造函数),进行重载都会导致一系列问题。但是也提供了一些示例,如果能够按照我们期望的方式运行,重载可能也是有用的。这个条款探讨了几种,通过避免在通用引用上重载的设计,或者通过限制通用引用可以匹配的参数类型,来实现所期望行为的方法。</p>
|
||||
<p>讨论基于<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中的示例,如果你还没有阅读那个条款,请先阅读那个条款再继续。</p>
|
||||
<p><a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中说明了对使用通用引用形参的函数,无论是独立函数还是成员函数(尤其是构造函数),进行重载都会导致一系列问题。但是也提供了一些示例,如果能够按照我们期望的方式运行,重载可能也是有用的。这个条款探讨了几种,通过避免在通用引用上重载的设计,或者通过限制通用引用可以匹配的参数类型,来实现所期望行为的方法。</p>
|
||||
<p>讨论基于<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中的示例,如果你还没有阅读那个条款,请先阅读那个条款再继续。</p>
|
||||
<h3 id="放弃重载"><a class="header" href="#放弃重载">放弃重载</a></h3>
|
||||
<p>在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中的第一个例子中,<code>logAndAdd</code>是许多函数的代表,这些函数可以使用不同的名字来避免在通用引用上的重载的弊端。例如两个重载的<code>logAndAdd</code>函数,可以分别改名为<code>logAndAddName</code>和<code>logAndAddNameIdx</code>。但是,这种方式不能用在第二个例子,<code>Person</code>构造函数中,因为构造函数的名字被语言固定了(译者注:即构造函数名与类名相同)。此外谁愿意放弃重载呢?</p>
|
||||
<p>在<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中的第一个例子中,<code>logAndAdd</code>是许多函数的代表,这些函数可以使用不同的名字来避免在通用引用上的重载的弊端。例如两个重载的<code>logAndAdd</code>函数,可以分别改名为<code>logAndAddName</code>和<code>logAndAddNameIdx</code>。但是,这种方式不能用在第二个例子,<code>Person</code>构造函数中,因为构造函数的名字被语言固定了(译者注:即构造函数名与类名相同)。此外谁愿意放弃重载呢?</p>
|
||||
<h3 id="传递const-t"><a class="header" href="#传递const-t">传递const T&</a></h3>
|
||||
<p>一种替代方案是退回到C++98,然后将传递通用引用替换为传递lvalue-refrence-to-<code>const</code>。事实上,这是<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中首先考虑的方法。缺点是效率不高。现在我们知道了通用引用和重载的相互关系,所以放弃一些效率来确保行为正确简单可能也是一种不错的折中。</p>
|
||||
<p>一种替代方案是退回到C++98,然后将传递通用引用替换为传递lvalue-refrence-to-<code>const</code>。事实上,这是<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中首先考虑的方法。缺点是效率不高。现在我们知道了通用引用和重载的相互关系,所以放弃一些效率来确保行为正确简单可能也是一种不错的折中。</p>
|
||||
<h3 id="传值"><a class="header" href="#传值">传值</a></h3>
|
||||
<p>通常在不增加复杂性的情况下提高性能的一种方法是,将按传引用形参替换为按值传递,这是违反直觉的。该设计遵循<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md">Item41</a>中给出的建议,即在你知道要拷贝时就按值传递,因此会参考那个条款来详细讨论如何设计与工作,效率如何。这里,在<code>Person</code>的例子中展示:</p>
|
||||
<p>通常在不增加复杂性的情况下提高性能的一种方法是,将按传引用形参替换为按值传递,这是违反直觉的。该设计遵循<a href="../8.Tweaks/item41.html">Item41</a>中给出的建议,即在你知道要拷贝时就按值传递,因此会参考那个条款来详细讨论如何设计与工作,效率如何。这里,在<code>Person</code>的例子中展示:</p>
|
||||
<pre><code class="language-cpp">class Person {
|
||||
public:
|
||||
explicit Person(std::string n) //代替T&&构造函数,
|
||||
@ -162,7 +162,7 @@ private:
|
||||
std::string name;
|
||||
};
|
||||
</code></pre>
|
||||
<p>因为没有<code>std::string</code>构造函数可以接受整型参数,所有<code>int</code>或者其他整型变量(比如<code>std::size_t</code>、<code>short</code>、<code>long</code>等)都会使用<code>int</code>类型重载的构造函数。相似的,所有<code>std::string</code>类似的实参(还有可以用来创建<code>std::string</code>的东西,比如字面量“<code>Ruth</code>”等)都会使用<code>std::string</code>类型的重载构造函数。没有意外情况。我想你可能会说有些人使用<code>0</code>或者<code>NULL</code>指代空指针会调用<code>int</code>重载的构造函数让他们很吃惊,但是这些人应该参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item8.md">Item8</a>反复阅读直到使用<code>0</code>或者<code>NULL</code>作为空指针让他们恶心。</p>
|
||||
<p>因为没有<code>std::string</code>构造函数可以接受整型参数,所有<code>int</code>或者其他整型变量(比如<code>std::size_t</code>、<code>short</code>、<code>long</code>等)都会使用<code>int</code>类型重载的构造函数。相似的,所有<code>std::string</code>类似的实参(还有可以用来创建<code>std::string</code>的东西,比如字面量“<code>Ruth</code>”等)都会使用<code>std::string</code>类型的重载构造函数。没有意外情况。我想你可能会说有些人使用<code>0</code>或者<code>NULL</code>指代空指针会调用<code>int</code>重载的构造函数让他们很吃惊,但是这些人应该参考<a href="../3.MovingToModernCpp/item8.html">Item8</a>反复阅读直到使用<code>0</code>或者<code>NULL</code>作为空指针让他们恶心。</p>
|
||||
<h3 id="使用tag-dispatch"><a class="header" href="#使用tag-dispatch">使用<em>tag dispatch</em></a></h3>
|
||||
<p>传递lvalue-reference-to-<code>const</code>以及按值传递都不支持完美转发。如果使用通用引用的动机是完美转发,我们就只能使用通用引用了,没有其他选择。但是又不想放弃重载。所以如果不放弃重载又不放弃通用引用,如何避免在通用引用上重载呢?</p>
|
||||
<p>实际上并不难。通过查看所有重载的所有形参以及调用点的所有传入实参,然后选择最优匹配的函数——考虑所有形参/实参的组合。通用引用通常提供了最优匹配,但是如果通用引用是包含其他<strong>非</strong>通用引用的形参列表的一部分,则非通用引用形参的较差匹配会使有一个通用引用的重载版本不被运行。这就是<em>tag dispatch</em>方法的基础,下面的示例会使这段话更容易理解。</p>
|
||||
@ -177,8 +177,8 @@ void logAndAdd(T&& name)
|
||||
names.emplace(std::forward<T>(name));
|
||||
}
|
||||
</code></pre>
|
||||
<p>就其本身而言,功能执行没有问题,但是如果引入一个<code>int</code>类型的重载来用索引查找对象,就会重新陷入<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中描述的麻烦。这个条款的目标是避免它。不通过重载,我们重新实现<code>logAndAdd</code>函数分拆为两个函数,一个针对整型值,一个针对其他。<code>logAndAdd</code>本身接受所有实参类型,包括整型和非整型。</p>
|
||||
<p>这两个真正执行逻辑的函数命名为<code>logAndAddImpl</code>,即我们使用重载。其中一个函数接受通用引用。所以我们同时使用了重载和通用引用。但是每个函数接受第二个形参,表征传入的实参是否为整型。这第二个形参可以帮助我们避免陷入到<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中提到的麻烦中,因为我们将其安排为第二个实参决定选择哪个重载函数。</p>
|
||||
<p>就其本身而言,功能执行没有问题,但是如果引入一个<code>int</code>类型的重载来用索引查找对象,就会重新陷入<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中描述的麻烦。这个条款的目标是避免它。不通过重载,我们重新实现<code>logAndAdd</code>函数分拆为两个函数,一个针对整型值,一个针对其他。<code>logAndAdd</code>本身接受所有实参类型,包括整型和非整型。</p>
|
||||
<p>这两个真正执行逻辑的函数命名为<code>logAndAddImpl</code>,即我们使用重载。其中一个函数接受通用引用。所以我们同时使用了重载和通用引用。但是每个函数接受第二个形参,表征传入的实参是否为整型。这第二个形参可以帮助我们避免陷入到<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中提到的麻烦中,因为我们将其安排为第二个实参决定选择哪个重载函数。</p>
|
||||
<p>是的,我知道,“不要在啰嗦了,赶紧亮出代码”。没有问题,代码如下,这是最接近正确版本的:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void logAndAdd(T&& name)
|
||||
@ -187,8 +187,8 @@ void logAndAdd(T&& name)
|
||||
std::is_integral<T>()); //不那么正确
|
||||
}
|
||||
</code></pre>
|
||||
<p>这个函数转发它的形参给<code>logAndAddImpl</code>函数,但是多传递了一个表示形参<code>T</code>是否为整型的实参。至少,这就是应该做的。对于右值的整型实参来说,这也是正确的。但是如同<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>中说明,如果左值实参传递给通用引用<code>name</code>,对<code>T</code>类型推断会得到左值引用。所以如果左值<code>int</code>被传入<code>logAndAdd</code>,<code>T</code>将被推断为<code>int&</code>。这不是一个整型类型,因为引用不是整型类型。这意味着<code>std::is_integral<T></code>对于任何左值实参返回false,即使确实传入了整型值。</p>
|
||||
<p>意识到这个问题基本相当于解决了它,因为C++标准库有一个<em>type trait</em>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>),<code>std::remove_reference</code>,函数名字就说明做了我们希望的:移除类型的引用说明符。所以正确实现的代码应该是这样:</p>
|
||||
<p>这个函数转发它的形参给<code>logAndAddImpl</code>函数,但是多传递了一个表示形参<code>T</code>是否为整型的实参。至少,这就是应该做的。对于右值的整型实参来说,这也是正确的。但是如同<a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>中说明,如果左值实参传递给通用引用<code>name</code>,对<code>T</code>类型推断会得到左值引用。所以如果左值<code>int</code>被传入<code>logAndAdd</code>,<code>T</code>将被推断为<code>int&</code>。这不是一个整型类型,因为引用不是整型类型。这意味着<code>std::is_integral<T></code>对于任何左值实参返回false,即使确实传入了整型值。</p>
|
||||
<p>意识到这个问题基本相当于解决了它,因为C++标准库有一个<em>type trait</em>(参见<a href="../3.MovingToModernCpp/item9.html">Item9</a>),<code>std::remove_reference</code>,函数名字就说明做了我们希望的:移除类型的引用说明符。所以正确实现的代码应该是这样:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void logAndAdd(T&& name)
|
||||
{
|
||||
@ -198,7 +198,7 @@ void logAndAdd(T&& name)
|
||||
);
|
||||
}
|
||||
</code></pre>
|
||||
<p>这个代码很巧妙。(在C++14中,你可以通过<code>std::remove_reference_t<T></code>来简化写法,参看<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>)</p>
|
||||
<p>这个代码很巧妙。(在C++14中,你可以通过<code>std::remove_reference_t<T></code>来简化写法,参看<a href="../3.MovingToModernCpp/item9.html">Item9</a>)</p>
|
||||
<p>处理完之后,我们可以将注意力转移到名为<code>logAndAddImpl</code>的函数上了。有两个重载函数,第一个仅用于非整型类型(即<code>std::is_integral<typename std::remove_reference<T>::type></code>是false):</p>
|
||||
<pre><code class="language-cpp">template<typename T> //非整型实参:添加到全局数据结构中
|
||||
void logAndAddImpl(T&& name, std::false_type) //译者注:高亮std::false_type
|
||||
@ -218,13 +218,13 @@ void logAndAddImpl(int idx, std::true_type) //译者注:高亮std::true_type
|
||||
</code></pre>
|
||||
<p>通过索引找到对应的<code>name</code>,然后让<code>logAndAddImpl</code>传递给<code>logAndAdd</code>(名字会被再<code>std::forward</code>给另一个<code>logAndAddImpl</code>重载),我们避免了将日志代码放入这个<code>logAndAddImpl</code>重载中。</p>
|
||||
<p>在这个设计中,类型<code>std::true_type</code>和<code>std::false_type</code>是“标签”(tag),其唯一目的就是强制重载解析按照我们的想法来执行。注意到我们甚至没有对这些参数进行命名。他们在运行时毫无用处,事实上我们希望编译器可以意识到这些标签形参没被使用,然后在程序执行时优化掉它们。(至少某些时候有些编译器会这样做。)通过创建标签对象,在<code>logAndAdd</code>内部将重载实现函数的调用“分发”(<em>dispatch</em>)给正确的重载。因此这个设计名称为:<em>tag dispatch</em>。这是模板元编程的标准构建模块,你对现代C++库中的代码了解越多,你就会越多遇到这种设计。</p>
|
||||
<p>就我们的目的而言,<em>tag dispatch</em>的重要之处在于它可以允许我们组合重载和通用引用使用,而没有<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中提到的问题。分发函数——<code>logAndAdd</code>——接受一个没有约束的通用引用参数,但是这个函数没有重载。实现函数——<code>logAndAddImpl</code>——是重载的,一个接受通用引用参数,但是重载规则不仅依赖通用引用形参,还依赖新引入的标签形参,标签值设计来保证有不超过一个的重载是合适的匹配。结果是标签来决定采用哪个重载函数。通用引用参数可以生成精确匹配的事实在这里并不重要。(译者注:这里确实比较啰嗦,如果理解了上面的内容,这段完全可以没有。)</p>
|
||||
<p>就我们的目的而言,<em>tag dispatch</em>的重要之处在于它可以允许我们组合重载和通用引用使用,而没有<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中提到的问题。分发函数——<code>logAndAdd</code>——接受一个没有约束的通用引用参数,但是这个函数没有重载。实现函数——<code>logAndAddImpl</code>——是重载的,一个接受通用引用参数,但是重载规则不仅依赖通用引用形参,还依赖新引入的标签形参,标签值设计来保证有不超过一个的重载是合适的匹配。结果是标签来决定采用哪个重载函数。通用引用参数可以生成精确匹配的事实在这里并不重要。(译者注:这里确实比较啰嗦,如果理解了上面的内容,这段完全可以没有。)</p>
|
||||
<h3 id="约束使用通用引用的模板"><a class="header" href="#约束使用通用引用的模板">约束使用通用引用的模板</a></h3>
|
||||
<p><em>tag dispatch</em>的关键是存在单独一个函数(没有重载)给客户端API。这个单独的函数分发给具体的实现函数。创建一个没有重载的分发函数通常是容易的,但是<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中所述第二个问题案例是<code>Person</code>类的完美转发构造函数,是个例外。编译器可能会自行生成拷贝和移动构造函数,所以即使你只写了一个构造函数并在其中使用<em>tag dispatch</em>,有一些对构造函数的调用也被编译器生成的函数处理,绕过了分发机制。</p>
|
||||
<p>实际上,真正的问题不是编译器生成的函数会绕过<em>tag dispatch</em>设计,而是不<strong>总</strong>会绕过去。你希望类的拷贝构造函数总是处理该类型的左值拷贝请求,但是如同<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中所述,提供具有通用引用的构造函数,会使通用引用构造函数在拷贝non-<code>const</code>左值时被调用(而不是拷贝构造函数)。那个条款还说明了当一个基类声明了完美转发构造函数,派生类实现自己的拷贝和移动构造函数时会调用那个完美转发构造函数,尽管正确的行为是调用基类的拷贝或者移动构造。</p>
|
||||
<p><em>tag dispatch</em>的关键是存在单独一个函数(没有重载)给客户端API。这个单独的函数分发给具体的实现函数。创建一个没有重载的分发函数通常是容易的,但是<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中所述第二个问题案例是<code>Person</code>类的完美转发构造函数,是个例外。编译器可能会自行生成拷贝和移动构造函数,所以即使你只写了一个构造函数并在其中使用<em>tag dispatch</em>,有一些对构造函数的调用也被编译器生成的函数处理,绕过了分发机制。</p>
|
||||
<p>实际上,真正的问题不是编译器生成的函数会绕过<em>tag dispatch</em>设计,而是不<strong>总</strong>会绕过去。你希望类的拷贝构造函数总是处理该类型的左值拷贝请求,但是如同<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中所述,提供具有通用引用的构造函数,会使通用引用构造函数在拷贝non-<code>const</code>左值时被调用(而不是拷贝构造函数)。那个条款还说明了当一个基类声明了完美转发构造函数,派生类实现自己的拷贝和移动构造函数时会调用那个完美转发构造函数,尽管正确的行为是调用基类的拷贝或者移动构造。</p>
|
||||
<p>这种情况,采用通用引用的重载函数通常比期望的更加贪心,虽然不像单个分派函数一样那么贪心,而又不满足使用<em>tag dispatch</em>的条件。你需要另外的技术,可以让你确定允许使用通用引用模板的条件。朋友,你需要的就是<code>std::enable_if</code>。</p>
|
||||
<p><code>std::enable_if</code>可以给你提供一种强制编译器执行行为的方法,像是特定模板不存在一样。这种模板被称为被<strong>禁止</strong>(disabled)。默认情况下,所有模板是<strong>启用</strong>的(enabled),但是使用<code>std::enable_if</code>可以使得仅在<code>std::enable_if</code>指定的条件满足时模板才启用。在这个例子中,我们只在传递的类型不是<code>Person</code>时使用<code>Person</code>的完美转发构造函数。如果传递的类型是<code>Person</code>,我们要禁止完美转发构造函数(即让编译器忽略它),因为这会让拷贝或者移动构造函数处理调用,这是我们想要使用<code>Person</code>初始化另一个<code>Person</code>的初衷。</p>
|
||||
<p>这个主意听起来并不难,但是语法比较繁杂,尤其是之前没有接触过的话,让我慢慢引导你。有一些<code>std::enbale_if</code>的contidion(条件)部分的样板,让我们从这里开始。下面的代码是<code>Person</code>完美转发构造函数的声明,多展示<code>std::enable_if</code>的部分来简化使用难度。我仅展示构造函数的声明,因为<code>std::enable_if</code>的使用对函数实现没影响。实现部分跟<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中没有区别。</p>
|
||||
<p>这个主意听起来并不难,但是语法比较繁杂,尤其是之前没有接触过的话,让我慢慢引导你。有一些<code>std::enbale_if</code>的contidion(条件)部分的样板,让我们从这里开始。下面的代码是<code>Person</code>完美转发构造函数的声明,多展示<code>std::enable_if</code>的部分来简化使用难度。我仅展示构造函数的声明,因为<code>std::enable_if</code>的使用对函数实现没影响。实现部分跟<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中没有区别。</p>
|
||||
<pre><code class="language-cpp">class Person {
|
||||
public:
|
||||
template<typename T,
|
||||
@ -234,7 +234,7 @@ public:
|
||||
};
|
||||
</code></pre>
|
||||
<p>为了理解高亮部分发生了什么,我很遗憾的表示你要自行参考其他代码,因为详细解释需要花费一定空间和时间,而本书并没有足够的空间(在你自行学习过程中,请研究“SFINAE”以及<code>std::enable_if</code>,因为“SFINAE”就是使<code>std::enable_if</code>起作用的技术)。这里我想要集中讨论条件的表示,该条件表示此构造函数是否启用。</p>
|
||||
<p>这里我们想表示的条件是确认<code>T</code>不是<code>Person</code>类型,即模板构造函数应该在<code>T</code>不是<code>Person</code>类型的时候启用。多亏了<em>type trait</em>可以确定两个对象类型是否相同(<code>std::is_same</code>),看起来我们需要的就是<code>!std::is_same<Person, T>::value</code>(注意语句开始的<code>!</code>,我们想要的是<strong>不</strong>相同)。这很接近我们想要的了,但是不完全正确,因为如同<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>中所述,使用左值来初始化通用引用的话会推导成左值引用,比如这个代码:</p>
|
||||
<p>这里我们想表示的条件是确认<code>T</code>不是<code>Person</code>类型,即模板构造函数应该在<code>T</code>不是<code>Person</code>类型的时候启用。多亏了<em>type trait</em>可以确定两个对象类型是否相同(<code>std::is_same</code>),看起来我们需要的就是<code>!std::is_same<Person, T>::value</code>(注意语句开始的<code>!</code>,我们想要的是<strong>不</strong>相同)。这很接近我们想要的了,但是不完全正确,因为如同<a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>中所述,使用左值来初始化通用引用的话会推导成左值引用,比如这个代码:</p>
|
||||
<pre><code class="language-cpp">Person p("Nancy");
|
||||
auto cloneOfP(p); //用左值初始化
|
||||
</code></pre>
|
||||
@ -244,10 +244,10 @@ auto cloneOfP(p); //用左值初始化
|
||||
<li><strong>是否是个引用</strong>。对于决定是否通用引用构造函数启用的目的来说,<code>Person</code>,<code>Person&</code>,<code>Person&&</code>都是跟<code>Person</code>一样的。</li>
|
||||
<li><strong>是不是<code>const</code>或者<code>volatile</code></strong>。如上所述,<code>const Person</code>,<code>volatile Person</code> ,<code>const volatile Person</code>也是跟<code>Person</code>一样的。</li>
|
||||
</ul>
|
||||
<p>这意味着我们需要一种方法消除对于<code>T</code>的引用,<code>const</code>,<code>volatile</code>修饰。再次,标准库提供了这样功能的<em>type trait</em>,就是<code>std::decay</code>。<code>std::decay<T>::value</code>与<code>T</code>是相同的,只不过会移除引用和cv限定符(<em>cv-qualifiers</em>,即<code>const</code>或<code>volatile</code>标识符)的修饰。(这里我没有说出另外的真相,<code>std::decay</code>如同其名一样,可以将数组或者函数退化成指针,参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md">Item1</a>,但是在这里讨论的问题中,它刚好合适)。我们想要控制构造函数是否启用的条件可以写成:</p>
|
||||
<p>这意味着我们需要一种方法消除对于<code>T</code>的引用,<code>const</code>,<code>volatile</code>修饰。再次,标准库提供了这样功能的<em>type trait</em>,就是<code>std::decay</code>。<code>std::decay<T>::value</code>与<code>T</code>是相同的,只不过会移除引用和cv限定符(<em>cv-qualifiers</em>,即<code>const</code>或<code>volatile</code>标识符)的修饰。(这里我没有说出另外的真相,<code>std::decay</code>如同其名一样,可以将数组或者函数退化成指针,参考<a href="../1.DeducingTypes/item1.html">Item1</a>,但是在这里讨论的问题中,它刚好合适)。我们想要控制构造函数是否启用的条件可以写成:</p>
|
||||
<pre><code class="language-cpp">!std::is_same<Person, typename std::decay<T>::type>::value
|
||||
</code></pre>
|
||||
<p>即<code>Person</code>和<code>T</code>的类型不同,忽略了所有引用和cv限定符。(如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>所述,<code>std::decay</code>前的“<code>typename</code>”是必需的,因为<code>std::decay<T>::type</code>的类型取决于模板形参<code>T</code>。)</p>
|
||||
<p>即<code>Person</code>和<code>T</code>的类型不同,忽略了所有引用和cv限定符。(如<a href="../3.MovingToModernCpp/item9.html">Item9</a>所述,<code>std::decay</code>前的“<code>typename</code>”是必需的,因为<code>std::decay<T>::type</code>的类型取决于模板形参<code>T</code>。)</p>
|
||||
<p>将其带回上面<code>std::enable_if</code>样板的代码中,加上调整一下格式,让各部分如何组合在一起看起来更容易,<code>Person</code>的完美转发构造函数的声明如下:</p>
|
||||
<pre><code class="language-cpp">class Person {
|
||||
public:
|
||||
@ -265,7 +265,7 @@ public:
|
||||
</code></pre>
|
||||
<p>如果你之前从没有看到过这种类型的代码,那你可太幸福了。最后才放出这种设计是有原因的。当你有其他机制来避免同时使用重载和通用引用时(你总会这样做),确实应该那样做。不过,一旦你习惯了使用函数语法和尖括号的使用,也不坏。此外,这可以提供你一直想要的行为表现。在上面的声明中,使用<code>Person</code>初始化一个<code>Person</code>——无论是左值还是右值,<code>const</code>还是non-<code>const</code>,<code>volatile</code>还是non-<code>volatile</code>——都不会调用到通用引用构造函数。</p>
|
||||
<p>成功了,对吗?确实!</p>
|
||||
<p>啊,不对。等会再庆祝。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>还有一个情景需要解决,我们需要继续探讨下去。</p>
|
||||
<p>啊,不对。等会再庆祝。<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>还有一个情景需要解决,我们需要继续探讨下去。</p>
|
||||
<p>假定从<code>Person</code>派生的类以常规方式实现拷贝和移动操作:</p>
|
||||
<pre><code class="language-cpp">class SpecialPerson: public Person {
|
||||
public:
|
||||
@ -280,7 +280,7 @@ public:
|
||||
…
|
||||
};
|
||||
</code></pre>
|
||||
<p>这和<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>中的代码是一样的,包括注释也是一样。当我们拷贝或者移动一个<code>SpecialPerson</code>对象时,我们希望调用基类对应的拷贝和移动构造函数,来拷贝或者移动基类部分,但是这里,我们将<code>SpecialPerson</code>传递给基类的构造函数,因为<code>SpecialPerson</code>和<code>Person</code>类型不同(在应用<code>std::decay</code>后也不同),所以完美转发构造函数是启用的,会实例化为精确匹配<code>SpecialPerson</code>实参的构造函数。相比于派生类到基类的转化——这个转化对于在<code>Person</code>拷贝和移动构造函数中把<code>SpecialPerson</code>对象绑定到<code>Person</code>形参非常重要,生成的精确匹配是更优的,所以这里的代码,拷贝或者移动<code>SpecialPerson</code>对象就会调用<code>Person</code>类的完美转发构造函数来执行基类的部分。跟<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md">Item26</a>的困境一样。</p>
|
||||
<p>这和<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>中的代码是一样的,包括注释也是一样。当我们拷贝或者移动一个<code>SpecialPerson</code>对象时,我们希望调用基类对应的拷贝和移动构造函数,来拷贝或者移动基类部分,但是这里,我们将<code>SpecialPerson</code>传递给基类的构造函数,因为<code>SpecialPerson</code>和<code>Person</code>类型不同(在应用<code>std::decay</code>后也不同),所以完美转发构造函数是启用的,会实例化为精确匹配<code>SpecialPerson</code>实参的构造函数。相比于派生类到基类的转化——这个转化对于在<code>Person</code>拷贝和移动构造函数中把<code>SpecialPerson</code>对象绑定到<code>Person</code>形参非常重要,生成的精确匹配是更优的,所以这里的代码,拷贝或者移动<code>SpecialPerson</code>对象就会调用<code>Person</code>类的完美转发构造函数来执行基类的部分。跟<a href="../5.RRefMovSemPerfForw/item26.html">Item26</a>的困境一样。</p>
|
||||
<p>派生类仅仅是按照常规的规则生成了自己的移动和拷贝构造函数,所以这个问题的解决还要落实在基类,尤其是控制是否使用<code>Person</code>通用引用构造函数启用的条件。现在我们意识到不只是禁止<code>Person</code>类型启用模板构造函数,而是禁止<code>Person</code>**以及任何派生自<code>Person</code>**的类型启用模板构造函数。讨厌的继承!</p>
|
||||
<p>你应该不意外在这里看到标准库中也有<em>type trait</em>判断一个类型是否继承自另一个类型,就是<code>std::is_base_of</code>。如果<code>std::is_base_of<T1, T2></code>是true就表示<code>T2</code>派生自<code>T1</code>。类型也可被认为是从他们自己派生,所以<code>std::is_base_of<T, T>::value</code>总是true。这就很方便了,我们想要修正控制<code>Person</code>完美转发构造函数的启用条件,只有当<code>T</code>在消除引用和cv限定符之后,并且既不是<code>Person</code>又不是<code>Person</code>的派生类时,才满足条件。所以使用<code>std::is_base_of</code>代替<code>std::is_same</code>就可以了:</p>
|
||||
<pre><code class="language-cpp">class Person {
|
||||
@ -343,7 +343,7 @@ private:
|
||||
<h3 id="折中"><a class="header" href="#折中">折中</a></h3>
|
||||
<p>本条款提到的前三个技术——放弃重载、传递const T&、传值——在函数调用中指定每个形参的类型。后两个技术——<em>tag dispatch</em>和限制模板适用范围——使用完美转发,因此不需要指定形参类型。这一基本决定(是否指定类型)有一定后果。</p>
|
||||
<p>通常,完美转发更有效率,因为它避免了仅仅去为了符合形参声明的类型而创建临时对象。在<code>Person</code>构造函数的例子中,完美转发允许将“<code>Nancy</code>”这种字符串字面量转发到<code>Person</code>内部的<code>std::string</code>的构造函数,不使用完美转发的技术则会从字符串字面值创建一个临时<code>std::string</code>对象,来满足<code>Person</code>构造函数指定的形参要求。</p>
|
||||
<p>但是完美转发也有缺点。即使某些类型的实参可以传递给接受特定类型的函数,也无法完美转发。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>中探索了完美转发失败的例子。</p>
|
||||
<p>但是完美转发也有缺点。即使某些类型的实参可以传递给接受特定类型的函数,也无法完美转发。<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>中探索了完美转发失败的例子。</p>
|
||||
<p>第二个问题是当客户传递无效参数时错误消息的可理解性。例如假如客户传递了一个由<code>char16_t</code>(一种C++11引入的类型表示16位字符)而不是<code>char</code>(<code>std::string</code>包含的)组成的字符串字面值来创建一个<code>Person</code>对象:</p>
|
||||
<pre><code class="language-cpp">Person p(u"Konrad Zuse"); //“Konrad Zuse”由const char16_t类型字符组成
|
||||
</code></pre>
|
||||
|
@ -141,7 +141,7 @@
|
||||
<main>
|
||||
<h2 id="条款二十八理解引用折叠"><a class="header" href="#条款二十八理解引用折叠">条款二十八:理解引用折叠</a></h2>
|
||||
<p><strong>Item 28: Understand reference collapsing</strong></p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>中指出,当实参传递给模板函数时,被推导的模板形参<code>T</code>根据实参是左值还是右值来编码。但是那条款并没有提到只有当实参被用来实例化通用引用形参时,上述推导才会发生,但是有充分的理由忽略这一点:因为通用引用是<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>中才提到。回过头来看,对通用引用和左值/右值编码的观察意味着对于这个模板,</p>
|
||||
<p><a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>中指出,当实参传递给模板函数时,被推导的模板形参<code>T</code>根据实参是左值还是右值来编码。但是那条款并没有提到只有当实参被用来实例化通用引用形参时,上述推导才会发生,但是有充分的理由忽略这一点:因为通用引用是<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>中才提到。回过头来看,对通用引用和左值/右值编码的观察意味着对于这个模板,</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void func(T&& param);
|
||||
</code></pre>
|
||||
@ -167,7 +167,7 @@ func(w); //用左值调用func;T被推导为Widget&
|
||||
<p>如果我们用<code>T</code>推导出来的类型(即<code>Widget&</code>)初始化模板,会得到:</p>
|
||||
<pre><code class="language-cpp">void func(Widget& && param);
|
||||
</code></pre>
|
||||
<p>引用的引用!但是编译器没有报错。我们从<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>中了解到因为通用引用<code>param</code>被传入一个左值,所以<code>param</code>的类型应该为左值引用,但是编译器如何把<code>T</code>推导的类型带入模板变成如下的结果,也就是最终的函数签名?</p>
|
||||
<p>引用的引用!但是编译器没有报错。我们从<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>中了解到因为通用引用<code>param</code>被传入一个左值,所以<code>param</code>的类型应该为左值引用,但是编译器如何把<code>T</code>推导的类型带入模板变成如下的结果,也就是最终的函数签名?</p>
|
||||
<pre><code class="language-cpp">void func(Widget& param);
|
||||
</code></pre>
|
||||
<p>答案是<strong>引用折叠</strong>(<em>reference collapsing</em>)。是的,禁止<strong>你</strong>声明引用的引用,但是<strong>编译器</strong>会在特定的上下文中产生这些,模板实例化就是其中一种情况。当编译器生成引用的引用时,引用折叠指导下一步发生什么。</p>
|
||||
@ -176,7 +176,7 @@ func(w); //用左值调用func;T被推导为Widget&
|
||||
<p>如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用。</p>
|
||||
</blockquote>
|
||||
<p>在我们上面的例子中,将推导类型<code>Widget&</code>替换进模板<code>func</code>会产生对左值引用的右值引用,然后引用折叠规则告诉我们结果就是左值引用。</p>
|
||||
<p>引用折叠是<code>std::forward</code>工作的一种关键机制。就像<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>中解释的一样,<code>std::forward</code>应用在通用引用参数上,所以经常能看到这样使用:</p>
|
||||
<p>引用折叠是<code>std::forward</code>工作的一种关键机制。就像<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>中解释的一样,<code>std::forward</code>应用在通用引用参数上,所以经常能看到这样使用:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void f(T&& fParam)
|
||||
{
|
||||
@ -199,7 +199,7 @@ T&& forward(typename
|
||||
remove_reference<Widget&>::type& param)
|
||||
{ return static_cast<Widget& &&>(param); }
|
||||
</code></pre>
|
||||
<p><code>std::remove_reference<Widget&>::type</code>这个<em>type trait</em>产生<code>Widget</code>(查看<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>),所以<code>std::forward</code>成为:</p>
|
||||
<p><code>std::remove_reference<Widget&>::type</code>这个<em>type trait</em>产生<code>Widget</code>(查看<a href="../3.MovingToModernCpp/item9.html">Item9</a>),所以<code>std::forward</code>成为:</p>
|
||||
<pre><code class="language-cpp">Widget& && forward(Widget& param)
|
||||
{ return static_cast<Widget& &&>(param); }
|
||||
</code></pre>
|
||||
@ -226,7 +226,7 @@ T&& forward(remove_reference_t<T>& param)
|
||||
return static_cast<T&&>(param);
|
||||
}
|
||||
</code></pre>
|
||||
<p>引用折叠发生在四种情况下。第一,也是最常见的就是模板实例化。第二,是<code>auto</code>变量的类型生成,具体细节类似于模板,因为<code>auto</code>变量的类型推导基本与模板类型推导雷同(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>)。考虑本条款前面的例子:</p>
|
||||
<p>引用折叠发生在四种情况下。第一,也是最常见的就是模板实例化。第二,是<code>auto</code>变量的类型生成,具体细节类似于模板,因为<code>auto</code>变量的类型推导基本与模板类型推导雷同(参见<a href="../1.DeducingTypes/item2.html">Item2</a>)。考虑本条款前面的例子:</p>
|
||||
<pre><code class="language-cpp">Widget widgetFactory(); //返回右值的函数
|
||||
Widget w; //一个变量(左值)
|
||||
func(w); //用左值调用func;T被推导为Widget&
|
||||
@ -249,13 +249,13 @@ func(widgetFactory()); //用又值调用func;T被推导为Widget
|
||||
<pre><code class="language-cpp">Widget&& w2 = widgetFactory()
|
||||
</code></pre>
|
||||
<p>没有引用的引用,这就是最终结果,<code>w2</code>是个右值引用。</p>
|
||||
<p>现在我们真正理解了<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>中引入的通用引用。通用引用不是一种新的引用,它实际上是满足以下两个条件下的右值引用:</p>
|
||||
<p>现在我们真正理解了<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>中引入的通用引用。通用引用不是一种新的引用,它实际上是满足以下两个条件下的右值引用:</p>
|
||||
<ul>
|
||||
<li><strong>类型推导区分左值和右值</strong>。<code>T</code>类型的左值被推导为<code>T&</code>类型,<code>T</code>类型的右值被推导为<code>T</code>。</li>
|
||||
<li><strong>发生引用折叠</strong>。</li>
|
||||
</ul>
|
||||
<p>通用引用的概念是有用的,因为它使你不必一定意识到引用折叠的存在,从直觉上推导左值和右值的不同类型,在凭直觉把推导的类型代入到它们出现的上下文中之后应用引用折叠规则。</p>
|
||||
<p>我说了有四种情况会发生引用折叠,但是只讨论了两种:模板实例化和<code>auto</code>的类型生成。第三种情况是<code>typedef</code>和别名声明的产生和使用中(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>)。如果,在创建或者评估<code>typedef</code>过程中出现了引用的引用,则引用折叠就会起作用。举例子来说,假设我们有一个<code>Widget</code>的类模板,该模板具有右值引用类型的嵌入式<code>typedef</code>:</p>
|
||||
<p>我说了有四种情况会发生引用折叠,但是只讨论了两种:模板实例化和<code>auto</code>的类型生成。第三种情况是<code>typedef</code>和别名声明的产生和使用中(参见<a href="../3.MovingToModernCpp/item9.html">Item9</a>)。如果,在创建或者评估<code>typedef</code>过程中出现了引用的引用,则引用折叠就会起作用。举例子来说,假设我们有一个<code>Widget</code>的类模板,该模板具有右值引用类型的嵌入式<code>typedef</code>:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
class Widget {
|
||||
public:
|
||||
@ -273,7 +273,7 @@ public:
|
||||
<pre><code class="language-cpp">typedef int& RvalueRefToT;
|
||||
</code></pre>
|
||||
<p>这清楚表明我们为<code>typedef</code>选择的名字可能不是我们希望的那样:当使用左值引用类型实例化<code>Widget</code>时,<code>RvalueRefToT</code>是<strong>左值引用</strong>的<code>typedef</code>。</p>
|
||||
<p>最后一种引用折叠发生的情况是,<code>decltype</code>使用的情况。如果在分析<code>decltype</code>期间,出现了引用的引用,引用折叠规则就会起作用(关于<code>decltype</code>,参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>)</p>
|
||||
<p>最后一种引用折叠发生的情况是,<code>decltype</code>使用的情况。如果在分析<code>decltype</code>期间,出现了引用的引用,引用折叠规则就会起作用(关于<code>decltype</code>,参见<a href="../1.DeducingTypes/item3.html">Item3</a>)</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>引用折叠发生在四种情况下:模板实例化,<code>auto</code>类型推导,<code>typedef</code>与别名声明的创建和使用,<code>decltype</code>。</li>
|
||||
|
@ -143,7 +143,7 @@
|
||||
<p><strong>Item 29: Assume that move operations are not present, not cheap, and not used</strong></p>
|
||||
<p>移动语义可以说是C++11最主要的特性。你可能会见过这些类似的描述“移动容器和拷贝指针一样开销小”, “拷贝临时对象现在如此高效,写代码避免这种情况简直就是过早优化”。这种情绪很容易理解。移动语义确实是这样重要的特性。它不仅允许编译器使用开销小的移动操作代替大开销的复制操作,而且默认这么做(当特定条件满足的时候)。以C++98的代码为基础,使用C++11重新编译你的代码,然后,哇,你的软件运行的更快了。</p>
|
||||
<p>移动语义确实可以做这些事,这把这个特性封为一代传说。但是传说总有些夸大成分。这个条款的目的就是给你泼一瓢冷水,保持理智看待移动语义。</p>
|
||||
<p>让我们从已知很多类型不支持移动操作开始这个过程。为了升级到C++11,C++98的很多标准库做了大修改,为很多类型提供了移动的能力,这些类型的移动实现比复制操作更快,并且对库的组件实现修改以利用移动操作。但是很有可能你工作中的代码没有完整地利用C++11。对于你的应用中(或者代码库中)的类型,没有适配C++11的部分,编译器即使支持移动语义也是无能为力的。的确,C++11倾向于为缺少移动操作的类生成它们,但是只有在没有声明复制操作,移动操作,或析构函数的类中才会生成移动操作(参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>)。数据成员或者某类型的基类禁止移动操作(比如通过delete移动操作,参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item11.md">Item11</a>),编译器不生成移动操作的支持。对于没有明确支持移动操作的类型,并且不符合编译器默认生成的条件的类,没有理由期望C++11会比C++98进行任何性能上的提升。</p>
|
||||
<p>让我们从已知很多类型不支持移动操作开始这个过程。为了升级到C++11,C++98的很多标准库做了大修改,为很多类型提供了移动的能力,这些类型的移动实现比复制操作更快,并且对库的组件实现修改以利用移动操作。但是很有可能你工作中的代码没有完整地利用C++11。对于你的应用中(或者代码库中)的类型,没有适配C++11的部分,编译器即使支持移动语义也是无能为力的。的确,C++11倾向于为缺少移动操作的类生成它们,但是只有在没有声明复制操作,移动操作,或析构函数的类中才会生成移动操作(参考<a href="../3.MovingToModernCpp/item17.html">Item17</a>)。数据成员或者某类型的基类禁止移动操作(比如通过delete移动操作,参考<a href="../3.MovingToModernCpp/item11.html">Item11</a>),编译器不生成移动操作的支持。对于没有明确支持移动操作的类型,并且不符合编译器默认生成的条件的类,没有理由期望C++11会比C++98进行任何性能上的提升。</p>
|
||||
<p>即使显式支持了移动操作,结果可能也没有你希望的那么好。比如,所有C++11的标准库容器都支持了移动操作,但是认为移动所有容器的开销都非常小是个错误。对于某些容器来说,压根就不存在开销小的方式来移动它所包含的内容。对另一些容器来说,容器的开销真正小的移动操作会有些容器元素不能满足的注意条件。</p>
|
||||
<p>考虑一下<code>std::array</code>,这是C++11中的新容器。<code>std::array</code>本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器,本身只保存了指向堆内存中容器内容的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这个指针的存在使得在常数时间移动整个容器成为可能,只需要从源容器拷贝保存指向容器内容的指针到目标容器,然后将源指针置为空指针就可以了:</p>
|
||||
<pre><code class="language-cpp">std::vector<Widget> vm1;
|
||||
@ -168,7 +168,7 @@ auto aw2 = std::move(aw1);
|
||||
<p>注意<code>aw1</code>中的元素被<strong>移动</strong>到了<code>aw2</code>中。假定<code>Widget</code>类的移动操作比复制操作快,移动<code>Widget</code>的<code>std::array</code>就比复制要快。所以<code>std::array</code>确实支持移动操作。但是使用<code>std::array</code>的移动操作还是复制操作都将花费线性时间的开销,因为每个容器中的元素终归需要拷贝或移动一次,这与“移动一个容器就像操作几个指针一样方便”的含义相去甚远。</p>
|
||||
<p>另一方面,<code>std::string</code>提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了小字符串优化(<em>small string optimization</em>,SSO)。“小”字符串(比如长度小于15个字符的)存储在了<code>std::string</code>的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。</p>
|
||||
<p>SSO的动机是大量证据表明,短字符串是大量应用使用的习惯。使用内存缓冲区存储而不分配堆内存空间,是为了更好的效率。然而这种内存管理的效率导致移动的效率并不必复制操作高,即使一个半吊子程序员也能看出来对于这样的字符串,拷贝并不比移动慢。</p>
|
||||
<p>即使对于支持快速移动操作的类型,某些看似可靠的移动操作最终也会导致复制。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md">Item14</a>解释了原因,标准库中的某些容器操作提供了强大的异常安全保证,确保依赖那些保证的C++98的代码在升级到C++11且仅当移动操作不会抛出异常,从而可能替换操作时,不会不可运行。结果就是,即使类提供了更具效率的移动操作,而且即使移动操作更合适(比如源对象是右值),编译器仍可能被迫使用复制操作,因为移动操作没有声明<code>noexcept</code>。</p>
|
||||
<p>即使对于支持快速移动操作的类型,某些看似可靠的移动操作最终也会导致复制。<a href="../3.MovingToModernCpp/item14.html">Item14</a>解释了原因,标准库中的某些容器操作提供了强大的异常安全保证,确保依赖那些保证的C++98的代码在升级到C++11且仅当移动操作不会抛出异常,从而可能替换操作时,不会不可运行。结果就是,即使类提供了更具效率的移动操作,而且即使移动操作更合适(比如源对象是右值),编译器仍可能被迫使用复制操作,因为移动操作没有声明<code>noexcept</code>。</p>
|
||||
<p>因此,存在几种情况,C++11的移动语义并无优势:</p>
|
||||
<ul>
|
||||
<li><strong>没有移动操作</strong>:要移动的对象没有提供移动操作,所以移动的写法也会变成复制操作。</li>
|
||||
@ -177,7 +177,7 @@ auto aw2 = std::move(aw1);
|
||||
</ul>
|
||||
<p>值得一提的是,还有另一个场景,会使得移动并没有那么有效率:</p>
|
||||
<ul>
|
||||
<li><strong>源对象是左值</strong>:除了极少数的情况外(例如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>),只有右值可以作为移动操作的来源。</li>
|
||||
<li><strong>源对象是左值</strong>:除了极少数的情况外(例如<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>),只有右值可以作为移动操作的来源。</li>
|
||||
</ul>
|
||||
<p>但是该条款的标题是假定移动操作不存在,成本高,未被使用。这就是通用代码中的典型情况,比如编写模板代码,因为你不清楚你处理的具体类型是什么。在这种情况下,你必须像出现移动语义之前那样,像在C++98里一样保守地去复制对象。“不稳定的”代码也是如此,即那些由于经常被修改导致类型特性变化的源代码。</p>
|
||||
<p>但是,通常,你了解你代码里使用的类型,依赖他们的特性不变性(比如是否支持快速移动操作)。这种情况,你无需这个条款的假设,只需要查找所用类型的移动操作详细信息。如果类型提供了快速移动操作,并且在调用移动操作的上下文中使用对象,可以安全的使用快速移动操作替换复制操作。</p>
|
||||
|
@ -143,7 +143,7 @@
|
||||
<p><strong>Item 30: Familiarize yourself with perfect forwarding failure cases</strong></p>
|
||||
<p>C++11最显眼的功能之一就是完美转发功能。<strong>完美</strong>转发,太<strong>完美</strong>了!哎,开始使用,你就发现“完美”,理想与现实还是有差距。C++11的完美转发是非常好用,但是只有当你愿意忽略一些误差情况(译者注:就是完美转发失败的情况),这个条款就是使你熟悉这些情形。</p>
|
||||
<p>在我们开始误差探索之前,有必要回顾一下“完美转发”的含义。“转发”仅表示将一个函数的形参传递——就是<strong>转发</strong>——给另一个函数。对于第二个函数(被传递的那个)目标是收到与第一个函数(执行传递的那个)完全相同的对象。这规则排除了按值传递的形参,因为它们是原始调用者传入内容的<strong>拷贝</strong>。我们希望被转发的函数能够使用最开始传进来的那些对象。指针形参也被排除在外,因为我们不想强迫调用者传入指针。关于通常目的的转发,我们将处理引用形参。</p>
|
||||
<p><strong>完美转发</strong>(<em>perfect forwarding</em>)意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是<code>const</code>还是<code>volatile</code>。结合到我们会处理引用形参,这意味着我们将使用通用引用(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>),因为通用引用形参被传入实参时才确定是左值还是右值。</p>
|
||||
<p><strong>完美转发</strong>(<em>perfect forwarding</em>)意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是<code>const</code>还是<code>volatile</code>。结合到我们会处理引用形参,这意味着我们将使用通用引用(参见<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>),因为通用引用形参被传入实参时才确定是左值还是右值。</p>
|
||||
<p>假定我们有一些函数<code>f</code>,然后想编写一个转发给它的函数(事实上是一个函数模板)。我们需要的核心看起来像是这样:</p>
|
||||
<pre><code class="language-cpp">template<typename T>
|
||||
void fwd(T&& param) //接受任何实参
|
||||
@ -158,7 +158,7 @@ void fwd(Ts&&... params) //接受任何实参
|
||||
f(std::forward<Ts>(params)...); //转发给f
|
||||
}
|
||||
</code></pre>
|
||||
<p>这种形式你会在标准化容器置入函数(emplace functions)中(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item42.md">Item42</a>)和智能指针的工厂函数<code>std::make_unique</code>和<code>std::make_shared</code>中(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>)看到,当然还有其他一些地方。</p>
|
||||
<p>这种形式你会在标准化容器置入函数(emplace functions)中(参见<a href="../8.Tweaks/item42.html">Item42</a>)和智能指针的工厂函数<code>std::make_unique</code>和<code>std::make_shared</code>中(参见<a href="../4.SmartPointers/item21.html">Item21</a>)看到,当然还有其他一些地方。</p>
|
||||
<p>给定我们的目标函数<code>f</code>和转发函数<code>fwd</code>,如果<code>f</code>使用某特定实参会执行某个操作,但是<code>fwd</code>使用相同的实参会执行不同的操作,完美转发就会失败</p>
|
||||
<pre><code class="language-cpp">f( expression ); //调用f执行某个操作
|
||||
fwd( expression ); //但调用fwd执行另一个操作,则fwd不能完美转发expression给f
|
||||
@ -182,12 +182,12 @@ fwd( expression ); //但调用fwd执行另一个操作,则fwd不能完美转
|
||||
<li>**编译器推导“错”了<code>fwd</code>的一个或者多个形参类型。**在这里,“错误”可能意味着<code>fwd</code>的实例将无法使用推导出的类型进行编译,但是也可能意味着使用<code>fwd</code>的推导类型调用<code>f</code>,与用传给<code>fwd</code>的实参直接调用<code>f</code>表现出不一致的行为。这种不同行为的原因可能是因为<code>f</code>是个重载函数的名字,并且由于是“不正确的”类型推导,在<code>fwd</code>内部调用的<code>f</code>重载和直接调用的<code>f</code>重载不一样。</li>
|
||||
</ul>
|
||||
<p>在上面的<code>fwd({ 1, 2, 3 })</code>例子中,问题在于,将花括号初始化传递给未声明为<code>std::initializer_list</code>的函数模板形参,被判定为——就像标准说的——“非推导上下文”。简单来讲,这意味着编译器不准在对<code>fwd</code>的调用中推导表达式<code>{ 1, 2, 3 }</code>的类型,因为<code>fwd</code>的形参没有声明为<code>std::initializer_list</code>。对于<code>fwd</code>形参的推导类型被阻止,编译器只能拒绝该调用。</p>
|
||||
<p>有趣的是,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>说明了使用花括号初始化的<code>auto</code>的变量的类型推导是成功的。这种变量被视为<code>std::initializer_list</code>对象,在转发函数应推导出类型为<code>std::initializer_list</code>的情况,这提供了一种简单的解决方法——使用<code>auto</code>声明一个局部变量,然后将局部变量传进转发函数:</p>
|
||||
<p>有趣的是,<a href="../1.DeducingTypes/item2.html">Item2</a>说明了使用花括号初始化的<code>auto</code>的变量的类型推导是成功的。这种变量被视为<code>std::initializer_list</code>对象,在转发函数应推导出类型为<code>std::initializer_list</code>的情况,这提供了一种简单的解决方法——使用<code>auto</code>声明一个局部变量,然后将局部变量传进转发函数:</p>
|
||||
<pre><code class="language-cpp">auto il = { 1, 2, 3 }; //il的类型被推导为std::initializer_list<int>
|
||||
fwd(il); //可以,完美转发il给f
|
||||
</code></pre>
|
||||
<h3 id="0或者null作为空指针"><a class="header" href="#0或者null作为空指针"><code>0</code>或者<code>NULL</code>作为空指针</a></h3>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item8.md">Item8</a>说明当你试图传递<code>0</code>或者<code>NULL</code>作为空指针给模板时,类型推导会出错,会把传来的实参推导为一个整型类型(典型情况为<code>int</code>)而不是指针类型。结果就是不管是<code>0</code>还是<code>NULL</code>都不能作为空指针被完美转发。解决方法非常简单,传一个<code>nullptr</code>而不是<code>0</code>或者<code>NULL</code>。具体的细节,参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item8.md">Item8</a>。</p>
|
||||
<p><a href="../3.MovingToModernCpp/item8.html">Item8</a>说明当你试图传递<code>0</code>或者<code>NULL</code>作为空指针给模板时,类型推导会出错,会把传来的实参推导为一个整型类型(典型情况为<code>int</code>)而不是指针类型。结果就是不管是<code>0</code>还是<code>NULL</code>都不能作为空指针被完美转发。解决方法非常简单,传一个<code>nullptr</code>而不是<code>0</code>或者<code>NULL</code>。具体的细节,参考<a href="../3.MovingToModernCpp/item8.html">Item8</a>。</p>
|
||||
<h3 id="仅有声明的整型static-const数据成员"><a class="header" href="#仅有声明的整型static-const数据成员">仅有声明的整型<code>static const</code>数据成员</a></h3>
|
||||
<p>通常,无需在类中定义整型<code>static const</code>数据成员;声明就可以了。这是因为编译器会对此类成员实行<strong>常量传播</strong>(<em>const propagation</em>),因此消除了保留内存的需要。比如,考虑下面的代码:</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
|
@ -141,7 +141,7 @@
|
||||
<main>
|
||||
<h1 id="第6章-lambda表达式"><a class="header" href="#第6章-lambda表达式">第6章 <em>lambda</em>表达式</a></h1>
|
||||
<p><strong>CHAPTER 6 Lambda Expressions</strong></p>
|
||||
<p><em>lambda</em>表达式是C++编程中的游戏规则改变者。这有点令人惊讶,因为它没有给语言带来新的表达能力。<em>lambda</em>可以做的所有事情都可以通过其他方式完成。但是<em>lambda</em>是创建函数对象相当便捷的一种方法,对于日常的C++开发影响是巨大的。没有<em>lambda</em>时,STL中的“<code>_if</code>”算法(比如,<code>std::find_if</code>,<code>std::remove_if</code>,<code>std::count_if</code>等)通常需要繁琐的谓词,但是当有<em>lambda</em>可用时,这些算法使用起来就变得相当方便。用比较函数(比如,<code>std::sort</code>,<code>std::nth_element</code>,<code>std::lower_bound</code>等)来自定义算法也是同样方便的。在STL外,<em>lambda</em>可以快速创建<code>std::unique_ptr</code>和<code>std::shared_ptr</code>的自定义删除器(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>和<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">19</a>),并且使线程API中条件变量的谓词指定变得同样简单(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md">Item39</a>)。除了标准库,<em>lambda</em>有利于即时的回调函数,接口适配函数和特定上下文中的一次性函数。<em>lambda</em>确实使C++成为更令人愉快的编程语言。</p>
|
||||
<p><em>lambda</em>表达式是C++编程中的游戏规则改变者。这有点令人惊讶,因为它没有给语言带来新的表达能力。<em>lambda</em>可以做的所有事情都可以通过其他方式完成。但是<em>lambda</em>是创建函数对象相当便捷的一种方法,对于日常的C++开发影响是巨大的。没有<em>lambda</em>时,STL中的“<code>_if</code>”算法(比如,<code>std::find_if</code>,<code>std::remove_if</code>,<code>std::count_if</code>等)通常需要繁琐的谓词,但是当有<em>lambda</em>可用时,这些算法使用起来就变得相当方便。用比较函数(比如,<code>std::sort</code>,<code>std::nth_element</code>,<code>std::lower_bound</code>等)来自定义算法也是同样方便的。在STL外,<em>lambda</em>可以快速创建<code>std::unique_ptr</code>和<code>std::shared_ptr</code>的自定义删除器(见<a href="../4.SmartPointers/item18.html">Item18</a>和<a href="../4.SmartPointers/item19.html">19</a>),并且使线程API中条件变量的谓词指定变得同样简单(参见<a href="../7.TheConcurrencyAPI/item39.html">Item39</a>)。除了标准库,<em>lambda</em>有利于即时的回调函数,接口适配函数和特定上下文中的一次性函数。<em>lambda</em>确实使C++成为更令人愉快的编程语言。</p>
|
||||
<p>与<em>lambda</em>相关的词汇可能会令人疑惑,这里做一下简单的回顾:</p>
|
||||
<ul>
|
||||
<li>
|
||||
@ -246,7 +246,7 @@ void workWithContainer(const C& container)
|
||||
);
|
||||
</code></pre>
|
||||
<p>这足以满足本实例的要求,但在通常情况下,按值捕获并不能完全解决悬空引用的问题。这里的问题是如果你按值捕获的是一个指针,你将该指针拷贝到<em>lambda</em>对应的闭包里,但这样并不能避免<em>lambda</em>外<code>delete</code>这个指针的行为,从而导致你的副本指针变成悬空指针。</p>
|
||||
<p>也许你要抗议说:“这不可能发生。看过了<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">第4章</a>,我对智能指针的使用非常热衷。只有那些失败的C++98的程序员才会用裸指针和<code>delete</code>语句。”这也许是正确的,但却是不相关的,因为事实上你的确会使用裸指针,也的确存在被你<code>delete</code>的可能性。只不过在现代的C++编程风格中,不容易在源代码中显露出来而已。</p>
|
||||
<p>也许你要抗议说:“这不可能发生。看过了<a href="../4.SmartPointers/item18.html">第4章</a>,我对智能指针的使用非常热衷。只有那些失败的C++98的程序员才会用裸指针和<code>delete</code>语句。”这也许是正确的,但却是不相关的,因为事实上你的确会使用裸指针,也的确存在被你<code>delete</code>的可能性。只不过在现代的C++编程风格中,不容易在源代码中显露出来而已。</p>
|
||||
<p>假设在一个<code>Widget</code>类,可以实现向过滤器的容器添加条目:</p>
|
||||
<pre><code class="language-c++">class Widget {
|
||||
public:
|
||||
@ -303,7 +303,7 @@ private:
|
||||
);
|
||||
}
|
||||
</code></pre>
|
||||
<p>明白了这个就相当于明白了<em>lambda</em>闭包的生命周期与<code>Widget</code>对象的关系,闭包内含有<code>Widget</code>的<code>this</code>指针的拷贝。特别是考虑以下的代码,参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">第4章</a>的内容,只使用智能指针:</p>
|
||||
<p>明白了这个就相当于明白了<em>lambda</em>闭包的生命周期与<code>Widget</code>对象的关系,闭包内含有<code>Widget</code>的<code>this</code>指针的拷贝。特别是考虑以下的代码,参考<a href="../4.SmartPointers/item18.html">第4章</a>的内容,只使用智能指针:</p>
|
||||
<pre><code class="language-c++">using FilterContainer = //跟之前一样
|
||||
std::vector<std::function<bool(int)>>;
|
||||
|
||||
@ -319,7 +319,7 @@ void doSomeWork()
|
||||
…
|
||||
} //销毁Widget;filters现在持有悬空指针!
|
||||
</code></pre>
|
||||
<p>当调用<code>doSomeWork</code>时,就会创建一个过滤器,其生命周期依赖于由<code>std::make_unique</code>产生的<code>Widget</code>对象,即一个含有指向<code>Widget</code>的指针——<code>Widget</code>的<code>this</code>指针——的过滤器。这个过滤器被添加到<code>filters</code>中,但当<code>doSomeWork</code>结束时,<code>Widget</code>会由管理它的<code>std::unique_ptr</code>来销毁(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>)。从这时起,<code>filter</code>会含有一个存着悬空指针的条目。</p>
|
||||
<p>当调用<code>doSomeWork</code>时,就会创建一个过滤器,其生命周期依赖于由<code>std::make_unique</code>产生的<code>Widget</code>对象,即一个含有指向<code>Widget</code>的指针——<code>Widget</code>的<code>this</code>指针——的过滤器。这个过滤器被添加到<code>filters</code>中,但当<code>doSomeWork</code>结束时,<code>Widget</code>会由管理它的<code>std::unique_ptr</code>来销毁(见<a href="../4.SmartPointers/item18.html">Item18</a>)。从这时起,<code>filter</code>会含有一个存着悬空指针的条目。</p>
|
||||
<p>这个特定的问题可以通过给你想捕获的数据成员做一个局部副本,然后捕获这个副本去解决:</p>
|
||||
<pre><code class="language-c++">void Widget::addFilter() const
|
||||
{
|
||||
|
@ -143,7 +143,7 @@
|
||||
<p><strong>Item 32: Use init capture to move objects into closures</strong></p>
|
||||
<p>在某些场景下,按值捕获和按引用捕获都不是你所想要的。如果你有一个只能被移动的对象(例如<code>std::unique_ptr</code>或<code>std::future</code>)要进入到闭包里,使用C++11是无法实现的。如果你要复制的对象复制开销非常高,但移动的成本却不高(例如标准库中的大多数容器),并且你希望的是宁愿移动该对象到闭包而不是复制它。然而C++11却无法实现这一目标。</p>
|
||||
<p>但那是C++11的时候。到了C++14就另一回事了,它能支持将对象移动到闭包中。如果你的编译器兼容支持C++14,那么请愉快地阅读下去。如果你仍然在使用仅支持C++11的编译器,也请愉快阅读,因为在C++11中有很多方法可以实现近似的移动捕获。</p>
|
||||
<p>缺少移动捕获被认为是C++11的一个缺点,直接的补救措施是将该特性添加到C++14中,但标准化委员会选择了另一种方法。他们引入了一种新的捕获机制,该机制非常灵活,移动捕获是它可以执行的技术之一。新功能被称作<strong>初始化捕获</strong>(<em>init capture</em>),C++11捕获形式能做的所有事它几乎可以做,甚至能完成更多功能。你不能用初始化捕获表达的东西是默认捕获模式,但<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item31.md">Item31</a>说明提醒了你无论如何都应该远离默认捕获模式。(在C++11捕获模式所能覆盖的场景里,初始化捕获的语法有点不大方便。因此在C++11的捕获模式能完成所需功能的情况下,使用它是完全合理的)。</p>
|
||||
<p>缺少移动捕获被认为是C++11的一个缺点,直接的补救措施是将该特性添加到C++14中,但标准化委员会选择了另一种方法。他们引入了一种新的捕获机制,该机制非常灵活,移动捕获是它可以执行的技术之一。新功能被称作<strong>初始化捕获</strong>(<em>init capture</em>),C++11捕获形式能做的所有事它几乎可以做,甚至能完成更多功能。你不能用初始化捕获表达的东西是默认捕获模式,但<a href="../6.LambdaExpressions/item31.html">Item31</a>说明提醒了你无论如何都应该远离默认捕获模式。(在C++11捕获模式所能覆盖的场景里,初始化捕获的语法有点不大方便。因此在C++11的捕获模式能完成所需功能的情况下,使用它是完全合理的)。</p>
|
||||
<p>使用初始化捕获可以让你指定:</p>
|
||||
<ol>
|
||||
<li>从lambda生成的闭包类中的<strong>数据成员名称</strong>;</li>
|
||||
@ -223,7 +223,7 @@ auto func =
|
||||
);
|
||||
</code></pre>
|
||||
<p>如<em>lambda</em>表达式一样,<code>std::bind</code>产生函数对象。我将由<code>std::bind</code>返回的函数对象称为<strong>bind对象</strong>(<em>bind objects</em>)。<code>std::bind</code>的第一个实参是可调用对象,后续实参表示要传递给该对象的值。</p>
|
||||
<p>一个bind对象包含了传递给<code>std::bind</code>的所有实参的副本。对于每个左值实参,bind对象中的对应对象都是复制构造的。对于每个右值,它都是移动构造的。在此示例中,第二个实参是一个右值(<code>std::move</code>的结果,请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>),因此将<code>data</code>移动构造到绑定对象中。这种移动构造是模仿移动捕获的关键,因为将右值移动到bind对象是我们解决无法将右值移动到C++11闭包中的方法。</p>
|
||||
<p>一个bind对象包含了传递给<code>std::bind</code>的所有实参的副本。对于每个左值实参,bind对象中的对应对象都是复制构造的。对于每个右值,它都是移动构造的。在此示例中,第二个实参是一个右值(<code>std::move</code>的结果,请参见<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>),因此将<code>data</code>移动构造到绑定对象中。这种移动构造是模仿移动捕获的关键,因为将右值移动到bind对象是我们解决无法将右值移动到C++11闭包中的方法。</p>
|
||||
<p>当“调用”bind对象(即调用其函数调用运算符)时,其存储的实参将传递到最初传递给<code>std::bind</code>的可调用对象。在此示例中,这意味着当调用<code>func</code>(bind对象)时,<code>func</code>中所移动构造的<code>data</code>副本将作为实参传递给<code>std::bind</code>中的<em>lambda</em>。</p>
|
||||
<p>该<em>lambda</em>与我们在C++14中使用的<em>lambda</em>相同,只是添加了一个形参<code>data</code>来对应我们的伪移动捕获对象。此形参是对bind对象中<code>data</code>副本的左值引用。(这不是右值引用,因为尽管用于初始化<code>data</code>副本的表达式(<code>std::move(data)</code>)为右值,但<code>data</code>副本本身为左值。)因此,<em>lambda</em>将对绑定在对象内部的移动构造的<code>data</code>副本进行操作。</p>
|
||||
<p>默认情况下,从<em>lambda</em>生成的闭包类中的<code>operator()</code>成员函数为<code>const</code>的。这具有在<em>lambda</em>主体内把闭包中的所有数据成员渲染为<code>const</code>的效果。但是,bind对象内部的移动构造的<code>data</code>副本不是<code>const</code>的,因此,为了防止在<em>lambda</em>内修改该<code>data</code>副本,<em>lambda</em>的形参应声明为reference-to-<code>const</code>。 如果将<em>lambda</em>声明为<code>mutable</code>,则闭包类中的<code>operator()</code>将不会声明为<code>const</code>,并且在<em>lambda</em>的形参声明中省略<code>const</code>也是合适的:</p>
|
||||
@ -254,7 +254,7 @@ auto func =
|
||||
std::make_unique<Widget>()
|
||||
);
|
||||
</code></pre>
|
||||
<p>具备讽刺意味的是,这里我展示了如何使用<code>std::bind</code>解决C++11 <em>lambda</em>中的限制,因为在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item34.md">Item34</a>中,我主张使用<em>lambda</em>而不是<code>std::bind</code>。但是,该条款解释的是在C++11中有些情况下<code>std::bind</code>可能有用,这就是其中一种。 (在C++14中,初始化捕获和<code>auto</code>形参等特性使得这些情况不再存在。)</p>
|
||||
<p>具备讽刺意味的是,这里我展示了如何使用<code>std::bind</code>解决C++11 <em>lambda</em>中的限制,因为在<a href="../6.LambdaExpressions/item34.html">Item34</a>中,我主张使用<em>lambda</em>而不是<code>std::bind</code>。但是,该条款解释的是在C++11中有些情况下<code>std::bind</code>可能有用,这就是其中一种。 (在C++14中,初始化捕获和<code>auto</code>形参等特性使得这些情况不再存在。)</p>
|
||||
<p><strong>请记住:</strong></p>
|
||||
<ul>
|
||||
<li>使用C++14的初始化捕获将对象移动到闭包中。</li>
|
||||
|
@ -154,15 +154,15 @@ public:
|
||||
};
|
||||
</code></pre>
|
||||
<p>在这个样例中,<em>lambda</em>对变量<code>x</code>做的唯一一件事就是把它转发给函数<code>normalize</code>。如果函数<code>normalize</code>对待左值右值的方式不一样,这个<em>lambda</em>的实现方式就不大合适了,因为即使传递到<em>lambda</em>的实参是一个右值,<em>lambda</em>传递进<code>normalize</code>的总是一个左值(形参<code>x</code>)。</p>
|
||||
<p>实现这个<em>lambda</em>的正确方式是把<code>x</code>完美转发给函数<code>normalize</code>。这样做需要对代码做两处修改。首先,<code>x</code>需要改成通用引用(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>),其次,需要使用<code>std::forward</code>将<code>x</code>转发到函数<code>normalize</code>(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>)。理论上,这都是小改动:</p>
|
||||
<p>实现这个<em>lambda</em>的正确方式是把<code>x</code>完美转发给函数<code>normalize</code>。这样做需要对代码做两处修改。首先,<code>x</code>需要改成通用引用(见<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>),其次,需要使用<code>std::forward</code>将<code>x</code>转发到函数<code>normalize</code>(见<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>)。理论上,这都是小改动:</p>
|
||||
<pre><code class="language-c++">auto f = [](auto&& x)
|
||||
{ return func(normalize(std::forward<???>(x))); };
|
||||
</code></pre>
|
||||
<p>在理论和实际之间存在一个问题:你应该传递给<code>std::forward</code>的什么类型,即确定我在上面写的<code>???</code>该是什么。</p>
|
||||
<p>一般来说,当你在使用完美转发时,你是在一个接受类型参数为<code>T</code>的模版函数里,所以你可以写<code>std::forward<T></code>。但在泛型<em>lambda</em>中,没有可用的类型参数<code>T</code>。在<em>lambda</em>生成的闭包里,模版化的<code>operator()</code>函数中的确有一个<code>T</code>,但在<em>lambda</em>里却无法直接使用它,所以也没什么用。</p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>解释过如果一个左值实参被传给通用引用的形参,那么形参类型会变成左值引用。传递的是右值,形参就会变成右值引用。这意味着在这个<em>lambda</em>中,可以通过检查形参<code>x</code>的类型来确定传递进来的实参是一个左值还是右值,<code>decltype</code>就可以实现这样的效果(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md">Item3</a>)。传递给<em>lambda</em>的是一个左值,<code>decltype(x)</code>就能产生一个左值引用;如果传递的是一个右值,<code>decltype(x)</code>就会产生右值引用。</p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>也解释过在调用<code>std::forward</code>时,惯例决定了类型实参是左值引用时来表明要传进左值,类型实参是非引用就表明要传进右值。在前面的<em>lambda</em>中,如果<code>x</code>绑定的是一个左值,<code>decltype(x)</code>就能产生一个左值引用。这符合惯例。然而如果<code>x</code>绑定的是一个右值,<code>decltype(x)</code>就会产生右值引用,而不是常规的非引用。</p>
|
||||
<p>再看一下<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md">Item28</a>中关于<code>std::forward</code>的C++14实现:</p>
|
||||
<p><a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>解释过如果一个左值实参被传给通用引用的形参,那么形参类型会变成左值引用。传递的是右值,形参就会变成右值引用。这意味着在这个<em>lambda</em>中,可以通过检查形参<code>x</code>的类型来确定传递进来的实参是一个左值还是右值,<code>decltype</code>就可以实现这样的效果(见<a href="../1.DeducingTypes/item3.html">Item3</a>)。传递给<em>lambda</em>的是一个左值,<code>decltype(x)</code>就能产生一个左值引用;如果传递的是一个右值,<code>decltype(x)</code>就会产生右值引用。</p>
|
||||
<p><a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>也解释过在调用<code>std::forward</code>时,惯例决定了类型实参是左值引用时来表明要传进左值,类型实参是非引用就表明要传进右值。在前面的<em>lambda</em>中,如果<code>x</code>绑定的是一个左值,<code>decltype(x)</code>就能产生一个左值引用。这符合惯例。然而如果<code>x</code>绑定的是一个右值,<code>decltype(x)</code>就会产生右值引用,而不是常规的非引用。</p>
|
||||
<p>再看一下<a href="../5.RRefMovSemPerfForw/item28.html">Item28</a>中关于<code>std::forward</code>的C++14实现:</p>
|
||||
<pre><code class="language-c++">template<typename T> //在std命名空间
|
||||
T&& forward(remove_reference_t<T>& param)
|
||||
{
|
||||
|
@ -143,7 +143,7 @@
|
||||
<p><strong>Item 34: Prefer lambdas to <code>std::bind</code></strong></p>
|
||||
<p>C++11中的<code>std::bind</code>是C++98的<code>std::bind1st</code>和<code>std::bind2nd</code>的后续,但在2005年已经非正式成为了标准库的一部分。那时标准化委员采用了TR1的文档,其中包含了<code>bind</code>的规范。(在TR1中,<code>bind</code>位于不同的命名空间,因此它是<code>std::tr1::bind</code>,而不是<code>std::bind</code>,接口细节也有所不同)。这段历史意味着一些程序员有十年及以上的<code>std::bind</code>使用经验。如果你是其中之一,可能会不愿意放弃一个对你有用的工具。这是可以理解的,但是在这种情况下,改变是更好的,因为在C++11中,<em>lambda</em>几乎总是比<code>std::bind</code>更好的选择。 从C++14开始,<em>lambda</em>的作用不仅强大,而且是完全值得使用的。</p>
|
||||
<p>这个条款假设你熟悉<code>std::bind</code>。 如果不是这样,你将需要获得基本的了解,然后再继续。 无论如何,这样的理解都是值得的,因为你永远不知道何时会在阅读或维护的代码库中遇到<code>std::bind</code>。</p>
|
||||
<p>与<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item32.md">Item32</a>中一样,我们将从<code>std::bind</code>返回的函数对象称为<strong>bind对象</strong>(<em>bind objects</em>)。</p>
|
||||
<p>与<a href="../6.LambdaExpressions/item32.html">Item32</a>中一样,我们将从<code>std::bind</code>返回的函数对象称为<strong>bind对象</strong>(<em>bind objects</em>)。</p>
|
||||
<p>优先<em>lambda</em>而不是<code>std::bind</code>的最重要原因是<em>lambda</em>更易读。 例如,假设我们有一个设置警报器的函数:</p>
|
||||
<pre><code class="language-c++">//一个时间点的类型定义(语法见条款9)
|
||||
using Time = std::chrono::steady_clock::time_point;
|
||||
@ -310,8 +310,8 @@ auto compressRateB = std::bind(compress, w, _1);
|
||||
<p>同样,唯一的方法是记住<code>std::bind</code>的工作方式。(答案是传递给bind对象的所有实参都是通过引用传递的,因为此类对象的函数调用运算符使用完美转发。)</p>
|
||||
<p>与<em>lambda</em>相比,使用<code>std::bind</code>进行编码的代码可读性较低,表达能力较低,并且效率可能较低。 在C++14中,没有<code>std::bind</code>的合理用例。 但是,在C++11中,可以在两个受约束的情况下证明使用<code>std::bind</code>是合理的:</p>
|
||||
<ul>
|
||||
<li><strong>移动捕获</strong>。C++11的<em>lambda</em>不提供移动捕获,但是可以通过结合<em>lambda</em>和<code>std::bind</code>来模拟。 有关详细信息,请参阅<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item32.md">Item32</a>,该条款还解释了在C++14中,<em>lambda</em>对初始化捕获的支持消除了这个模拟的需求。</li>
|
||||
<li><strong>多态函数对象</strong>。因为bind对象上的函数调用运算符使用完美转发,所以它可以接受任何类型的实参(以<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>中描述的完美转发的限制为界限)。当你要绑定带有模板化函数调用运算符的对象时,此功能很有用。 例如这个类,</li>
|
||||
<li><strong>移动捕获</strong>。C++11的<em>lambda</em>不提供移动捕获,但是可以通过结合<em>lambda</em>和<code>std::bind</code>来模拟。 有关详细信息,请参阅<a href="../6.LambdaExpressions/item32.html">Item32</a>,该条款还解释了在C++14中,<em>lambda</em>对初始化捕获的支持消除了这个模拟的需求。</li>
|
||||
<li><strong>多态函数对象</strong>。因为bind对象上的函数调用运算符使用完美转发,所以它可以接受任何类型的实参(以<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>中描述的完美转发的限制为界限)。当你要绑定带有模板化函数调用运算符的对象时,此功能很有用。 例如这个类,</li>
|
||||
</ul>
|
||||
<pre><code class="language-c++">class PolyWidget {
|
||||
public:
|
||||
|
@ -172,9 +172,9 @@ std::thread t(doAsyncWork);
|
||||
<p>如果你把这些问题推给另一个人做,你就会变得很轻松,而使用<code>std::async</code>就做了这件事:</p>
|
||||
<pre><code class="language-cpp">auto fut = std::async(doAsyncWork); //线程管理责任交给了标准库的开发者
|
||||
</code></pre>
|
||||
<p>这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额异常的可能性,因为这个调用可能不会开启一个新的线程。你会想:“怎么可能?如果我要求比系统可以提供的更多的软件线程,创建<code>std::thread</code>和调用<code>std::async</code>为什么会有区别?”确实有区别,因为以这种形式调用(即使用默认启动策略——见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item36.md">Item36</a>)时,<code>std::async</code>不保证会创建新的软件线程。然而,他们允许通过调度器来将特定函数(本例中为<code>doAsyncWork</code>)运行在等待此函数结果的线程上(即在对<code>fut</code>调用<code>get</code>或者<code>wait</code>的线程上),合理的调度器在系统资源超额或者线程耗尽时就会利用这个自由度。</p>
|
||||
<p>这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额异常的可能性,因为这个调用可能不会开启一个新的线程。你会想:“怎么可能?如果我要求比系统可以提供的更多的软件线程,创建<code>std::thread</code>和调用<code>std::async</code>为什么会有区别?”确实有区别,因为以这种形式调用(即使用默认启动策略——见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>)时,<code>std::async</code>不保证会创建新的软件线程。然而,他们允许通过调度器来将特定函数(本例中为<code>doAsyncWork</code>)运行在等待此函数结果的线程上(即在对<code>fut</code>调用<code>get</code>或者<code>wait</code>的线程上),合理的调度器在系统资源超额或者线程耗尽时就会利用这个自由度。</p>
|
||||
<p>如果考虑自己实现“在等待结果的线程上运行输出结果的函数”,之前提到了可能引出负载不均衡的问题,这问题不那么容易解决,因为应该是<code>std::async</code>和运行时的调度程序来解决这个问题而不是你。遇到负载不均衡问题时,对机器内发生的事情,运行时调度程序比你有更全面的了解,因为它管理的是所有执行过程,而不仅仅个别开发者运行的代码。</p>
|
||||
<p>有了<code>std::async</code>,GUI线程中响应变慢仍然是个问题,因为调度器并不知道你的哪个线程有高响应要求。这种情况下,你会想通过向<code>std::async</code>传递<code>std::launch::async</code>启动策略来保证想运行函数在不同的线程上执行(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item36.md">Item36</a>)。</p>
|
||||
<p>有了<code>std::async</code>,GUI线程中响应变慢仍然是个问题,因为调度器并不知道你的哪个线程有高响应要求。这种情况下,你会想通过向<code>std::async</code>传递<code>std::launch::async</code>启动策略来保证想运行函数在不同的线程上执行(见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>)。</p>
|
||||
<p>最前沿的线程调度器使用系统级线程池(<em>thread pool</em>)来避免资源超额的问题,并且通过工作窃取算法(<em>work-stealing algorithm</em>)来提升了跨硬件核心的负载均衡。C++标准实际上并不要求使用线程池或者工作窃取,实际上C++11并发规范的某些技术层面使得实现这些技术的难度可能比想象中更有挑战。不过,库开发者在标准库实现中采用了这些技术,也有理由期待这个领域会有更多进展。如果你当前的并发编程采用基于任务的方式,在这些技术发展中你会持续获得回报。相反如果你直接使用<code>std::thread</code>编程,处理线程耗尽、资源超额、负责均衡问题的责任就压在了你身上,更不用说你对这些问题的解决方法与同机器上其他程序采用的解决方案配合得好不好了。</p>
|
||||
<p>对比基于线程的编程方式,基于任务的设计为开发者避免了手动线程管理的痛苦,并且自然提供了一种获取异步执行程序的结果(即返回值或者异常)的方式。当然,仍然存在一些场景直接使用<code>std::thread</code>会更有优势:</p>
|
||||
<ul>
|
||||
|
@ -141,10 +141,10 @@
|
||||
<main>
|
||||
<h2 id="条款三十六如果有异步的必要请指定stdlaunchasync"><a class="header" href="#条款三十六如果有异步的必要请指定stdlaunchasync">条款三十六:如果有异步的必要请指定<code>std::launch::async</code></a></h2>
|
||||
<p><strong>Item 36: Specify <code>std::launch::async</code> if asynchronicity is essential.</strong></p>
|
||||
<p>当你调用<code>std::async</code>执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你要求<code>std::async</code>执行的操作。你事实上要求这个函数按照<code>std::async</code>启动策略来执行。有两种标准策略,每种都通过<code>std::launch</code>这个限域<code>enum</code>的一个枚举名表示(关于枚举的更多细节参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item10.md">Item10</a>)。假定一个函数<code>f</code>传给<code>std::async</code>来执行:</p>
|
||||
<p>当你调用<code>std::async</code>执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你要求<code>std::async</code>执行的操作。你事实上要求这个函数按照<code>std::async</code>启动策略来执行。有两种标准策略,每种都通过<code>std::launch</code>这个限域<code>enum</code>的一个枚举名表示(关于枚举的更多细节参见<a href="../3.MovingToModernCpp/item10.html">Item10</a>)。假定一个函数<code>f</code>传给<code>std::async</code>来执行:</p>
|
||||
<ul>
|
||||
<li><strong><code>std::launch::async</code>启动策略</strong>意味着<code>f</code>必须异步执行,即在不同的线程。</li>
|
||||
<li><strong><code>std::launch::deferred</code>启动策略</strong>意味着<code>f</code>仅当在<code>std::async</code>返回的<em>future</em>上调用<code>get</code>或者<code>wait</code>时才执行。这表示<code>f</code><strong>推迟</strong>到存在这样的调用时才执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当<code>get</code>或<code>wait</code>被调用,<code>f</code>会同步执行,即调用方被阻塞,直到<code>f</code>运行结束。如果<code>get</code>和<code>wait</code>都没有被调用,<code>f</code>将不会被执行。(这是个简化说法。关键点不是要在其上调用<code>get</code>或<code>wait</code>的那个<em>future</em>,而是<em>future</em>引用的那个共享状态。(<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md">Item38</a>讨论了<em>future</em>与共享状态的关系。)因为<code>std::future</code>支持移动,也可以用来构造<code>std::shared_future</code>,并且因为<code>std::shared_future</code>可以被拷贝,对共享状态——对<code>f</code>传到的那个<code>std::async</code>进行调用产生的——进行引用的<em>future</em>对象,有可能与<code>std::async</code>返回的那个<em>future</em>对象不同。这非常绕口,所以经常回避这个事实,简称为在<code>std::async</code>返回的<em>future</em>上调用<code>get</code>或<code>wait</code>。)</li>
|
||||
<li><strong><code>std::launch::deferred</code>启动策略</strong>意味着<code>f</code>仅当在<code>std::async</code>返回的<em>future</em>上调用<code>get</code>或者<code>wait</code>时才执行。这表示<code>f</code><strong>推迟</strong>到存在这样的调用时才执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当<code>get</code>或<code>wait</code>被调用,<code>f</code>会同步执行,即调用方被阻塞,直到<code>f</code>运行结束。如果<code>get</code>和<code>wait</code>都没有被调用,<code>f</code>将不会被执行。(这是个简化说法。关键点不是要在其上调用<code>get</code>或<code>wait</code>的那个<em>future</em>,而是<em>future</em>引用的那个共享状态。(<a href="../7.TheConcurrencyAPI/item38.html">Item38</a>讨论了<em>future</em>与共享状态的关系。)因为<code>std::future</code>支持移动,也可以用来构造<code>std::shared_future</code>,并且因为<code>std::shared_future</code>可以被拷贝,对共享状态——对<code>f</code>传到的那个<code>std::async</code>进行调用产生的——进行引用的<em>future</em>对象,有可能与<code>std::async</code>返回的那个<em>future</em>对象不同。这非常绕口,所以经常回避这个事实,简称为在<code>std::async</code>返回的<em>future</em>上调用<code>get</code>或<code>wait</code>。)</li>
|
||||
</ul>
|
||||
<p>可能让人惊奇的是,<code>std::async</code>的默认启动策略——你不显式指定一个策略时它使用的那个——不是上面中任意一个。相反,是求或在一起的。下面的两种调用含义相同:</p>
|
||||
<pre><code class="language-cpp">auto fut1 = std::async(f); //使用默认启动策略运行f
|
||||
@ -152,7 +152,7 @@ auto fut2 = std::async(std::launch::async | //使用async或者deferred运
|
||||
std::launch::deferred,
|
||||
f);
|
||||
</code></pre>
|
||||
<p>因此默认策略允许<code>f</code>异步或者同步执行。如同<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md">Item35</a>中指出,这种灵活性允许<code>std::async</code>和标准库的线程管理组件承担线程创建和销毁的责任,避免资源超额,以及平衡负载。这就是使用<code>std::async</code>并发编程如此方便的原因。</p>
|
||||
<p>因此默认策略允许<code>f</code>异步或者同步执行。如同<a href="../7.TheConcurrencyAPI/Item35.html">Item35</a>中指出,这种灵活性允许<code>std::async</code>和标准库的线程管理组件承担线程创建和销毁的责任,避免资源超额,以及平衡负载。这就是使用<code>std::async</code>并发编程如此方便的原因。</p>
|
||||
<p>但是,使用默认启动策略的<code>std::async</code>也有一些有趣的影响。给定一个线程<code>t</code>执行此语句:</p>
|
||||
<pre><code class="language-cpp">auto fut = std::async(f); //使用默认启动策略运行f
|
||||
</code></pre>
|
||||
@ -165,7 +165,7 @@ auto fut2 = std::async(std::launch::async | //使用async或者deferred运
|
||||
<pre><code class="language-cpp">auto fut = std::async(f); //f的TLS可能是为单独的线程建的,
|
||||
//也可能是为在fut上调用get或者wait的线程建的
|
||||
</code></pre>
|
||||
<p>这还会影响到基于<code>wait</code>的循环使用超时机制,因为在一个延时的任务(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md">Item35</a>)上调用<code>wait_for</code>或者<code>wait_until</code>会产生<code>std::launch::deferred</code>值。意味着,以下循环看似应该最终会终止,但可能实际上永远运行:</p>
|
||||
<p>这还会影响到基于<code>wait</code>的循环使用超时机制,因为在一个延时的任务(参见<a href="../7.TheConcurrencyAPI/Item35.html">Item35</a>)上调用<code>wait_for</code>或者<code>wait_until</code>会产生<code>std::launch::deferred</code>值。意味着,以下循环看似应该最终会终止,但可能实际上永远运行:</p>
|
||||
<pre><code class="language-cpp">using namespace std::literals; //为了使用C++14中的时间段后缀;参见条款34
|
||||
|
||||
void f() //f休眠1秒,然后返回
|
||||
@ -220,7 +220,7 @@ reallyAsync(F&& f, Ts&&... params) //返回异步调用
|
||||
std::forward<Ts>(params)...);
|
||||
}
|
||||
</code></pre>
|
||||
<p>这个函数接受一个可调用对象<code>f</code>和0或多个形参<code>params</code>,然后完美转发(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>)给<code>std::async</code>,使用<code>std::launch::async</code>作为启动策略。就像<code>std::async</code>一样,返回<code>std::future</code>作为用<code>params</code>调用<code>f</code>得到的结果。确定结果的类型很容易,因为<em>type trait</em> <code>std::result_of</code>可以提供给你。(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md">Item9</a>关于<em>type trait</em>的详细表述。)</p>
|
||||
<p>这个函数接受一个可调用对象<code>f</code>和0或多个形参<code>params</code>,然后完美转发(参见<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>)给<code>std::async</code>,使用<code>std::launch::async</code>作为启动策略。就像<code>std::async</code>一样,返回<code>std::future</code>作为用<code>params</code>调用<code>f</code>得到的结果。确定结果的类型很容易,因为<em>type trait</em> <code>std::result_of</code>可以提供给你。(参见<a href="../3.MovingToModernCpp/item9.html">Item9</a>关于<em>type trait</em>的详细表述。)</p>
|
||||
<p><code>reallyAsync</code>就像<code>std::async</code>一样使用:</p>
|
||||
<pre><code class="language-cpp">auto fut = reallyAsync(f); //异步运行f,如果std::async抛出异常它也会抛出
|
||||
</code></pre>
|
||||
|
@ -151,7 +151,7 @@
|
||||
</ul>
|
||||
<p>(译者注:<code>std::thread</code>可以视作状态保存的对象,保存的状态可能也包括可调用对象,有没有具体的线程承载就是有没有连接)</p>
|
||||
<p><code>std::thread</code>的可结合性如此重要的原因之一就是当可结合的线程的析构函数被调用,程序执行会终止。比如,假定有一个函数<code>doWork</code>,使用一个过滤函数<code>filter</code>,一个最大值<code>maxVal</code>作为形参。<code>doWork</code>检查是否满足计算所需的条件,然后使用在0到<code>maxVal</code>之间的通过过滤器的所有值进行计算。如果进行过滤非常耗时,并且确定<code>doWork</code>条件是否满足也很耗时,则将两件事并发计算是很合理的。</p>
|
||||
<p>我们希望为此采用基于任务的设计(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md">Item35</a>),但是假设我们希望设置做过滤的线程的优先级。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md">Item35</a>阐释了那需要线程的原生句柄,只能通过<code>std::thread</code>的API来完成;基于任务的API(比如<em>future</em>)做不到。所以最终采用基于线程而不是基于任务。</p>
|
||||
<p>我们希望为此采用基于任务的设计(参见<a href="../7.TheConcurrencyAPI/Item35.html">Item35</a>),但是假设我们希望设置做过滤的线程的优先级。<a href="../7.TheConcurrencyAPI/Item35.html">Item35</a>阐释了那需要线程的原生句柄,只能通过<code>std::thread</code>的API来完成;基于任务的API(比如<em>future</em>)做不到。所以最终采用基于线程而不是基于任务。</p>
|
||||
<p>我们可能写出以下代码:</p>
|
||||
<p>代码如下:</p>
|
||||
<pre><code class="language-cpp">constexpr auto tenMillion = 10000000; //constexpr见条款15
|
||||
@ -181,7 +181,7 @@ bool doWork(std::function<bool(int)> filter, //返回计算是否执行
|
||||
<p>在解释这份代码为什么有问题之前,我先把<code>tenMillion</code>的初始化值弄得更可读一些,这利用了C++14的能力,使用单引号作为数字分隔符:</p>
|
||||
<pre><code class="language-cpp">constexpr auto tenMillion = 10'000'000; //C++14
|
||||
</code></pre>
|
||||
<p>还要指出,在开始运行之后设置<code>t</code>的优先级就像把马放出去之后再关上马厩门一样(译者注:太晚了)。更好的设计是在挂起状态时开始<code>t</code>(这样可以在执行任何计算前调整优先级),但是我不想你为考虑那些代码而分心。如果你对代码中忽略的部分感兴趣,可以转到<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md">Item39</a>,那个Item告诉你如何以开始那些挂起状态的线程。</p>
|
||||
<p>还要指出,在开始运行之后设置<code>t</code>的优先级就像把马放出去之后再关上马厩门一样(译者注:太晚了)。更好的设计是在挂起状态时开始<code>t</code>(这样可以在执行任何计算前调整优先级),但是我不想你为考虑那些代码而分心。如果你对代码中忽略的部分感兴趣,可以转到<a href="../7.TheConcurrencyAPI/item39.html">Item39</a>,那个Item告诉你如何以开始那些挂起状态的线程。</p>
|
||||
<p>返回<code>doWork</code>。如果<code>conditionsAreSatisfied()</code>返回<code>true</code>,没什么问题,但是如果返回<code>false</code>或者抛出异常,在<code>doWork</code>结束调用<code>t</code>的析构函数时,<code>std::thread</code>对象<code>t</code>会是可结合的。这造成程序执行中止。</p>
|
||||
<p>你可能会想,为什么<code>std::thread</code>析构的行为是这样的,那是因为另外两种显而易见的方式更糟:</p>
|
||||
<ul>
|
||||
@ -195,7 +195,7 @@ bool doWork(std::function<bool(int)> filter, //返回计算是否执行
|
||||
</ul>
|
||||
<p>标准委员会认为,销毁可结合的线程如此可怕以至于实际上禁止了它(规定销毁可结合的线程导致程序终止)。</p>
|
||||
<p>这使你有责任确保使用<code>std::thread</code>对象时,在所有的路径上超出定义所在的作用域时都是不可结合的。但是覆盖每条路径可能很复杂,可能包括自然执行通过作用域,或者通过<code>return</code>,<code>continue</code>,<code>break</code>,<code>goto</code>或异常跳出作用域,有太多可能的路径。</p>
|
||||
<p>每当你想在执行跳至块之外的每条路径执行某种操作,最通用的方式就是将该操作放入局部对象的析构函数中。这些对象称为<strong>RAII对象</strong>(<em>RAII objects</em>),从<strong>RAII类</strong>中实例化。(RAII全称为 “Resource Acquisition Is Initialization”(资源获得即初始化),尽管技术关键点在析构上而不是实例化上)。RAII类在标准库中很常见。比如STL容器(每个容器析构函数都销毁容器中的内容物并释放内存),标准智能指针(<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>-<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md">20</a>解释了,<code>std::uniqu_ptr</code>的析构函数调用他指向的对象的删除器,<code>std::shared_ptr</code>和<code>std::weak_ptr</code>的析构函数递减引用计数),<code>std::fstream</code>对象(它们的析构函数关闭对应的文件)等。但是标准库没有<code>std::thread</code>的RAII类,可能是因为标准委员会拒绝将<code>join</code>和<code>detach</code>作为默认选项,不知道应该怎么样完成RAII。</p>
|
||||
<p>每当你想在执行跳至块之外的每条路径执行某种操作,最通用的方式就是将该操作放入局部对象的析构函数中。这些对象称为<strong>RAII对象</strong>(<em>RAII objects</em>),从<strong>RAII类</strong>中实例化。(RAII全称为 “Resource Acquisition Is Initialization”(资源获得即初始化),尽管技术关键点在析构上而不是实例化上)。RAII类在标准库中很常见。比如STL容器(每个容器析构函数都销毁容器中的内容物并释放内存),标准智能指针(<a href="../4.SmartPointers/item18.html">Item18</a>-<a href="../4.SmartPointers/item20.html">20</a>解释了,<code>std::uniqu_ptr</code>的析构函数调用他指向的对象的删除器,<code>std::shared_ptr</code>和<code>std::weak_ptr</code>的析构函数递减引用计数),<code>std::fstream</code>对象(它们的析构函数关闭对应的文件)等。但是标准库没有<code>std::thread</code>的RAII类,可能是因为标准委员会拒绝将<code>join</code>和<code>detach</code>作为默认选项,不知道应该怎么样完成RAII。</p>
|
||||
<p>幸运的是,完成自行实现的类并不难。比如,下面的类实现允许调用者指定<code>ThreadRAII</code>对象(一个<code>std::thread</code>的RAII对象)析构时,调用<code>join</code>或者<code>detach</code>:</p>
|
||||
<pre><code class="language-cpp">class ThreadRAII {
|
||||
public:
|
||||
@ -275,8 +275,8 @@ private:
|
||||
}
|
||||
</code></pre>
|
||||
<p>这种情况下,我们选择在<code>ThreadRAII</code>的析构函数对异步执行的线程进行<code>join</code>,因为在先前分析中,<code>detach</code>可能导致噩梦般的调试过程。我们之前也分析了<code>join</code>可能会导致表现异常(坦率说,也可能调试困难),但是在未定义行为(<code>detach</code>导致),程序终止(使用原生<code>std::thread</code>导致),或者表现异常之间选择一个后果,可能表现异常是最好的那个。</p>
|
||||
<p>哎,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md">Item39</a>表明了使用<code>ThreadRAII</code>来保证在<code>std::thread</code>的析构时执行<code>join</code>有时不仅可能导致程序表现异常,还可能导致程序挂起。“适当”的解决方案是此类程序应该和异步执行的<em>lambda</em>通信,告诉它不需要执行了,可以直接返回,但是C++11中不支持<strong>可中断线程</strong>(<em>interruptible threads</em>)。可以自行实现,但是这不是本书讨论的主题。(关于这一点,Anthony Williams的《C++ Concurrency in Action》(Manning Publications,2012)的section 9.2中有详细讨论。)(译者注:此书中文版已出版,名为《C++并发编程实战》,且本文翻译时(2020)已有第二版出版。)</p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>说明因为<code>ThreadRAII</code>声明了一个析构函数,因此不会有编译器生成移动操作,但是没有理由<code>ThreadRAII</code>对象不能移动。如果要求编译器生成这些函数,函数的功能也正确,所以显式声明来告诉编译器自动生成也是合适的:</p>
|
||||
<p>哎,<a href="../7.TheConcurrencyAPI/item39.html">Item39</a>表明了使用<code>ThreadRAII</code>来保证在<code>std::thread</code>的析构时执行<code>join</code>有时不仅可能导致程序表现异常,还可能导致程序挂起。“适当”的解决方案是此类程序应该和异步执行的<em>lambda</em>通信,告诉它不需要执行了,可以直接返回,但是C++11中不支持<strong>可中断线程</strong>(<em>interruptible threads</em>)。可以自行实现,但是这不是本书讨论的主题。(关于这一点,Anthony Williams的《C++ Concurrency in Action》(Manning Publications,2012)的section 9.2中有详细讨论。)(译者注:此书中文版已出版,名为《C++并发编程实战》,且本文翻译时(2020)已有第二版出版。)</p>
|
||||
<p><a href="../3.MovingToModernCpp/item17.html">Item17</a>说明因为<code>ThreadRAII</code>声明了一个析构函数,因此不会有编译器生成移动操作,但是没有理由<code>ThreadRAII</code>对象不能移动。如果要求编译器生成这些函数,函数的功能也正确,所以显式声明来告诉编译器自动生成也是合适的:</p>
|
||||
<pre><code class="language-cpp">class ThreadRAII {
|
||||
public:
|
||||
enum class DtorAction { join, detach }; //跟之前一样
|
||||
|
@ -141,9 +141,9 @@
|
||||
<main>
|
||||
<h2 id="条款三十八关注不同线程句柄的析构行为"><a class="header" href="#条款三十八关注不同线程句柄的析构行为">条款三十八:关注不同线程句柄的析构行为</a></h2>
|
||||
<p><strong>Item 38:Be aware of varying thread handle destructor behavior</strong></p>
|
||||
<p><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item37.md">Item37</a>中说明了可结合的<code>std::thread</code>对应于执行的系统线程。未延迟(non-deferred)任务的<em>future</em>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item36.md">Item36</a>)与系统线程有相似的关系。因此,可以将<code>std::thread</code>对象和<em>future</em>对象都视作系统线程的<strong>句柄</strong>(<em>handles</em>)。</p>
|
||||
<p>从这个角度来说,有趣的是<code>std::thread</code>和<em>future</em>在析构时有相当不同的行为。在<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item37.md">Item37</a>中说明,可结合的<code>std::thread</code>析构会终止你的程序,因为两个其他的替代选择——隐式<code>join</code>或者隐式<code>detach</code>都是更加糟糕的。但是,<em>future</em>的析构表现有时就像执行了隐式<code>join</code>,有时又像是隐式执行了<code>detach</code>,有时又没有执行这两个选择。它永远不会造成程序终止。这个线程句柄多种表现值得研究一下。</p>
|
||||
<p>我们可以观察到实际上<em>future</em>是通信信道的一端,被调用者通过该信道将结果发送给调用者。(<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md">Item39</a>说,与<em>future</em>有关的这种通信信道也可以被用于其他目的。但是对于本条款,我们只考虑它们作为这样一个机制的用法,即被调用者传送结果给调用者。)被调用者(通常是异步执行)将计算结果写入通信信道中(通常通过<code>std::promise</code>对象),调用者使用<em>future</em>读取结果。你可以想象成下面的图示,虚线表示信息的流动方向:</p>
|
||||
<p><a href="../7.TheConcurrencyAPI/item37.html">Item37</a>中说明了可结合的<code>std::thread</code>对应于执行的系统线程。未延迟(non-deferred)任务的<em>future</em>(参见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>)与系统线程有相似的关系。因此,可以将<code>std::thread</code>对象和<em>future</em>对象都视作系统线程的<strong>句柄</strong>(<em>handles</em>)。</p>
|
||||
<p>从这个角度来说,有趣的是<code>std::thread</code>和<em>future</em>在析构时有相当不同的行为。在<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>中说明,可结合的<code>std::thread</code>析构会终止你的程序,因为两个其他的替代选择——隐式<code>join</code>或者隐式<code>detach</code>都是更加糟糕的。但是,<em>future</em>的析构表现有时就像执行了隐式<code>join</code>,有时又像是隐式执行了<code>detach</code>,有时又没有执行这两个选择。它永远不会造成程序终止。这个线程句柄多种表现值得研究一下。</p>
|
||||
<p>我们可以观察到实际上<em>future</em>是通信信道的一端,被调用者通过该信道将结果发送给调用者。(<a href="../7.TheConcurrencyAPI/item39.html">Item39</a>说,与<em>future</em>有关的这种通信信道也可以被用于其他目的。但是对于本条款,我们只考虑它们作为这样一个机制的用法,即被调用者传送结果给调用者。)被调用者(通常是异步执行)将计算结果写入通信信道中(通常通过<code>std::promise</code>对象),调用者使用<em>future</em>读取结果。你可以想象成下面的图示,虚线表示信息的流动方向:</p>
|
||||
<p><img src="media/item38_fig1.png" alt="item38_fig1" /></p>
|
||||
<p>但是被调用者的结果存储在哪里?被调用者会在调用者<code>get</code>相关的<em>future</em>之前执行完成,所以结果不能存储在被调用者的<code>std::promise</code>。这个对象是局部的,当被调用者执行结束后,会被销毁。</p>
|
||||
<p>结果同样不能存储在调用者的<em>future</em>,因为(当然还有其他原因)<code>std::future</code>可能会被用来创建<code>std::shared_future</code>(这会将被调用者的结果所有权从<code>std::future</code>转移给<code>std::shared_future</code>),而<code>std::shared_future</code>在<code>std::future</code>被销毁之后可能被复制很多次。鉴于不是所有的结果都可以被拷贝(即只可移动类型),并且结果的生命周期至少与最后一个引用它的<em>future</em>一样长,这些潜在的<em>future</em>中哪个才是被调用者用来存储结果的?</p>
|
||||
@ -155,16 +155,16 @@
|
||||
<li><strong>引用了共享状态——使用<code>std::async</code>启动的未延迟任务建立的那个——的最后一个<em>future</em>的析构函数会阻塞住</strong>,直到任务完成。本质上,这种<em>future</em>的析构函数对执行异步任务的线程执行了隐式的<code>join</code>。</li>
|
||||
<li><strong>其他所有<em>future</em>的析构函数简单地销毁<em>future</em>对象</strong>。对于异步执行的任务,就像对底层的线程执行<code>detach</code>。对于延迟任务来说如果这是最后一个<em>future</em>,意味着这个延迟任务永远不会执行了。</li>
|
||||
</ul>
|
||||
<p>这些规则听起来好复杂。我们真正要处理的是一个简单的“正常”行为以及一个单独的例外。正常行为是<em>future</em>析构函数销毁<em>future</em>。就是这样。那意味着不<code>join</code>也不<code>detach</code>,也不运行什么,只销毁<em>future</em>的数据成员(当然,还做了另一件事,就是递减了共享状态中的引用计数,这个共享状态是由引用它的<em>future</em>和被调用者的<code>std::promise</code>共同控制的。这个引用计数让库知道共享状态什么时候可以被销毁。对于引用计数的一般信息参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item19</a>。)</p>
|
||||
<p>这些规则听起来好复杂。我们真正要处理的是一个简单的“正常”行为以及一个单独的例外。正常行为是<em>future</em>析构函数销毁<em>future</em>。就是这样。那意味着不<code>join</code>也不<code>detach</code>,也不运行什么,只销毁<em>future</em>的数据成员(当然,还做了另一件事,就是递减了共享状态中的引用计数,这个共享状态是由引用它的<em>future</em>和被调用者的<code>std::promise</code>共同控制的。这个引用计数让库知道共享状态什么时候可以被销毁。对于引用计数的一般信息参见<a href="../4.SmartPointers/item19.html">Item19</a>。)</p>
|
||||
<p>正常行为的例外情况仅在某个<code>future</code>同时满足下列所有情况下才会出现:</p>
|
||||
<ul>
|
||||
<li><strong>它关联到由于调用<code>std::async</code>而创建出的共享状态</strong>。</li>
|
||||
<li><strong>任务的启动策略是<code>std::launch::async</code></strong>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item36.md">Item36</a>),原因是运行时系统选择了该策略,或者在对<code>std::async</code>的调用中指定了该策略。</li>
|
||||
<li><strong>任务的启动策略是<code>std::launch::async</code></strong>(参见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>),原因是运行时系统选择了该策略,或者在对<code>std::async</code>的调用中指定了该策略。</li>
|
||||
<li><strong>这个<em>future</em>是关联共享状态的最后一个<em>future</em></strong>。对于<code>std::future</code>,情况总是如此,对于<code>std::shared_future</code>,如果还有其他的<code>std::shared_future</code>,与要被销毁的<em>future</em>引用相同的共享状态,则要被销毁的<em>future</em>遵循正常行为(即简单地销毁它的数据成员)。</li>
|
||||
</ul>
|
||||
<p>只有当上面的三个条件都满足时,<em>future</em>的析构函数才会表现“异常”行为,就是在异步任务执行完之前阻塞住。实际上,这相当于对由于运行<code>std::async</code>创建出任务的线程隐式<code>join</code>。</p>
|
||||
<p>通常会听到将这种异常的析构函数行为称为“<code>std::async</code>来的<em>futures</em>阻塞了它们的析构函数”。作为近似描述没有问题,但是有时你不只需要一个近似描述。现在你已经知道了其中真相。</p>
|
||||
<p>你可能想要了解更加深入。比如“为什么由<code>std::async</code>启动的未延迟任务的共享状态,会有这么个特殊规则”,这很合理。据我所知,标准委员会希望避免隐式<code>detach</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item37.md">Item37</a>)的有关问题,但是不想采取强制程序终止这种激进的方案(就像对可结合的<code>sth::thread</code>做的那样(译者注:指析构时<code>std::thread</code>若可结合则调用<code>std::terminal</code>终止程序),同样参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item37.md">Item37</a>),所以妥协使用隐式<code>join</code>。这个决定并非没有争议,并且认真讨论过在C++14中放弃这种行为。最后,决定先不改变,所以C++11和C++14中这里的行为是一致的。</p>
|
||||
<p>你可能想要了解更加深入。比如“为什么由<code>std::async</code>启动的未延迟任务的共享状态,会有这么个特殊规则”,这很合理。据我所知,标准委员会希望避免隐式<code>detach</code>(参见<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>)的有关问题,但是不想采取强制程序终止这种激进的方案(就像对可结合的<code>sth::thread</code>做的那样(译者注:指析构时<code>std::thread</code>若可结合则调用<code>std::terminal</code>终止程序),同样参见<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>),所以妥协使用隐式<code>join</code>。这个决定并非没有争议,并且认真讨论过在C++14中放弃这种行为。最后,决定先不改变,所以C++11和C++14中这里的行为是一致的。</p>
|
||||
<p><em>future</em>的API没办法确定是否<em>future</em>引用了一个<code>std::async</code>调用产生的共享状态,因此给定一个任意的<em>future</em>对象,无法判断会不会阻塞析构函数从而等待异步任务的完成。这就产生了有意思的事情:</p>
|
||||
<pre><code class="language-cpp">//这个容器可能在析构函数处阻塞,因为其中至少一个future可能引用由std::async启动的
|
||||
//未延迟任务创建出来的共享状态
|
||||
@ -185,7 +185,7 @@ auto fut = pt.get_future(); //从pt获取future
|
||||
</code></pre>
|
||||
<p>此时,我们知道<em>future</em>没有关联<code>std::async</code>创建的共享状态,所以析构函数肯定正常方式执行。</p>
|
||||
<p>一旦被创建,<code>std::packaged_task</code>类型的<code>pt</code>就可以在一个线程上执行。(也可以通过调用<code>std::async</code>运行,但是如果你想使用<code>std::async</code>运行任务,没有理由使用<code>std::packaged_task</code>,因为在<code>std::packaged_task</code>安排任务并执行之前,<code>std::async</code>会做<code>std::packaged_task</code>做的所有事。)</p>
|
||||
<p><code>std::packaged_task</code>不可拷贝,所以当<code>pt</code>被传递给<code>std::thread</code>构造函数时,必须先转为右值(通过<code>std::move</code>,参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md">Item23</a>):</p>
|
||||
<p><code>std::packaged_task</code>不可拷贝,所以当<code>pt</code>被传递给<code>std::thread</code>构造函数时,必须先转为右值(通过<code>std::move</code>,参见<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a>):</p>
|
||||
<pre><code class="language-cpp">std::thread t(std::move(pt)); //在t上运行pt
|
||||
</code></pre>
|
||||
<p>这个例子是你对于<em>future</em>的析构函数的正常行为有一些了解,但是将这些语句放在一个作用域的语句块里更容易看:</p>
|
||||
@ -201,7 +201,7 @@ auto fut = pt.get_future(); //从pt获取future
|
||||
</code></pre>
|
||||
<p>此处最有趣的代码是在创建<code>std::thread</code>对象<code>t</code>之后,代码块结束前的“<code>…</code>”。使代码有趣的事是,在“<code>…</code>”中<code>t</code>上会发生什么。有三种可能性:</p>
|
||||
<ul>
|
||||
<li><strong>对<code>t</code>什么也不做</strong>。这种情况,<code>t</code>会在语句块结束时是可结合的,这会使得程序终止(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item37.md">Item37</a>)。</li>
|
||||
<li><strong>对<code>t</code>什么也不做</strong>。这种情况,<code>t</code>会在语句块结束时是可结合的,这会使得程序终止(参见<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>)。</li>
|
||||
<li><strong>对<code>t</code>调用<code>join</code></strong>。这种情况,不需要<code>fut</code>在它的析构函数处阻塞,因为<code>join</code>被显式调用了。</li>
|
||||
<li><strong>对<code>t</code>调用<code>detach</code></strong>。这种情况,不需要在<code>fut</code>的析构函数执行<code>detach</code>,因为显式调用了。</li>
|
||||
</ul>
|
||||
|
@ -190,7 +190,7 @@ while (!flag); //等待事件
|
||||
</code></pre>
|
||||
<p>这种方法不存在基于条件变量的设计的缺点。不需要互斥锁,在反应任务开始轮询之前检测任务就对flag置位也不会出现问题,并且不会出现虚假唤醒。好,好,好。</p>
|
||||
<p>不好的一点是反应任务中轮询的开销。在任务等待flag被置位的时间里,任务基本被阻塞了,但是一直在运行。这样,反应线程占用了可能能给另一个任务使用的硬件线程,每次启动或者完成它的时间片都增加了上下文切换的开销,并且保持核心一直在运行状态,否则的话本来可以停下来省电。一个真正阻塞的任务不会发生上面的任何情况。这也是基于条件变量的优点,因为<code>wait</code>调用中的任务真的阻塞住了。</p>
|
||||
<p>将条件变量和flag的设计组合起来很常用。一个flag表示是否发生了感兴趣的事件,但是通过互斥锁同步了对该flag的访问。因为互斥锁阻止并发访问该flag,所以如<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md">Item40</a>所述,不需要将flag设置为<code>std::atomic</code>。一个简单的<code>bool</code>类型就可以,检测任务代码如下:</p>
|
||||
<p>将条件变量和flag的设计组合起来很常用。一个flag表示是否发生了感兴趣的事件,但是通过互斥锁同步了对该flag的访问。因为互斥锁阻止并发访问该flag,所以如<a href="../7.TheConcurrencyAPI/item40.html">Item40</a>所述,不需要将flag设置为<code>std::atomic</code>。一个简单的<code>bool</code>类型就可以,检测任务代码如下:</p>
|
||||
<pre><code class="language-cpp">std::condition_variable cv; //跟之前一样
|
||||
std::mutex m;
|
||||
bool flag(false); //不是std::atomic
|
||||
@ -211,7 +211,7 @@ cv.notify_one(); //通知反应任务(第2部分)
|
||||
… //继续反应动作(m现在解锁)
|
||||
</code></pre>
|
||||
<p>这份代码解决了我们一直讨论的问题。无论在检测线程对条件变量发出通知之前反应线程是否调用了<code>wait</code>都可以工作,即使出现了虚假唤醒也可以工作,而且不需要轮询。但是仍然有些古怪,因为检测任务通过奇怪的方式与反应线程通信。(译者注:下面的话挺绕的,可以参考原文)检测任务通过通知条件变量告诉反应线程,等待的事件可能发生了,但是反应线程必须通过检查flag来确保事件发生了。检测线程置位flag来告诉反应线程事件确实发生了,但是检测线程仍然还要先需要通知条件变量,以唤醒反应线程来检查flag。这种方案是可以工作的,但是不太优雅。</p>
|
||||
<p>一个替代方案是让反应任务通过在检测任务设置的<em>future</em>上<code>wait</code>来避免使用条件变量,互斥锁和flag。这可能听起来也是个古怪的方案。毕竟,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md">Item38</a>中说明了<em>future</em>代表了从被调用方到(通常是异步的)调用方的通信信道的接收端,这里的检测任务和反应任务没有调用-被调用的关系。然而,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md">Item38</a>中也说说明了发送端是个<code>std::promise</code>,接收端是个<em>future</em>的通信信道不是只能用在调用-被调用场景。这样的通信信道可以用在任何你需要从程序一个地方传递信息到另一个地方的场景。这里,我们用来在检测任务和反应任务之间传递信息,传递的信息就是感兴趣的事件已经发生。</p>
|
||||
<p>一个替代方案是让反应任务通过在检测任务设置的<em>future</em>上<code>wait</code>来避免使用条件变量,互斥锁和flag。这可能听起来也是个古怪的方案。毕竟,<a href="../7.TheConcurrencyAPI/item38.html">Item38</a>中说明了<em>future</em>代表了从被调用方到(通常是异步的)调用方的通信信道的接收端,这里的检测任务和反应任务没有调用-被调用的关系。然而,<a href="../7.TheConcurrencyAPI/item38.html">Item38</a>中也说说明了发送端是个<code>std::promise</code>,接收端是个<em>future</em>的通信信道不是只能用在调用-被调用场景。这样的通信信道可以用在任何你需要从程序一个地方传递信息到另一个地方的场景。这里,我们用来在检测任务和反应任务之间传递信息,传递的信息就是感兴趣的事件已经发生。</p>
|
||||
<p>方案很简单。检测任务有一个<code>std::promise</code>对象(即通信信道的写入端),反应任务有对应的<em>future</em>。当检测任务看到事件已经发生,设置<code>std::promise</code>对象(即写入到通信信道)。同时,<code>wait</code>会阻塞住反应任务直到<code>std::promise</code>被设置。</p>
|
||||
<p>现在,<code>std::promise</code>和<em>futures</em>(即<code>std::future</code>和<code>std::shared_future</code>)都是需要类型参数的模板。形参表明通过通信信道被传递的信息的类型。在这里,没有数据被传递,只需要让反应任务知道它的<em>future</em>已经被设置了。我们在<code>std::promise</code>和<em>future</em>模板中需要的东西是表明通信信道中没有数据被传递的一个类型。这个类型就是<code>void</code>。检测任务使用<code>std::promise<void></code>,反应任务使用<code>std::future<void></code>或者<code>std::shared_future<void></code>。当感兴趣的事件发生时,检测任务设置<code>std::promise<void></code>,反应任务在<em>future</em>上<code>wait</code>。尽管反应任务不从检测任务那里接收任何数据,通信信道也可以让反应任务知道,检测任务什么时候已经通过对<code>std::promise<void></code>调用<code>set_value</code>“写入”了<code>void</code>数据。</p>
|
||||
<p>所以,有</p>
|
||||
@ -227,7 +227,7 @@ p.get_future().wait(); //等待对应于p的那个future
|
||||
… //对事件作出反应
|
||||
</code></pre>
|
||||
<p>像使用flag的方法一样,此设计不需要互斥锁,无论在反应线程调用<code>wait</code>之前检测线程是否设置了<code>std::promise</code>都可以工作,并且不受虚假唤醒的影响(只有条件变量才容易受到此影响)。与基于条件变量的方法一样,反应任务在调用<code>wait</code>之后是真被阻塞住的,不会一直占用系统资源。是不是很完美?</p>
|
||||
<p>当然不是,基于<em>future</em>的方法没有了上述问题,但是有其他新的问题。比如,<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md">Item38</a>中说明,<code>std::promise</code>和<em>future</em>之间有个共享状态,并且共享状态是动态分配的。因此你应该假定此设计会产生基于堆的分配和释放开销。</p>
|
||||
<p>当然不是,基于<em>future</em>的方法没有了上述问题,但是有其他新的问题。比如,<a href="../7.TheConcurrencyAPI/item38.html">Item38</a>中说明,<code>std::promise</code>和<em>future</em>之间有个共享状态,并且共享状态是动态分配的。因此你应该假定此设计会产生基于堆的分配和释放开销。</p>
|
||||
<p>也许更重要的是,<code>std::promise</code>只能设置一次。<code>std::promise</code>和<em>future</em>之间的通信是<strong>一次性</strong>的:不能重复使用。这是与基于条件变量或者基于flag的设计的明显差异,条件变量和flag都可以通信多次。(条件变量可以被重复通知,flag也可以重复清除和设置。)</p>
|
||||
<p>一次通信可能没有你想象中那么大的限制。假定你想创建一个挂起的系统线程。就是,你想避免多次线程创建的那种经常开销,以便想要使用这个线程执行程序时,避免潜在的线程创建工作。或者你想创建一个挂起的线程,以便在线程运行前对其进行设置这样的设置包括优先级或者核心亲和性(<em>core affinity</em>)。C++并发API没有提供这种设置能力,但是<code>std::thread</code>提供了<code>native_handle</code>成员函数,它的结果就是提供给你对平台原始线程API的访问(通常是POSIX或者Windows的线程)。这些低层次的API使你可以对线程设置优先级和亲和性。</p>
|
||||
<p>假设你仅仅想要对某线程挂起一次(在创建后,运行线程函数前),使用<code>void</code>的<em>future</em>就是一个可行方案。这是这个技术的关键点:</p>
|
||||
@ -246,7 +246,7 @@ void detect() //检测任务的函数
|
||||
t.join(); //使t不可结合(见条款37)
|
||||
}
|
||||
</code></pre>
|
||||
<p>因为所有离开<code>detect</code>的路径中<code>t</code>都要是不可结合的,所以使用类似于<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item37.md">Item37</a>中<code>ThreadRAII</code>的RAII类很明智。代码如下:</p>
|
||||
<p>因为所有离开<code>detect</code>的路径中<code>t</code>都要是不可结合的,所以使用类似于<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>中<code>ThreadRAII</code>的RAII类很明智。代码如下:</p>
|
||||
<pre><code class="language-cpp">void detect()
|
||||
{
|
||||
ThreadRAII tr( //使用RAII对象
|
||||
|
@ -179,7 +179,7 @@ volatile int vc(0); //“volatile计数器”
|
||||
</ol>
|
||||
<p><code>vc</code>的最后结果是1,即使看起来自增了两次。</p>
|
||||
<p>不仅只有这一种可能的结果,通常来说<code>vc</code>的最终结果是不可预测的,因为<code>vc</code>会发生数据竞争,对于数据竞争造成未定义行为,标准规定表示编译器生成的代码可能是任何逻辑。当然,编译器不会利用这种行为来作恶。但是它们通常做出一些没有数据竞争的程序中才有效的优化,这些优化在存在数据竞争的程序中会造成异常和不可预测的行为。</p>
|
||||
<p>RMW操作不是仅有的<code>std::atomic</code>在并发中有效而<code>volatile</code>无效的例子。假定一个任务计算第二个任务需要的一个重要的值。当第一个任务完成计算,必须传递给第二个任务。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md">Item39</a>表明一种使用<code>std::atomic<bool></code>的方法来使第一个任务通知第二个任务计算完成。计算值的任务的代码如下:</p>
|
||||
<p>RMW操作不是仅有的<code>std::atomic</code>在并发中有效而<code>volatile</code>无效的例子。假定一个任务计算第二个任务需要的一个重要的值。当第一个任务完成计算,必须传递给第二个任务。<a href="../7.TheConcurrencyAPI/item39.html">Item39</a>表明一种使用<code>std::atomic<bool></code>的方法来使第一个任务通知第二个任务计算完成。计算值的任务的代码如下:</p>
|
||||
<pre><code class="language-cpp">std::atomic<bool> valVailable(false);
|
||||
auto imptValue = computeImportantValue(); //计算值
|
||||
valAvailable = true; //告诉另一个任务,值可用了
|
||||
@ -249,7 +249,7 @@ x = 10; //写x(不会被优化掉)
|
||||
x = 20; //再次写x
|
||||
</code></pre>
|
||||
<p>如果<code>x</code>是内存映射的(或者已经映射到跨进程共享的内存位置等),这正是我们想要的。</p>
|
||||
<p>突击测试!在最后一段代码中,<code>y</code>是什么类型:<code>int</code>还是<code>volatile int</code>?(<code>y</code>的类型使用<code>auto</code>类型推导,所以使用<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>中的规则。规则上说非引用非指针类型的声明(就是<code>y</code>的情况),<code>const</code>和<code>volatile</code>限定符被拿掉。<code>y</code>的类型因此仅仅是<code>int</code>。这意味着对<code>y</code>的冗余读取和写入可以被消除。在例子中,编译器必须执行对<code>y</code>的初始化和赋值两个语句,因为<code>x</code>是<code>volatile</code>的,所以第二次对<code>x</code>的读取可能会产生一个与第一次不同的值。)</p>
|
||||
<p>突击测试!在最后一段代码中,<code>y</code>是什么类型:<code>int</code>还是<code>volatile int</code>?(<code>y</code>的类型使用<code>auto</code>类型推导,所以使用<a href="../1.DeducingTypes/item2.html">Item2</a>中的规则。规则上说非引用非指针类型的声明(就是<code>y</code>的情况),<code>const</code>和<code>volatile</code>限定符被拿掉。<code>y</code>的类型因此仅仅是<code>int</code>。这意味着对<code>y</code>的冗余读取和写入可以被消除。在例子中,编译器必须执行对<code>y</code>的初始化和赋值两个语句,因为<code>x</code>是<code>volatile</code>的,所以第二次对<code>x</code>的读取可能会产生一个与第一次不同的值。)</p>
|
||||
<p>在处理特殊内存时,必须保留看似冗余访问和无用存储的事实,顺便说明了为什么<code>std::atomic</code>不适合这种场景。编译器被允许消除对<code>std::atomic</code>的冗余操作。代码的编写方式与<code>volatile</code>那些不那么相同,但是如果我们暂时忽略它,只关注编译器执行的操作,则概念上可以说,编译器看到这个,</p>
|
||||
<pre><code class="language-cpp">std::atomic<int> x;
|
||||
auto y = x; //概念上会读x(见下)
|
||||
@ -267,7 +267,7 @@ x = 20; //写x
|
||||
<pre><code class="language-cpp">auto y = x; //错误
|
||||
y = x; //错误
|
||||
</code></pre>
|
||||
<p>这是因为<code>std::atomic</code>类型的拷贝操作是被删除的(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item11.md">Item11</a>)。因为有个很好的理由删除。想象一下如果<code>y</code>使用<code>x</code>来初始化会发生什么。因为<code>x</code>是<code>std::atomic</code>类型,<code>y</code>的类型被推导为<code>std::atomic</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md">Item2</a>)。我之前说了<code>std::atomic</code>最好的特性之一就是所有成员函数都是原子性的,但是为了使从<code>x</code>拷贝初始化<code>y</code>的过程是原子性的,编译器不得不生成代码,把读取<code>x</code>和写入<code>y</code>放在一个单独的原子性操作中。硬件通常无法做到这一点,因此<code>std::atomic</code>不支持拷贝构造。出于同样的原因,拷贝赋值也被删除了,这也是为什么从<code>x</code>赋值给<code>y</code>也编译失败。(移动操作在<code>std::atomic</code>没有显式声明,因此根据<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md">Item17</a>中描述的规则来看,<code>std::atomic</code>不支持移动构造和移动赋值)。</p>
|
||||
<p>这是因为<code>std::atomic</code>类型的拷贝操作是被删除的(参见<a href="../3.MovingToModernCpp/item11.html">Item11</a>)。因为有个很好的理由删除。想象一下如果<code>y</code>使用<code>x</code>来初始化会发生什么。因为<code>x</code>是<code>std::atomic</code>类型,<code>y</code>的类型被推导为<code>std::atomic</code>(参见<a href="../1.DeducingTypes/item2.html">Item2</a>)。我之前说了<code>std::atomic</code>最好的特性之一就是所有成员函数都是原子性的,但是为了使从<code>x</code>拷贝初始化<code>y</code>的过程是原子性的,编译器不得不生成代码,把读取<code>x</code>和写入<code>y</code>放在一个单独的原子性操作中。硬件通常无法做到这一点,因此<code>std::atomic</code>不支持拷贝构造。出于同样的原因,拷贝赋值也被删除了,这也是为什么从<code>x</code>赋值给<code>y</code>也编译失败。(移动操作在<code>std::atomic</code>没有显式声明,因此根据<a href="../3.MovingToModernCpp/item17.html">Item17</a>中描述的规则来看,<code>std::atomic</code>不支持移动构造和移动赋值)。</p>
|
||||
<p>可以将<code>x</code>的值传递给<code>y</code>,但是需要使用<code>std::atomic</code>的<code>load</code>和<code>store</code>成员函数。<code>load</code>函数原子性地读取,<code>store</code>原子性地写入。要使用<code>x</code>初始化<code>y</code>,然后将<code>x</code>的值放入<code>y</code>,代码应该这样写:</p>
|
||||
<pre><code class="language-cpp">std::atomic<int> y(x.load()); //读x
|
||||
y.store(x.load()); //再次读x
|
||||
|
@ -144,7 +144,7 @@
|
||||
<p>对于C++中的通用技术和特性,总是存在适用和不适用的场景。除了本章覆盖的两个例外,描述什么场景使用哪种通用技术通常来说很容易。这两个例外是传值(pass by value)和安置(emplacement)。决定何时使用这两种技术受到多种因素的影响,本书提供的最佳建议是在使用它们的同时仔细考虑清楚,尽管它们都是高效的现代C++编程的重要角色。接下来的条款提供了使用它们来编写软件是否合适的所需信息。</p>
|
||||
<h2 id="条款四十一对于移动成本低且总是被拷贝的可拷贝形参考虑按值传递"><a class="header" href="#条款四十一对于移动成本低且总是被拷贝的可拷贝形参考虑按值传递">条款四十一:对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递</a></h2>
|
||||
<p><strong>Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied</strong></p>
|
||||
<p>有些函数的形参是可拷贝的。(在本条款中,“拷贝”一个形参通常意思是,将形参作为拷贝或移动操作的源对象。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/Introduction.md">简介</a>中提到,C++没有术语来区分拷贝操作得到的副本和移动操作得到的副本。)比如说,<code>addName</code>成员函数可以拷贝自己的形参到一个私有容器。为了提高效率,应该拷贝左值,移动右值。</p>
|
||||
<p>有些函数的形参是可拷贝的。(在本条款中,“拷贝”一个形参通常意思是,将形参作为拷贝或移动操作的源对象。<a href="../Introduction.html">简介</a>中提到,C++没有术语来区分拷贝操作得到的副本和移动操作得到的副本。)比如说,<code>addName</code>成员函数可以拷贝自己的形参到一个私有容器。为了提高效率,应该拷贝左值,移动右值。</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
public:
|
||||
void addName(const std::string& newName) //接受左值;拷贝它
|
||||
@ -159,7 +159,7 @@ private:
|
||||
</code></pre>
|
||||
<p>这是可行的,但是需要编写两个函数来做本质相同的事。这有点让人难受:两个函数声明,两个函数实现,两个函数写说明,两个函数的维护。唉。</p>
|
||||
<p>此外,目标代码中会有两个函数——你可能会担心程序的空间占用。这种情况下,两个函数都可能内联,可能会避免同时两个函数同时存在导致的代码膨胀问题,但是一旦没有被内联,目标代码就会出现两个函数。</p>
|
||||
<p>另一种方法是使<code>addName</code>函数成为具有通用引用的函数模板(参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md">Item24</a>):</p>
|
||||
<p>另一种方法是使<code>addName</code>函数成为具有通用引用的函数模板(参考<a href="../5.RRefMovSemPerfForw/item24.html">Item24</a>):</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
public:
|
||||
template<typename T> //接受左值和右值;
|
||||
@ -169,7 +169,7 @@ public:
|
||||
…
|
||||
};
|
||||
</code></pre>
|
||||
<p>这减少了源代码的维护工作,但是通用引用会导致其他复杂性。作为模板,<code>addName</code>的实现必须放置在头文件中。在编译器展开的时候,可能会产生多个函数,因为不止为左值和右值实例化,也可能为<code>std::string</code>和可转换为<code>std::string</code>的类型分别实例化为多个函数(参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>)。同时有些实参类型不能通过通用引用传递(参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>),而且如果传递了不合法的实参类型,编译器错误会令人生畏(参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item27.md">Item27</a>)。</p>
|
||||
<p>这减少了源代码的维护工作,但是通用引用会导致其他复杂性。作为模板,<code>addName</code>的实现必须放置在头文件中。在编译器展开的时候,可能会产生多个函数,因为不止为左值和右值实例化,也可能为<code>std::string</code>和可转换为<code>std::string</code>的类型分别实例化为多个函数(参考<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>)。同时有些实参类型不能通过通用引用传递(参考<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>),而且如果传递了不合法的实参类型,编译器错误会令人生畏(参考<a href="../5.RRefMovSemPerfForw/item27.html">Item27</a>)。</p>
|
||||
<p>是否存在一种编写<code>addName</code>的方法,可以左值拷贝,右值移动,只用处理一个函数(源代码和目标代码中),且避免使用通用引用?答案是是的。你要做的就是放弃你学习C++编程的第一条规则。这条规则是避免在传递用户定义的对象时使用传值方式。像是<code>addName</code>函数中的<code>newName</code>形参,按值传递可能是一种完全合理的策略。</p>
|
||||
<p>在我们讨论为什么对于<code>addName</code>中的<code>newName</code>按值传递非常合理之前,让我们来考虑该会怎样实现:</p>
|
||||
<pre><code class="language-cpp">class Widget {
|
||||
@ -344,7 +344,7 @@ private:
|
||||
<p>这种情况下,按值传递的开销包括了内存分配和内存销毁——可能会比<code>std::string</code>的移动操作高出几个数量级。</p>
|
||||
<p>有趣的是,如果旧密码短于新密码,在赋值过程中就不可避免要销毁、分配内存,这种情况,按值传递跟按引用传递的效率是一样的。因此,基于赋值的形参拷贝操作开销取决于具体的实参的值,这种分析适用于在动态分配内存中存值的形参类型。不是所有类型都满足,但是很多——包括<code>std::string</code>和<code>std::vector</code>——是这样。</p>
|
||||
<p>这种潜在的开销增加仅在传递左值实参时才适用,因为执行内存分配和释放通常发生在真正的拷贝操作(即,不是移动)中。对右值实参,移动几乎就足够了。</p>
|
||||
<p>结论是,使用通过赋值拷贝一个形参进行按值传递的函数的额外开销,取决于传递的类型,左值和右值的比例,这个类型是否需要动态分配内存,以及,如果需要分配内存的话,赋值操作符的具体实现,还有赋值目标占的内存至少要跟赋值源占的内存一样大。对于<code>std::string</code>来说,开销还取决于实现是否使用了小字符串优化(SSO——参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item29.md">Item29</a>),如果是,那么要赋值的值是否匹配SSO缓冲区。</p>
|
||||
<p>结论是,使用通过赋值拷贝一个形参进行按值传递的函数的额外开销,取决于传递的类型,左值和右值的比例,这个类型是否需要动态分配内存,以及,如果需要分配内存的话,赋值操作符的具体实现,还有赋值目标占的内存至少要跟赋值源占的内存一样大。对于<code>std::string</code>来说,开销还取决于实现是否使用了小字符串优化(SSO——参考<a href="../5.RRefMovSemPerfForw/item29.html">Item29</a>),如果是,那么要赋值的值是否匹配SSO缓冲区。</p>
|
||||
<p>所以,正如我所说,当形参通过赋值进行拷贝时,分析按值传递的开销是复杂的。通常,最有效的经验就是“在证明没问题之前假设有问题”,就是除非已证明按值传递会为你需要的形参类型产生可接受的执行效率,否则使用重载或者通用引用的实现方式。</p>
|
||||
<p>到此为止,对于需要运行尽可能快的软件来说,按值传递可能不是一个好策略,因为避免即使开销很小的移动操作也非常重要。此外,有时并不能清楚知道会发生多少次移动操作。在<code>Widget::addName</code>例子中,按值传递仅多了一次移动操作,但是如果<code>Widget::addName</code>调用了<code>Widget::validateName</code>,这个函数也是按值传递。(假定它有理由总是拷贝它的形参,比如把验证过的所有值存在一个数据结构中。)并假设<code>validateName</code>调用了第三个函数,也是按值传递……</p>
|
||||
<p>可以看到这将会通向何方。在调用链中,每个函数都使用传值,因为“只多了一次移动的开销”,但是整个调用链总体就会产生无法忍受的开销,通过引用传递,调用链不会增加这种开销。</p>
|
||||
|
@ -168,7 +168,7 @@ public:
|
||||
<p>为了在<code>std::string</code>容器中创建新元素,调用了<code>std::string</code>的构造函数,但是这份代码并不仅调用了一次构造函数,而是调用了两次,而且还调用了<code>std::string</code>析构函数。下面是在<code>push_back</code>运行时发生了什么:</p>
|
||||
<ol>
|
||||
<li>一个<code>std::string</code>的临时对象从字面量“<code>xyzzy</code>”被创建。这个对象没有名字,我们可以称为<code>temp</code>。<code>temp</code>的构造是第一次<code>std::string</code>构造。因为是临时变量,所以<code>temp</code>是右值。</li>
|
||||
<li><code>temp</code>被传递给<code>push_back</code>的右值重载函数,绑定到右值引用形参<code>x</code>。在<code>std::vector</code>的内存中一个<code>x</code>的副本被创建。这次构造——也是第二次构造——在<code>std::vector</code>内部真正创建一个对象。(将<code>x</code>副本拷贝到<code>std::vector</code>内部的构造函数是移动构造函数,因为<code>x</code>在它被拷贝前被转换为一个右值,成为右值引用。有关将右值引用形参强制转换为右值的信息,请参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md">Item25</a>)。</li>
|
||||
<li><code>temp</code>被传递给<code>push_back</code>的右值重载函数,绑定到右值引用形参<code>x</code>。在<code>std::vector</code>的内存中一个<code>x</code>的副本被创建。这次构造——也是第二次构造——在<code>std::vector</code>内部真正创建一个对象。(将<code>x</code>副本拷贝到<code>std::vector</code>内部的构造函数是移动构造函数,因为<code>x</code>在它被拷贝前被转换为一个右值,成为右值引用。有关将右值引用形参强制转换为右值的信息,请参见<a href="../5.RRefMovSemPerfForw/item25.html">Item25</a>)。</li>
|
||||
<li>在<code>push_back</code>返回之后,<code>temp</code>立刻被销毁,调用了一次<code>std::string</code>的析构函数。</li>
|
||||
</ol>
|
||||
<p>对于性能执着的人不禁注意到是否存在一种方法可以获取字符串字面量并将其直接传入到步骤2里在<code>std::vector</code>内构造<code>std::string</code>的代码中,可以避免临时对象<code>temp</code>的创建与销毁。这样的效率最好,对于性能执着的人也不会有什么意见了。</p>
|
||||
@ -176,7 +176,7 @@ public:
|
||||
<p><code>emplace_back</code>就是像我们想要的那样做的:使用传递给它的任何实参直接在<code>std::vector</code>内部构造一个<code>std::string</code>。没有临时变量会生成:</p>
|
||||
<pre><code class="language-cpp">vs.emplace_back("xyzzy"); //直接用“xyzzy”在vs内构造std::string
|
||||
</code></pre>
|
||||
<p><code>emplace_back</code>使用完美转发,因此只要你没有遇到完美转发的限制(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md">Item30</a>),就可以传递任何实参以及组合到<code>emplace_back</code>。比如,如果你想通过接受一个字符和一个数量的<code>std::string</code>构造函数,在<code>vs</code>中创建一个<code>std::string</code>,代码如下:</p>
|
||||
<p><code>emplace_back</code>使用完美转发,因此只要你没有遇到完美转发的限制(参见<a href="../5.RRefMovSemPerfForw/item30.html">Item30</a>),就可以传递任何实参以及组合到<code>emplace_back</code>。比如,如果你想通过接受一个字符和一个数量的<code>std::string</code>构造函数,在<code>vs</code>中创建一个<code>std::string</code>,代码如下:</p>
|
||||
<pre><code class="language-cpp">vs.emplace_back(50, 'x'); //插入由50个“x”组成的一个std::string
|
||||
</code></pre>
|
||||
<p><code>emplace_back</code>可以用于每个支持<code>push_back</code>的标准容器。类似的,每个支持<code>push_front</code>的标准容器都支持<code>emplace_front</code>。每个支持<code>insert</code>(除了<code>std::forward_list</code>和<code>std::array</code>)的标准容器支持<code>emplace</code>。关联容器提供<code>emplace_hint</code>来补充接受“hint”迭代器的<code>insert</code>函数,<code>std::forward_list</code>有<code>emplace_after</code>来匹配<code>insert_after</code>。</p>
|
||||
@ -217,7 +217,7 @@ vs.emplace_back(50, 'x'); //同上
|
||||
<p>在决定是否使用置入函数时,需要注意另外两个问题。首先是资源管理。假定你有一个盛放<code>std::shared_ptr<Widget></code>s的容器,</p>
|
||||
<pre><code class="language-cpp">std::list<std::shared_ptr<Widget>> ptrs;
|
||||
</code></pre>
|
||||
<p>然后你想添加一个通过自定义删除器释放的<code>std::shared_ptr</code>(参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">Item19</a>)。<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>说明你应该使用<code>std::make_shared</code>来创建<code>std::shared_ptr</code>,但是它也承认有时你无法做到这一点。比如当你要指定一个自定义删除器时。这时,你必须直接<code>new</code>一个原始指针,然后通过<code>std::shared_ptr</code>来管理。</p>
|
||||
<p>然后你想添加一个通过自定义删除器释放的<code>std::shared_ptr</code>(参见<a href="../4.SmartPointers/item19.html">Item19</a>)。<a href="../4.SmartPointers/item21.html">Item21</a>说明你应该使用<code>std::make_shared</code>来创建<code>std::shared_ptr</code>,但是它也承认有时你无法做到这一点。比如当你要指定一个自定义删除器时。这时,你必须直接<code>new</code>一个原始指针,然后通过<code>std::shared_ptr</code>来管理。</p>
|
||||
<p>如果自定义删除器是这个函数,</p>
|
||||
<pre><code class="language-cpp">void killWidget(Widget* pWidget);
|
||||
</code></pre>
|
||||
@ -244,7 +244,7 @@ vs.emplace_back(50, 'x'); //同上
|
||||
</ol>
|
||||
<p>在这个场景中,生命周期不良好,这个失误不能赖<code>std::shared_ptr</code>。使用带自定义删除器的<code>std::unique_ptr</code>也会有同样的问题。根本上讲,像<code>std::shared_ptr</code>和<code>std::unique_ptr</code>这样的资源管理类的高效性是以资源(比如从<code>new</code>来的原始指针)被<strong>立即</strong>传递给资源管理对象的构造函数为条件的。实际上,<code>std::make_shared</code>和<code>std::make_unique</code>这样的函数自动做了这些事,是使它们如此重要的原因。</p>
|
||||
<p>在对存储资源管理类对象的容器(比如<code>std::list<std::shared_ptr<Widget>></code>)调用插入函数时,函数的形参类型通常确保在资源的获取(比如使用<code>new</code>)和资源管理对象的创建之间没有其他操作。在置入函数中,完美转发推迟了资源管理对象的创建,直到可以在容器的内存中构造它们为止,这给“异常导致资源泄漏”提供了可能。所有的标准库容器都容易受到这个问题的影响。在使用资源管理对象的容器时,必须注意确保在使用置入函数而不是插入函数时,不会为提高效率带来的降低异常安全性付出代价。</p>
|
||||
<p>坦白说,无论如何,你不应该将“<code>new Widget</code>”之类的表达式传递给<code>emplace_back</code>或者<code>push_back</code>或者大多数这种函数,因为,就像<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>中解释的那样,这可能导致我们刚刚讨论的异常安全性问题。消除资源泄漏可能性的方法是,使用独立语句把从“<code>new Widget</code>”获取的指针传递给资源管理类对象,然后这个对象作为右值传递给你本来想传递“<code>new Widget</code>”的函数(<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md">Item21</a>有这个观点的详细讨论)。使用<code>push_back</code>的代码应该如下:</p>
|
||||
<p>坦白说,无论如何,你不应该将“<code>new Widget</code>”之类的表达式传递给<code>emplace_back</code>或者<code>push_back</code>或者大多数这种函数,因为,就像<a href="../4.SmartPointers/item21.html">Item21</a>中解释的那样,这可能导致我们刚刚讨论的异常安全性问题。消除资源泄漏可能性的方法是,使用独立语句把从“<code>new Widget</code>”获取的指针传递给资源管理类对象,然后这个对象作为右值传递给你本来想传递“<code>new Widget</code>”的函数(<a href="../4.SmartPointers/item21.html">Item21</a>有这个观点的详细讨论)。使用<code>push_back</code>的代码应该如下:</p>
|
||||
<pre><code class="language-cpp">std::shared_ptr<Widget> spw(new Widget, //创建Widget,让spw管理它
|
||||
killWidget);
|
||||
ptrs.push_back(std::move(spw)); //添加spw右值
|
||||
|
380
print.html
380
print.html
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user