mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2025-03-29 05:20:14 +08:00
Merge pull request #78 from neko-horikawaraiko/neko-horikawaraiko-patch-1
fix item23-42
This commit is contained in:
commit
369666334a
@ -24,17 +24,17 @@ void f(ParamType param);
|
||||
````
|
||||
它的调用看起来像这样
|
||||
````cpp
|
||||
f(expr); //使用表达式调用f
|
||||
f(expr); //使用表达式调用f
|
||||
````
|
||||
在编译期间,编译器使用`expr`进行两个类型推导:一个是针对`T`的,另一个是针对`ParamType`的。这两个类型通常是不同的,因为`ParamType`包含一些修饰,比如`const`和引用修饰符。举个例子,如果模板这样声明:
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(const T& param); //ParamType是const T&
|
||||
void f(const T& param); //ParamType是const T&
|
||||
````
|
||||
然后这样进行调用
|
||||
````cpp
|
||||
int x = 0;
|
||||
f(x); //用一个int类型的变量调用f
|
||||
f(x); //用一个int类型的变量调用f
|
||||
````
|
||||
`T`被推导为`int`,`ParamType`却被推导为`const int&`
|
||||
|
||||
@ -49,7 +49,7 @@ f(x); //用一个int类型的变量调用f
|
||||
template<typename T>
|
||||
void f(ParamType param);
|
||||
|
||||
f(expr); //从expr中推导T和ParamType
|
||||
f(expr); //从expr中推导T和ParamType
|
||||
````
|
||||
|
||||
### 情景一:`ParamType`是一个指针或引用,但不是通用引用
|
||||
@ -62,19 +62,19 @@ f(expr); //从expr中推导T和ParamType
|
||||
举个例子,如果这是我们的模板,
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(T& param); //param是一个引用
|
||||
void f(T& param); //param是一个引用
|
||||
````
|
||||
我们声明这些变量,
|
||||
````cpp
|
||||
int x=27; //x是int
|
||||
const int cx=x; //cx是const int
|
||||
const int& rx=x; //rx是指向作为const int的x的引用
|
||||
int x=27; //x是int
|
||||
const int cx=x; //cx是const int
|
||||
const int& rx=x; //rx是指向作为const int的x的引用
|
||||
````
|
||||
在不同的调用中,对`param`和`T`推导的类型会是这样:
|
||||
````cpp
|
||||
f(x); //T是int,param的类型是int&
|
||||
f(cx); //T是const int,param的类型是const int&
|
||||
f(rx); //T是const int,param的类型是const int&
|
||||
f(x); //T是int,param的类型是int&
|
||||
f(cx); //T是const int,param的类型是const int&
|
||||
f(rx); //T是const int,param的类型是const int&
|
||||
````
|
||||
在第二个和第三个调用中,注意因为`cx`和`rx`被指定为`const`值,所以`T`被推导为`const int`,从而产生了`const int&`的形参类型。这对于调用者来说很重要。当他们传递一个`const`对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-`const`的。这也是为什么将一个`const`对象传递给以`T&`类型为形参的模板安全的:对象的常量性`const`ness会被保留为`T`的一部分。
|
||||
|
||||
@ -86,15 +86,15 @@ f(rx); //T是const int,param的类型是const int&
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void f(const T& param); //param现在是reference-to-const
|
||||
void f(const T& param); //param现在是reference-to-const
|
||||
|
||||
int x = 27; //如之前一样
|
||||
const int cx = x; //如之前一样
|
||||
const int& rx = x; //如之前一样
|
||||
int x = 27; //如之前一样
|
||||
const int cx = x; //如之前一样
|
||||
const int& rx = x; //如之前一样
|
||||
|
||||
f(x); //T是int,param的类型是const int&
|
||||
f(cx); //T是int,param的类型是const int&
|
||||
f(rx); //T是int,param的类型是const int&
|
||||
f(x); //T是int,param的类型是const int&
|
||||
f(cx); //T是int,param的类型是const int&
|
||||
f(rx); //T是int,param的类型是const int&
|
||||
```
|
||||
|
||||
同之前一样,`rx`的reference-ness在类型推导中被忽略了。
|
||||
@ -103,13 +103,13 @@ f(rx); //T是int,param的类型是const int&
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void f(T* param); //param现在是指针
|
||||
void f(T* param); //param现在是指针
|
||||
|
||||
int x = 27; //同之前一样
|
||||
const int *px = &x; //px是指向作为const int的x的指针
|
||||
int x = 27; //同之前一样
|
||||
const int *px = &x; //px是指向作为const int的x的指针
|
||||
|
||||
f(&x); //T是int,param的类型是int*
|
||||
f(px); //T是const int,param的类型是const int*
|
||||
f(&x); //T是int,param的类型是int*
|
||||
f(px); //T是const int,param的类型是const int*
|
||||
```
|
||||
|
||||
到现在为止,你会发现你自己打哈欠犯困,因为C++的类型推导规则对引用和指针形参如此自然,书面形式来看这些非常枯燥。所有事情都那么理所当然!那正是在类型推导系统中你所想要的。
|
||||
@ -124,23 +124,23 @@ f(px); //T是const int,param的类型是const int*
|
||||
举个例子:
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(T&& param); //param现在是一个通用引用类型
|
||||
void f(T&& param); //param现在是一个通用引用类型
|
||||
|
||||
int x=27; //如之前一样
|
||||
const int cx=x; //如之前一样
|
||||
const int & rx=cx; //如之前一样
|
||||
int x=27; //如之前一样
|
||||
const int cx=x; //如之前一样
|
||||
const int & rx=cx; //如之前一样
|
||||
|
||||
f(x); //x是左值,所以T是int&,
|
||||
//param类型也是int&
|
||||
f(x); //x是左值,所以T是int&,
|
||||
//param类型也是int&
|
||||
|
||||
f(cx); //cx是左值,所以T是const int&,
|
||||
//param类型也是const int&
|
||||
f(cx); //cx是左值,所以T是const int&,
|
||||
//param类型也是const int&
|
||||
|
||||
f(rx); //rx是左值,所以T是const int&,
|
||||
//param类型也是const int&
|
||||
f(rx); //rx是左值,所以T是const int&,
|
||||
//param类型也是const int&
|
||||
|
||||
f(27); //27是右值,所以T是int,
|
||||
//param类型就是int&&
|
||||
f(27); //27是右值,所以T是int,
|
||||
//param类型就是int&&
|
||||
````
|
||||
[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)详细解释了为什么这些例子是像这样发生的。这里关键在于通用引用的类型推导规则是不同于普通的左值或者右值引用的。尤其是,当通用引用被使用时,类型推导会区分左值实参和右值实参,但是对非通用引用时不会区分。
|
||||
|
||||
@ -149,7 +149,7 @@ f(27); //27是右值,所以T是int,
|
||||
当`ParamType`既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理:
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(T param); //以传值的方式处理param
|
||||
void f(T param); //以传值的方式处理param
|
||||
````
|
||||
这意味着无论传递什么`param`都会成为它的一份拷贝——一个完整的新对象。事实上`param`成为一个新对象这一行为会影响`T`如何从`expr`中推导出结果。
|
||||
|
||||
@ -158,25 +158,25 @@ void f(T param); //以传值的方式处理param
|
||||
|
||||
因此
|
||||
````cpp
|
||||
int x=27; //如之前一样
|
||||
const int cx=x; //如之前一样
|
||||
const int & rx=cx; //如之前一样
|
||||
int x=27; //如之前一样
|
||||
const int cx=x; //如之前一样
|
||||
const int & rx=cx; //如之前一样
|
||||
|
||||
f(x); //T和param的类型都是int
|
||||
f(cx); //T和param的类型都是int
|
||||
f(rx); //T和param的类型都是int
|
||||
f(x); //T和param的类型都是int
|
||||
f(cx); //T和param的类型都是int
|
||||
f(rx); //T和param的类型都是int
|
||||
````
|
||||
注意即使`cx`和`rx`表示`const`值,`param`也不是`const`。这是有意义的。`param`是一个完全独立于`cx`和`rx`的对象——是`cx`或`rx`的一个拷贝。具有常量性的`cx`和`rx`不可修改并不代表`param`也是一样。这就是为什么`expr`的常量性`const`ness(或易变性`volatile`ness)在推导`param`类型时会被忽略:因为`expr`不可修改并不意味着他的拷贝也不能被修改。
|
||||
|
||||
认识到只有在传值给形参时才会忽略`const`(和`volatile`)这一点很重要,正如我们看到的,对于reference-to-`const`和pointer-to-`const`形参来说,`expr`的常量性`const`ness在推导时会被保留。但是考虑这样的情况,expr是一个`const`指针,指向`const`对象,`expr`通过传值传递给`param`:
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(T param); //仍然以传值的方式处理param
|
||||
void f(T param); //仍然以传值的方式处理param
|
||||
|
||||
const char* const ptr = //ptr是一个常量指针,指向常量对象
|
||||
"Fun with pointers";
|
||||
const char* const ptr = //ptr是一个常量指针,指向常量对象
|
||||
"Fun with pointers";
|
||||
|
||||
f(ptr); //传递const char * const类型的实参
|
||||
f(ptr); //传递const char * const类型的实参
|
||||
````
|
||||
在这里,解引用符号(\*)的右边的`const`表示`ptr`本身是一个`const`:`ptr`不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的`const`表示`ptr`指向一个字符串,这个字符串是`const`,因此字符串不能被修改)。当`ptr`作为实参传给`f`,组成这个指针的每一比特都被拷贝进`param`。像这种情况,`ptr`**自身的值会被传给形参**,根据类型推导的第三条规则,`ptr`自身的常量性`const`ness将会被省略,所以`param`是`const char*`,也就是一个可变指针指向`const`字符串。在类型推导中,这个指针指向的数据的常量性`const`ness将会被保留,但是当拷贝`ptr`来创造一个新指针`param`时,`ptr`自身的常量性`const`ness将会被忽略。
|
||||
|
||||
@ -184,9 +184,9 @@ f(ptr); //传递const char * const类型的实参
|
||||
|
||||
上面的内容几乎覆盖了模板类型推导的大部分内容,但这里还有一些小细节值得注意,比如数组类型不同于指针类型,虽然它们两个有时候是可互换的。关于这个错觉最常见的例子是,在很多上下文中数组会退化为指向它的第一个元素的指针。这样的退化允许像这样的代码可以被编译:
|
||||
````cpp
|
||||
const char name[] = "J. P. Briggs"; //name的类型是const char[13]
|
||||
const char name[] = "J. P. Briggs"; //name的类型是const char[13]
|
||||
|
||||
const char * ptrToName = name; //数组退化为指针
|
||||
const char * ptrToName = name; //数组退化为指针
|
||||
````
|
||||
在这里`const char*`指针`ptrToName`会由`name`初始化,而`name`的类型为`const char[13]`,这两种类型(`const char*`和`const char[13]`)是不一样的,但是由于数组退化为指针的规则,编译器允许这样的代码。
|
||||
|
||||
@ -194,9 +194,9 @@ const char * ptrToName = name; //数组退化为指针
|
||||
但要是一个数组传值给一个模板会怎样?会发生什么?
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(T param); //传值形参的模板
|
||||
void f(T param); //传值形参的模板
|
||||
|
||||
f(name); //T和param会推导成什么类型?
|
||||
f(name); //T和param会推导成什么类型?
|
||||
````
|
||||
我们从一个简单的例子开始,这里有一个函数的形参是数组,是的,这样的语法是合法的,
|
||||
````cpp
|
||||
@ -204,22 +204,22 @@ void myFunc(int param[]);
|
||||
````
|
||||
但是数组声明会被视作指针声明,这意味着`myFunc`的声明和下面声明是等价的:
|
||||
````cpp
|
||||
void myFunc(int* param); //与上面相同的函数
|
||||
void myFunc(int* param); //与上面相同的函数
|
||||
````
|
||||
数组与指针形参这样的等价是C语言的产物,C++又是建立在C语言的基础上,它让人产生了一种数组和指针是等价的的错觉。
|
||||
|
||||
因为数组形参会视作指针形参,所以传值给模板的一个数组类型会被推导为一个指针类型。这意味着在模板函数`f`的调用中,它的类型形参`T`会被推导为`const char*`:
|
||||
````cpp
|
||||
f(name); //name是一个数组,但是T被推导为const char*
|
||||
f(name); //name是一个数组,但是T被推导为const char*
|
||||
````
|
||||
但是现在难题来了,虽然函数不能声明形参为真正的数组,但是**可以**接受指向数组的**引用**!所以我们修改`f`为传引用:
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(T& param); //传引用形参的模板
|
||||
void f(T& param); //传引用形参的模板
|
||||
````
|
||||
我们这样进行调用,
|
||||
````cpp
|
||||
f(name); //传数组给f
|
||||
f(name); //传数组给f
|
||||
````
|
||||
`T`被推导为了真正的数组!这个类型包括了数组的大小,在这个例子中`T`被推导为`const char[13]`,`param`则被推导为`const char (&)[13]`。是的,这种语法看起来简直有毒,但是知道它将会让你在关心这些问题的人的提问中获得大神的称号。
|
||||
|
||||
@ -228,21 +228,21 @@ f(name); //传数组给f
|
||||
//在编译期间返回一个数组大小的常量值(
|
||||
//数组形参没有名字,因为我们只关心数组
|
||||
//的大小)
|
||||
template<typename T, std::size_t N> //关于
|
||||
constexpr std::size_t arraySize(T (&)[N]) noexcept //constexpr
|
||||
{ //和noexcept
|
||||
return N; //的信息
|
||||
} //请看下面
|
||||
template<typename T, std::size_t N> //关于
|
||||
constexpr std::size_t arraySize(T (&)[N]) noexcept //constexpr
|
||||
{ //和noexcept
|
||||
return N; //的信息
|
||||
} //请看下面
|
||||
````
|
||||
在[Item15](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item15.md)提到将一个函数声明为`constexpr`使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组,然后第二个数组可以使用第一个数组的大小作为它的大小,就像这样:
|
||||
````cpp
|
||||
int keyVals[] = {1,3,5,7,9,11,22,25}; //keyVals有七个元素
|
||||
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; //keyVals有七个元素
|
||||
|
||||
int mappedVals[arraySize(keyVals)]; //mappedVals也有七个
|
||||
int mappedVals[arraySize(keyVals)]; //mappedVals也有七个
|
||||
````
|
||||
当然作为一个现代C++程序员,你自然应该想到使用`std::array`而不是内置的数组:
|
||||
````cpp
|
||||
std::array<int, arraySize(keyVals)> mappedVals; //mappedVals的大小为7
|
||||
std::array<int, arraySize(keyVals)> mappedVals; //mappedVals的大小为7
|
||||
````
|
||||
至于`arraySize`被声明为`noexcept`,会使得编译器生成更好的代码,具体的细节请参见[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md)。
|
||||
|
||||
@ -250,19 +250,19 @@ std::array<int, arraySize(keyVals)> mappedVals; //mappedVals的大小为7
|
||||
|
||||
在C++中不止是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组类型推导的全部讨论都可以应用到函数类型推导和退化为函数指针上来。结果是:
|
||||
````cpp
|
||||
void someFunc(int, double); //someFunc是一个函数,
|
||||
//类型是void(int,double)
|
||||
void someFunc(int, double); //someFunc是一个函数,
|
||||
//类型是void(int,double)
|
||||
|
||||
template<typename T>
|
||||
void f1(T param); //传值给f1
|
||||
void f1(T param); //传值给f1
|
||||
|
||||
template<typename T>
|
||||
void f2(T & param); //传引用给f2
|
||||
void f2(T & param); //传引用给f2
|
||||
|
||||
f1(someFunc); //param被推导为指向函数的指针,
|
||||
//类型是void(*)(int, double)
|
||||
f2(someFunc); //param被推导为指向函数的引用,
|
||||
//类型是void(&)(int, double)
|
||||
f1(someFunc); //param被推导为指向函数的指针,
|
||||
//类型是void(*)(int, double)
|
||||
f2(someFunc); //param被推导为指向函数的引用,
|
||||
//类型是void(&)(int, double)
|
||||
````
|
||||
这个实际上没有什么不同,但是如果你知道数组退化为指针,你也会知道函数退化为指针。
|
||||
|
||||
|
@ -14,7 +14,7 @@ void f(ParmaType param);
|
||||
和这个调用来解释:
|
||||
|
||||
```cpp
|
||||
f(expr); //使用一些表达式调用f
|
||||
f(expr); //使用一些表达式调用f
|
||||
```
|
||||
|
||||
在`f`的调用中,编译器使用`expr`推导`T`和`ParamType`的类型。
|
||||
@ -34,23 +34,23 @@ const auto & rx=cx;
|
||||
````
|
||||
类型说明符是`const auto&`。在这里例子中要推导`x`,`rx`和`cx`的类型,编译器的行为看起来就像是认为这里每个声明都有一个模板,然后使用合适的初始化表达式进行调用:
|
||||
````cpp
|
||||
template<typename T> //概念化的模板用来推导x的类型
|
||||
template<typename T> //概念化的模板用来推导x的类型
|
||||
void func_for_x(T param);
|
||||
|
||||
func_for_x(27); //概念化调用:
|
||||
//param的推导类型是x的类型
|
||||
func_for_x(27); //概念化调用:
|
||||
//param的推导类型是x的类型
|
||||
|
||||
template<typename T> //概念化的模板用来推导cx的类型
|
||||
template<typename T> //概念化的模板用来推导cx的类型
|
||||
void func_for_cx(const T param);
|
||||
|
||||
func_for_cx(x); //概念化调用:
|
||||
//param的推导类型是cx的类型
|
||||
func_for_cx(x); //概念化调用:
|
||||
//param的推导类型是cx的类型
|
||||
|
||||
template<typename T> //概念化的模板用来推导rx的类型
|
||||
template<typename T> //概念化的模板用来推导rx的类型
|
||||
void func_for_rx(const T & param);
|
||||
|
||||
func_for_rx(x); //概念化调用:
|
||||
//param的推导类型是rx的类型
|
||||
func_for_rx(x); //概念化调用:
|
||||
//param的推导类型是rx的类型
|
||||
````
|
||||
正如我说的,`auto`类型推导除了一个例外(我们很快就会讨论),其他情况都和模板类型推导一样。
|
||||
|
||||
@ -62,35 +62,35 @@ func_for_rx(x); //概念化调用:
|
||||
|
||||
我们早已看过情景一和情景三的例子:
|
||||
````cpp
|
||||
auto x = 27; //情景三(x既不是指针也不是引用)
|
||||
const auto cx = x; //情景三(cx也一样)
|
||||
const auto & rx=cx; //情景一(rx是非通用引用)
|
||||
auto x = 27; //情景三(x既不是指针也不是引用)
|
||||
const auto cx = x; //情景三(cx也一样)
|
||||
const auto & rx=cx; //情景一(rx是非通用引用)
|
||||
````
|
||||
情景二像你期待的一样运作:
|
||||
|
||||
```cpp
|
||||
auto&& uref1 = x; //x是int左值,
|
||||
//所以uref1类型为int&
|
||||
auto&& uref2 = cx; //cx是const int左值,
|
||||
//所以uref2类型为const int&
|
||||
auto&& uref3 = 27; //27是int右值,
|
||||
//所以uref3类型为int&&
|
||||
auto&& uref1 = x; //x是int左值,
|
||||
//所以uref1类型为int&
|
||||
auto&& uref2 = cx; //cx是const int左值,
|
||||
//所以uref2类型为const int&
|
||||
auto&& uref3 = 27; //27是int右值,
|
||||
//所以uref3类型为int&&
|
||||
```
|
||||
|
||||
[Item1](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md)讨论并总结了对于non-reference类型说明符,数组和函数名如何退化为指针。那些内容也同样适用于`auto`类型推导:
|
||||
|
||||
````cpp
|
||||
const char name[] = //name的类型是const char[13]
|
||||
const char name[] = //name的类型是const char[13]
|
||||
"R. N. Briggs";
|
||||
|
||||
auto arr1 = name; //arr1的类型是const char*
|
||||
auto& arr2 = name; //arr2的类型是const char (&)[13]
|
||||
auto arr1 = name; //arr1的类型是const char*
|
||||
auto& arr2 = name; //arr2的类型是const char (&)[13]
|
||||
|
||||
void someFunc(int, double); //someFunc是一个函数,
|
||||
//类型为void(int, double)
|
||||
void someFunc(int, double); //someFunc是一个函数,
|
||||
//类型为void(int, double)
|
||||
|
||||
auto func1 = someFunc; //func1的类型是void (*)(int, double)
|
||||
auto& func2 = someFunc; //func2的类型是void (&)(int, double)
|
||||
auto func1 = someFunc; //func1的类型是void (*)(int, double)
|
||||
auto& func2 = someFunc; //func2的类型是void (&)(int, double)
|
||||
````
|
||||
就像你看到的那样,`auto`类型推导和模板类型推导几乎一样的工作,它们就像一个硬币的两面。
|
||||
|
||||
@ -117,34 +117,34 @@ auto x4{ 27 };
|
||||
````
|
||||
这些声明都能通过编译,但是他们不像替换之前那样有相同的意义。前面两个语句确实声明了一个类型为`int`值为27的变量,但是后面两个声明了一个存储一个元素27的 `std::initializer_list<int>`类型的变量。
|
||||
````cpp
|
||||
auto x1 = 27; //类型是int,值是27
|
||||
auto x2(27); //同上
|
||||
auto x3 = { 27 }; //类型是std::initializer_list<int>,
|
||||
//值是{ 27 }
|
||||
auto x4{ 27 }; //同上
|
||||
auto x1 = 27; //类型是int,值是27
|
||||
auto x2(27); //同上
|
||||
auto x3 = { 27 }; //类型是std::initializer_list<int>,
|
||||
//值是{ 27 }
|
||||
auto x4{ 27 }; //同上
|
||||
````
|
||||
这就造成了auto类型推导不同于模板类型推导的特殊情况。当用`auto`声明的变量使用花括号进行初始化,auto类型推导推出的类型则为`std::initializer_list`。如果这样的一个类型不能被成功推导(比如花括号里面包含的是不同类型的变量),编译器会拒绝这样的代码:
|
||||
````cpp
|
||||
auto x5 = { 1, 2, 3.0 }; //错误!无法推导std::initializer_list<T>中的T
|
||||
auto x5 = { 1, 2, 3.0 }; //错误!无法推导std::initializer_list<T>中的T
|
||||
````
|
||||
就像注释说的那样,在这种情况下类型推导将会失败,但是对我们来说认识到这里确实发生了两种类型推导是很重要的。一种是由于`auto`的使用:`x5`的类型不得不被推导。因为`x5`使用花括号的方式进行初始化,`x5`必须被推导为`std::initializer_list`。但是`std::initializer_list`是一个模板。`std::initializer_list<T>`会被某种类型`T`实例化,所以这意味着`T`也会被推导。 推导落入了这里发生的第二种类型推导——模板类型推导的范围。在这个例子中推导之所以失败,是因为在花括号中的值并不是同一种类型。
|
||||
|
||||
对于花括号的处理是`auto`类型推导和模板类型推导唯一不同的地方。当使用`auto`声明的变量使用花括号的语法进行初始化的时候,会推导出`std::initializer_list<T>`的实例化,但是对于模板类型推导这样就行不通:
|
||||
````cpp
|
||||
auto x = { 11, 23, 9 }; //x的类型是std::initializer_list<int>
|
||||
auto x = { 11, 23, 9 }; //x的类型是std::initializer_list<int>
|
||||
|
||||
template<typename T> //带有与x的声明等价的
|
||||
void f(T param); //形参声明的模板
|
||||
template<typename T> //带有与x的声明等价的
|
||||
void f(T param); //形参声明的模板
|
||||
|
||||
f({ 11, 23, 9 }); //错误!不能推导出T
|
||||
f({ 11, 23, 9 }); //错误!不能推导出T
|
||||
````
|
||||
然而如果在模板中指定`T`是`std::initializer_list<T>`而留下未知`T`,模板类型推导就能正常工作:
|
||||
````cpp
|
||||
template<typename T>
|
||||
void f(std::initializer_list<T> initList);
|
||||
|
||||
f({ 11, 23, 9 }); //T被推导为int,initList的类型为
|
||||
//std::initializer_list<int>
|
||||
f({ 11, 23, 9 }); //T被推导为int,initList的类型为
|
||||
//std::initializer_list<int>
|
||||
````
|
||||
因此`auto`类型推导和模板类型推导的真正区别在于,`auto`类型推导假定花括号表示`std::initializer_list`而模板类型推导不会这样(确切的说是不知道怎么办)。
|
||||
|
||||
@ -154,17 +154,17 @@ f({ 11, 23, 9 }); //T被推导为int,initList的类型为
|
||||
````cpp
|
||||
auto createInitList()
|
||||
{
|
||||
return { 1, 2, 3 }; //错误!不能推导{ 1, 2, 3 }的类型
|
||||
return { 1, 2, 3 }; //错误!不能推导{ 1, 2, 3 }的类型
|
||||
}
|
||||
````
|
||||
同样在C++14的lambda函数中这样使用auto也不能通过编译:
|
||||
````cpp
|
||||
std::vector<int> v;
|
||||
...
|
||||
…
|
||||
auto resetV =
|
||||
[&v](const auto& newValue){ v = newValue; }; //C++14
|
||||
...
|
||||
resetV({ 1, 2, 3 }); //错误!不能推导{ 1, 2, 3 }的类型
|
||||
[&v](const auto& newValue){ v = newValue; }; //C++14
|
||||
…
|
||||
resetV({ 1, 2, 3 }); //错误!不能推导{ 1, 2, 3 }的类型
|
||||
````
|
||||
|
||||
**请记住:**
|
||||
|
@ -6,30 +6,30 @@
|
||||
|
||||
我们将从一个简单的情况开始,没有任何令人惊讶的情况。相比模板类型推导和`auto`类型推导(参见[Item1](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md)和[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md)),`decltype`只是简单的返回名字或者表达式的类型:
|
||||
````cpp
|
||||
const int i=0; //decltype(i)是const int
|
||||
const int i = 0; //decltype(i)是const int
|
||||
|
||||
bool f(const Widget& w); //decltype(w)是const Widget&
|
||||
//decltype(f)是bool(const Widget&)
|
||||
bool f(const Widget& w); //decltype(w)是const Widget&
|
||||
//decltype(f)是bool(const Widget&)
|
||||
|
||||
struct Point{
|
||||
int x,y; //decltype(Point::x)是int
|
||||
}; //decltype(Point::y)是int
|
||||
int x,y; //decltype(Point::x)是int
|
||||
}; //decltype(Point::y)是int
|
||||
|
||||
Widget w; //decltype(w)是Widget
|
||||
Widget w; //decltype(w)是Widget
|
||||
|
||||
if (f(w))... //decltype(f(w))是bool
|
||||
if (f(w))… //decltype(f(w))是bool
|
||||
|
||||
template<typename T> //std::vector的简化版本
|
||||
template<typename T> //std::vector的简化版本
|
||||
class vector{
|
||||
public:
|
||||
...
|
||||
T& operator[](std::size_t index);
|
||||
...
|
||||
}
|
||||
…
|
||||
T& operator[](std::size_t index);
|
||||
…
|
||||
};
|
||||
|
||||
vector<int> v; //decltype(v)是vector<int>
|
||||
...
|
||||
if (v[0] == 0)... //decltype(v[0])是int&
|
||||
vector<int> v; //decltype(v)是vector<int>
|
||||
…
|
||||
if (v[0] == 0)… //decltype(v[0])是int&
|
||||
````
|
||||
看见了吧?没有任何奇怪的东西。
|
||||
|
||||
@ -39,9 +39,9 @@ if (v[0] == 0)... //decltype(v[0])是int&
|
||||
|
||||
使用`decltype`使得我们很容易去实现它,这是我们写的第一个版本,使用`decltype`计算返回类型,这个模板需要改良,我们把这个推迟到后面:
|
||||
````cpp
|
||||
template<typename Container, typename Index> //可以工作,
|
||||
auto authAndAccess(Container& c, Index i) //但是需要改良
|
||||
->decltype(c[i])
|
||||
template<typename Container, typename Index> //可以工作,
|
||||
auto authAndAccess(Container& c, Index i) //但是需要改良
|
||||
->decltype(c[i])
|
||||
{
|
||||
authenticateUser();
|
||||
return c[i];
|
||||
@ -54,30 +54,30 @@ auto authAndAccess(Container& c, Index i) //但是需要改良
|
||||
|
||||
C++11允许自动推导单一语句的*lambda*表达式的返回类型, C++14扩展到允许自动推导所有的*lambda*表达式和函数,甚至它们内含多条语句。对于`authAndAccess`来说这意味着在C++14标准下我们可以忽略尾置返回类型,只留下一个`auto`。在这种形式下`auto`不再进行`auto`类型推导,取而代之的是它意味着编译器将会从函数实现中推导出函数的返回类型。
|
||||
````cpp
|
||||
template<typename Container, typename Index> //C++14版本,
|
||||
auto authAndAccess(Container& c, Index i) //不那么正确
|
||||
template<typename Container, typename Index> //C++14版本,
|
||||
auto authAndAccess(Container& c, Index i) //不那么正确
|
||||
{
|
||||
authenticateUser();
|
||||
return c[i]; //从c[i]中推导返回类型
|
||||
return c[i]; //从c[i]中推导返回类型
|
||||
}
|
||||
````
|
||||
[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md)解释了函数返回类型中使用`auto`,编译器实际上是使用的模板类型推导的那套规则。如果那样的话就会这里就会有一些问题。正如我们之前讨论的,`operator[]`对于大多数`T`类型的容器会返回一个`T&`,但是[Item1](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md)解释了在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。基于这样的规则,考虑它会对下面用户的代码有哪些影响:
|
||||
|
||||
````cpp
|
||||
std::deque<int> d;
|
||||
...
|
||||
authAndAccess(d, 5) = 10; //认证用户,返回d[5],
|
||||
//然后把10赋值给它
|
||||
//无法通过编译器!
|
||||
…
|
||||
authAndAccess(d, 5) = 10; //认证用户,返回d[5],
|
||||
//然后把10赋值给它
|
||||
//无法通过编译器!
|
||||
````
|
||||
在这里`d[5]`本该返回一个`int&`,但是模板类型推导会剥去引用的部分,因此产生了`int`返回类型。函数返回的那个`int`是一个右值,上面的代码尝试把10赋值给右值`int`,C++11禁止这样做,所以代码无法编译。
|
||||
|
||||
要想让`authAndAccess`像我们期待的那样工作,我们需要使用`decltype`类型推导来推导它的返回值,即指定`authAndAccess`应该返回一个和`c[i]`表达式类型一样的类型。C++期望在某些情况下当类型被暗示时需要使用`decltype`类型推导的规则,C++14通过使用`decltype(auto)`说明符使得这成为可能。我们第一次看见`decltype(auto)`可能觉得非常的矛盾(到底是`decltype`还是`auto`?),实际上我们可以这样解释它的意义:`auto`说明符表示这个类型将会被推导,`decltype`说明`decltype`的规则将会被用到这个推导过程中。因此我们可以这样写`authAndAccess`:
|
||||
````cpp
|
||||
template<typename Container, typename Index> //C++14版本,
|
||||
decltype(auto) //可以工作,
|
||||
authAndAccess(Container& c, Index i) //但是还需要
|
||||
{ //改良
|
||||
template<typename Container, typename Index> //C++14版本,
|
||||
decltype(auto) //可以工作,
|
||||
authAndAccess(Container& c, Index i) //但是还需要
|
||||
{ //改良
|
||||
authenticateUser();
|
||||
return c[i];
|
||||
}
|
||||
@ -91,10 +91,10 @@ Widget w;
|
||||
|
||||
const Widget& cw = w;
|
||||
|
||||
auto myWidget1 = cw; //auto类型推导
|
||||
//myWidget1的类型为Widget
|
||||
decltype(auto) myWidget2 = cw; //decltype类型推导
|
||||
//myWidget2的类型是const Widget&
|
||||
auto myWidget1 = cw; //auto类型推导
|
||||
//myWidget1的类型为Widget
|
||||
decltype(auto) myWidget2 = cw; //decltype类型推导
|
||||
//myWidget2的类型是const Widget&
|
||||
````
|
||||
|
||||
但是这里有两个问题困惑着你。一个是我之前提到的`authAndAccess`的改良至今都没有描述。让我们现在加上它。
|
||||
@ -115,14 +115,14 @@ auto s = authAndAccess(makeStringDeque(), 5);
|
||||
````
|
||||
要想支持这样使用`authAndAccess`我们就得修改一下当前的声明使得它支持左值和右值。重载是一个不错的选择(一个函数重载声明为左值引用,另一个声明为右值引用),但是我们就不得不维护两个重载函数。另一个方法是使`authAndAccess`的引用可以绑定左值和右值,[Item24]()解释了那正是通用引用能做的,所以我们这里可以使用通用引用进行声明:
|
||||
````cpp
|
||||
template<typename Containter, typename Index> //现在c是通用引用
|
||||
template<typename Containter, typename Index> //现在c是通用引用
|
||||
decltype(auto) authAndAccess(Container&& c, Index i);
|
||||
````
|
||||
在这个模板中,我们不知道我们操纵的容器的类型是什么,那意味着我们同样不知道它使用的索引对象(index objects)的类型,对一个未知类型的对象使用传值通常会造成不必要的拷贝,对程序的性能有极大的影响,还会造成对象切片行为(参见[item41](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md)),以及给同事落下笑柄。但是就容器索引来说,我们遵照标准模板库对于对于索引的处理是有理由的(比如`std::string`,`std::vector`和`std::deque`的`operator[]`),所以我们坚持传值调用。
|
||||
|
||||
然而,我们还需要更新一下模板的实现,让它能听从[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)的告诫应用`std::forward`实现通用引用:
|
||||
````cpp
|
||||
template<typename Container, typename Index> //最终的C++14版本
|
||||
template<typename Container, typename Index> //最终的C++14版本
|
||||
decltype(auto)
|
||||
authAndAccess(Container&& c, Index i)
|
||||
{
|
||||
@ -132,7 +132,7 @@ authAndAccess(Container&& c, Index i)
|
||||
````
|
||||
这样就能对我们的期望交上一份满意的答卷,但是这要求编译器支持C++14。如果你没有这样的编译器,你还需要使用C++11版本的模板,它看起来和C++14版本的极为相似,除了你不得不指定函数返回类型之外:
|
||||
````cpp
|
||||
template<typename Container, typename Index> //最终的C++11版本
|
||||
template<typename Container, typename Index> //最终的C++11版本
|
||||
auto
|
||||
authAndAccess(Container&& c, Index i)
|
||||
->decltype(std::forward<Container>(c)[i])
|
||||
@ -158,15 +158,15 @@ int x = 0;
|
||||
````cpp
|
||||
decltype(auto) f1()
|
||||
{
|
||||
int x = 0;
|
||||
...
|
||||
return x; //decltype(x)是int,所以f1返回int
|
||||
int x = 0;
|
||||
…
|
||||
return x; //decltype(x)是int,所以f1返回int
|
||||
}
|
||||
|
||||
decltype(auto) f2()
|
||||
{
|
||||
int x =0l;
|
||||
return (x); //decltype((x))是int&,所以f2返回int&
|
||||
int x = 0;
|
||||
return (x); //decltype((x))是int&,所以f2返回int&
|
||||
}
|
||||
````
|
||||
注意不仅`f2`的返回类型不同于`f1`,而且它还引用了一个局部变量!这样的代码将会把你送上未定义行为的特快列车,一辆你绝对不想上第二次的车。
|
||||
|
@ -26,20 +26,20 @@ IDE编辑器可以直接显示`x`推导的结果为`int`,`y`推导的结果为
|
||||
举个例子,假如我们想看到之前那段代码中`x`和`y`的类型,我们可以首先声明一个类模板但**不定义**。就像这样:
|
||||
|
||||
````cpp
|
||||
template<typename T> //只对TD进行声明
|
||||
class TD; //TD == "Type Displayer"
|
||||
template<typename T> //只对TD进行声明
|
||||
class TD; //TD == "Type Displayer"
|
||||
````
|
||||
如果尝试实例化这个类模板就会引出一个错误消息,因为这里没有用来实例化的类模板定义。为了查看`x`和`y`的类型,只需要使用它们的类型去实例化`TD`:
|
||||
````cpp
|
||||
TD<decltype(x)> xType; //引出包含x和y
|
||||
TD<decltype(y)> yType; //的类型的错误消息
|
||||
TD<decltype(x)> xType; //引出包含x和y
|
||||
TD<decltype(y)> yType; //的类型的错误消息
|
||||
````
|
||||
我使用***variableName*****Type**的结构来命名变量,因为这样它们产生的错误消息可以有助于我们查找。对于上面的代码,我的编译器产生了这样的错误信息,我取一部分贴到下面:
|
||||
````cpp
|
||||
error: aggregate 'TD<int> xType' has incomplete type and
|
||||
cannot be defined
|
||||
cannot be defined
|
||||
error: aggregate 'TD<const int *> yType' has incomplete type and
|
||||
cannot be defined
|
||||
cannot be defined
|
||||
````
|
||||
另一个编译器也产生了一样的错误,只是格式稍微改变了一下:
|
||||
````cpp
|
||||
@ -52,7 +52,7 @@ error: 'yType' uses undefined class 'TD<const int *>'
|
||||
|
||||
使用`printf`的方法使类型信息只有在运行时才会显示出来(尽管我不是非常建议你使用`printf`),但是它提供了一种格式化输出的方法。现在唯一的问题是只需对于你关心的变量使用一种优雅的文本表示。“这有什么难的,“你这样想,”这正是`typeid`和`std::type_info::name`的价值所在”。为了实现我们我们想要查看`x`和`y`的类型的需求,你可能会这样写:
|
||||
````cpp
|
||||
std::cout << typeid(x).name() << '\n'; //显示x和y的类型
|
||||
std::cout << typeid(x).name() << '\n'; //显示x和y的类型
|
||||
std::cout << typeid(y).name() << '\n';
|
||||
````
|
||||
这种方法对一个对象如`x`或`y`调用`typeid`产生一个`std::type_info`的对象,然后`std::type_info`里面的成员函数`name()`来产生一个C风格的字符串(即一个`const char*`)表示变量的名字。
|
||||
@ -61,16 +61,16 @@ std::cout << typeid(y).name() << '\n';
|
||||
|
||||
因为对于`x`和`y`来说这样的结果是正确的,你可能认为问题已经接近了,别急,考虑一个更复杂的例子:
|
||||
````cpp
|
||||
template<typename T> //要调用的模板函数
|
||||
template<typename T> //要调用的模板函数
|
||||
void f(const T& param);
|
||||
|
||||
std::vector<Widget> createVec(); //工厂函数
|
||||
std::vector<Widget> createVec(); //工厂函数
|
||||
|
||||
const auto vw = createVec(); //使用工厂函数返回值初始化vw
|
||||
const auto vw = createVec(); //使用工厂函数返回值初始化vw
|
||||
|
||||
if (!vw.empty()){
|
||||
f(&vw[0]); //调用f
|
||||
...
|
||||
f(&vw[0]); //调用f
|
||||
…
|
||||
}
|
||||
````
|
||||
在这段代码中包含了一个用户定义的类型`Widget`,一个STL容器`std::vector`和一个`auto`变量`vw`,这个更现实的情况是你可能在会遇到的并且想获得他们类型推导的结果,比如模板类型形参`T`,比如函数`f`形参`param`。
|
||||
@ -80,16 +80,16 @@ if (!vw.empty()){
|
||||
template<typename T>
|
||||
void f(const T& param)
|
||||
{
|
||||
using std::cout;
|
||||
cout << "T = " << typeid(T).name() << '\n'; //显示T
|
||||
using std::cout;
|
||||
cout << "T = " << typeid(T).name() << '\n'; //显示T
|
||||
|
||||
cout << "param = " << typeid(param).name() << '\n'; //显示
|
||||
... //param
|
||||
} //的类型
|
||||
cout << "param = " << typeid(param).name() << '\n'; //显示
|
||||
... //param
|
||||
} //的类型
|
||||
````
|
||||
GNU和Clang执行这段代码将会输出这样的结果
|
||||
````cpp
|
||||
T = PK6Widget
|
||||
T = PK6Widget
|
||||
param = PK6Widget
|
||||
````
|
||||
我们早就知道在这些编译器中`PK`表示“pointer to `const`”,所以只有数字`6`对我们来说是神奇的。其实数字是类名称(`Widget`)的字符串长度,所以这些编译器告诉我们`T`和`param`都是`const Widget*`。
|
||||
@ -124,29 +124,29 @@ const std::_Simple_types<...>::value_type *const &
|
||||
template<typename T>
|
||||
void f(const T& param)
|
||||
{
|
||||
using std::cout;
|
||||
using boost::typeindex::type_id_with_cvr;
|
||||
using std::cout;
|
||||
using boost::typeindex::type_id_with_cvr;
|
||||
|
||||
//显示T
|
||||
cout << "T = "
|
||||
<< type_id_with_cvr<T>().pretty_name()
|
||||
<< '\n';
|
||||
//显示T
|
||||
cout << "T = "
|
||||
<< type_id_with_cvr<T>().pretty_name()
|
||||
<< '\n';
|
||||
|
||||
//显示param类型
|
||||
cout << "param = "
|
||||
<< type_id_with_cvr<decltype(param)>().pretty_name()
|
||||
<< '\n';
|
||||
//显示param类型
|
||||
cout << "param = "
|
||||
<< type_id_with_cvr<decltype(param)>().pretty_name()
|
||||
<< '\n';
|
||||
}
|
||||
````
|
||||
`boost::typeindex::type_id_with_cvr`获取一个类型实参(我们想获得相应信息的那个类型),它不消除实参的`const`,`volatile`和引用修饰符(因此模板名中有“`with_cur`”)。结果是一个`boost::typeindex::type_index`对象,它的`pretty_name`成员函数输出一个`std::string`,包含我们能看懂的类型表示。
|
||||
基于这个`f`的实现版本,再次考虑那个使用`typeid`时获取`param`类型信息出错的调用:
|
||||
|
||||
````cpp
|
||||
std::vetor<Widget> createVec(); //工厂函数
|
||||
const auto vw = createVec(); //使用工厂函数返回值初始化vw
|
||||
std::vetor<Widget> createVec(); //工厂函数
|
||||
const auto vw = createVec(); //使用工厂函数返回值初始化vw
|
||||
if (!vw.empty()){
|
||||
f(&vw[0]); //调用f
|
||||
...
|
||||
f(&vw[0]); //调用f
|
||||
…
|
||||
}
|
||||
````
|
||||
在GNU和Clang的编译器环境下,使用Boost.TypeIndex版本的`f`最后会产生下面的(准确的)输出:
|
||||
|
@ -19,14 +19,14 @@ int x;
|
||||
|
||||
别介意,让我们转换一个话题, 对一个局部变量使用解引用迭代器的方式初始化:
|
||||
````cpp
|
||||
template<typename It> //对从b到e的所有元素使用
|
||||
void dwim(It b, It e) //dwim(“do what I mean”)算法
|
||||
template<typename It> //对从b到e的所有元素使用
|
||||
void dwim(It b, It e) //dwim(“do what I mean”)算法
|
||||
{
|
||||
while (b != e) {
|
||||
typename std::iterator_traits<It>::value_type
|
||||
currValue = *b;
|
||||
...
|
||||
}
|
||||
while (b != e) {
|
||||
typename std::iterator_traits<It>::value_type
|
||||
currValue = *b;
|
||||
…
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
@ -38,45 +38,45 @@ void dwim(It b, It e) //dwim(“do what I mean”)算法
|
||||
|
||||
别担心,它只在过去是这样,到了C++11所有的这些问题都消失了,这都多亏了`auto`。`auto`变量从初始化表达式中推导出类型,所以我们必须初始化。这意味着当你在现代化C++的高速公路上飞奔的同时你不得不对只声明不初始化变量的老旧方法说拜拜:
|
||||
````cpp
|
||||
int x1; //潜在的未初始化的变量
|
||||
int x1; //潜在的未初始化的变量
|
||||
|
||||
auto x2; //错误!必须要初始化
|
||||
auto x2; //错误!必须要初始化
|
||||
|
||||
auto x3 = 0; //没问题,x已经定义了
|
||||
auto x3 = 0; //没问题,x已经定义了
|
||||
````
|
||||
而且即使使用解引用迭代器初始化局部变量也不会对你的高速驾驶有任何影响
|
||||
````cpp
|
||||
template<typename It> //如之前一样
|
||||
template<typename It> //如之前一样
|
||||
void dwim(It b,It e)
|
||||
{
|
||||
while (b != e) {
|
||||
auto currValue = *b;
|
||||
...
|
||||
}
|
||||
while (b != e) {
|
||||
auto currValue = *b;
|
||||
…
|
||||
}
|
||||
}
|
||||
````
|
||||
因为使用[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md)所述的`auto`类型推导技术,它甚至能表示一些只有编译器才知道的类型:
|
||||
````cpp
|
||||
auto derefUPLess =
|
||||
[](const std::unique_ptr<Widget> &p1, //用于std::unique_ptr
|
||||
const std::unique_ptr<Widget> &p2) //指向的Widget类型的
|
||||
{ return *p1 < *p2; }; //比较函数
|
||||
[](const std::unique_ptr<Widget> &p1, //用于std::unique_ptr
|
||||
const std::unique_ptr<Widget> &p2) //指向的Widget类型的
|
||||
{ return *p1 < *p2; }; //比较函数
|
||||
````
|
||||
很酷对吧,如果使用C++14,将会变得更酷,因为*lambda*表达式中的形参也可以使用`auto`:
|
||||
````cpp
|
||||
auto derefLess = //C++14版本
|
||||
[](const auto& p1, //被任何像指针一样的东西
|
||||
const auto& p2) //指向的值的比较函数
|
||||
{ return *p1 < *p2; };
|
||||
auto derefLess = //C++14版本
|
||||
[](const auto& p1, //被任何像指针一样的东西
|
||||
const auto& p2) //指向的值的比较函数
|
||||
{ return *p1 < *p2; };
|
||||
````
|
||||
尽管这很酷,但是你可能会想我们完全不需要使用`auto`声明局部变量来保存一个闭包,因为我们可以使用`std::function`对象。没错,我们的确可以那么做,但是事情可能不是完全如你想的那样。当然现在你可能会问,`std::function`对象到底是什么。让我来给你解释一下。
|
||||
|
||||
`std::function`是一个C++11标准模板库中的一个模板,它泛化了函数指针的概念。与函数指针只能指向函数不同,`std::function`可以指向任何可调用对象,也就是那些像函数一样能进行调用的东西。当你声明函数指针时你必须指定函数类型(即函数签名),同样当你创建`std::function`对象时你也需要提供函数签名,由于它是一个模板所以你需要在它的模板参数里面提供。举个例子,假设你想声明一个`std::function`对象`func`使它指向一个可调用对象,比如一个具有这样函数签名的函数,
|
||||
|
||||
````cpp
|
||||
bool(const std::unique_ptr<Widget> &, //C++11
|
||||
const std::unique_ptr<Widget> &) //std::unique_ptr<Widget>
|
||||
//比较函数的签名
|
||||
bool(const std::unique_ptr<Widget> &, //C++11
|
||||
const std::unique_ptr<Widget> &) //std::unique_ptr<Widget>
|
||||
//比较函数的签名
|
||||
````
|
||||
你就得这么写:
|
||||
````cpp
|
||||
@ -87,32 +87,32 @@ std::function<bool(const std::unique_ptr<Widget> &,
|
||||
````cpp
|
||||
std::function<bool(const std::unique_ptr<Widget> &,
|
||||
const std::unique_ptr<Widget> &)>
|
||||
derefUPLess = [](const std::unique_ptr<Widget> &p1,
|
||||
const std::unique_ptr<Widget> &p2)
|
||||
{ return *p1 < *p2; };
|
||||
derefUPLess = [](const std::unique_ptr<Widget> &p1,
|
||||
const std::unique_ptr<Widget> &p2)
|
||||
{ return *p1 < *p2; };
|
||||
````
|
||||
语法冗长不说,还需要重复写很多形参类型,使用`std::function`还不如使用`auto`。用`auto`声明的变量保存一个和闭包一样类型的(新)闭包,因此使用了与闭包相同大小存储空间。实例化`std::function`并声明一个对象这个对象将会有固定的大小。这个大小可能不足以存储一个闭包,这个时候`std::function`的构造函数将会在堆上面分配内存来存储,这就造成了使用`std::function`比`auto`声明变量会消耗更多的内存。并且通过具体实现我们得知通过`std::function`调用一个闭包几乎无疑比`auto`声明的对象调用要慢。换句话说,`std::function`方法比`auto`方法要更耗空间且更慢,还可能有*out-of-memory*异常。并且正如上面的例子,比起写`std::function`实例化的类型来,使用`auto`要方便得多。在这场存储闭包的比赛中,`auto`无疑取得了胜利(也可以使用`std::bind`来生成一个闭包,但在[Item34](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item34.md)我会尽我最大努力说服你使用*lambda*表达式代替`std::bind`)
|
||||
|
||||
使用`auto`除了可以避免未初始化的无效变量,省略冗长的声明类型,直接保存闭包外,它还有一个好处是可以避免一个问题,我称之为与类型快捷方式(type shortcuts)有关的问题。你将看到这样的代码——甚至你会这么写:
|
||||
````cpp
|
||||
std::vector<int> v;
|
||||
...
|
||||
…
|
||||
unsigned sz = v.size();
|
||||
````
|
||||
`v.size()`的标准返回类型是`std::vector<int>::size_type`,但是只有少数开发者意识到这点。`std::vector<int>::size_type`实际上被指定为无符号整型,所以很多人都认为用`unsigned`就足够了,写下了上述的代码。这会造成一些有趣的结果。举个例子,在**Windows 32-bit**上`std::vector<int>::size_type`和`unsigned`是一样的大小,但是在**Windows 64-bit**上`std::vector<int>::size_type`是64位,`unsigned`是32位。这意味着这段代码在Windows 32-bit上正常工作,但是当把应用程序移植到Windows 64-bit上时就可能会出现一些问题。谁愿意花时间处理这些细枝末节的问题呢?
|
||||
|
||||
所以使用`auto`可以确保你不需要浪费时间:
|
||||
````cpp
|
||||
auto sz =v.size(); //sz的类型是std::vector<int>::size_type
|
||||
auto sz =v.size(); //sz的类型是std::vector<int>::size_type
|
||||
````
|
||||
你还是不相信使用`auto`是多么明智的选择?考虑下面的代码:
|
||||
````cpp
|
||||
std::unordered_map<std::string, int> m;
|
||||
...
|
||||
…
|
||||
|
||||
for(const std::pair<std::string, int>& p : m)
|
||||
{
|
||||
... //用p做一些事
|
||||
… //用p做一些事
|
||||
}
|
||||
````
|
||||
看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗?
|
||||
@ -123,7 +123,7 @@ for(const std::pair<std::string, int>& p : m)
|
||||
````cpp
|
||||
for(const auto& p : m)
|
||||
{
|
||||
... //如之前一样
|
||||
… //如之前一样
|
||||
}
|
||||
````
|
||||
这样无疑更具效率,且更容易书写。而且,这个代码有一个非常吸引人的特性,如果你获取`p`的地址,你确实会得到一个指向`m`中元素的指针。在没有`auto`的版本中`p`会指向一个临时变量,这个临时变量在每次迭代完成时会被销毁。
|
||||
|
@ -9,18 +9,18 @@ std::vector<bool> features(const Widget& w);
|
||||
更进一步假设第5个*bit*表示`Widget`是否具有高优先级,我们可以写这样的代码:
|
||||
````cpp
|
||||
Widget w;
|
||||
...
|
||||
bool highPriority = features(w)[5]; //w高优先级吗?
|
||||
...
|
||||
processWidget(w, highPriority); //根据它的优先级处理w
|
||||
…
|
||||
bool highPriority = features(w)[5]; //w高优先级吗?
|
||||
…
|
||||
processWidget(w, highPriority); //根据它的优先级处理w
|
||||
````
|
||||
这个代码没有任何问题。它会正常工作,但是如果我们使用`auto`代替`highPriority`的显式指定类型做一些看起来很无害的改变:
|
||||
````cpp
|
||||
auto highPriority = features(w)[5]; //w高优先级吗?
|
||||
auto highPriority = features(w)[5]; //w高优先级吗?
|
||||
````
|
||||
情况变了。所有代码仍然可编译,但是行为不再可预测:
|
||||
````cpp
|
||||
processWidget(w,highPriority); //未定义行为!
|
||||
processWidget(w,highPriority); //未定义行为!
|
||||
````
|
||||
就像注释说的,这个`processWidget`是一个未定义行为。为什么呢?答案有可能让你很惊讶,使用`auto`后`highPriority`不再是`bool`类型。虽然从概念上来说`std::vector<bool>`意味着存放`bool`,但是`std::vector<bool>`的`operator[]`不会返回容器中元素的引用(这就是`std::vector::operator[]`可返回**除了`bool`以外**的任何类型),取而代之它返回一个`std::vector<bool>::reference`的对象(一个嵌套于`std::vector<bool>`中的类)。
|
||||
|
||||
@ -44,8 +44,8 @@ auto highPriority = features(w)[5]; //推导highPriority的类型
|
||||
调用`features`将返回一个`std::vector<bool>`临时对象,这个对象没有名字,为了方便我们的讨论,我这里叫他`temp`。`std::vector<bool>::reference`包含一个指向*word*的指针(`temp`管理这个*word*中的*bit*s),还有相应于第5个*bit*的偏移。`highPriority`是这个`std::vector<bool>::reference`的拷贝,所以`highPriority`也包含一个指针,指向`temp`中的这个*word*,加上相应于第5个*bit*的偏移。在这个语句结束的时候`temp`将会被销毁,因为它是一个临时变量。因此`highPriority`包含一个悬置的(*dangling*)指针,如果用于`processWidget`调用中将会造成未定义行为:
|
||||
|
||||
````cpp
|
||||
processWidget(w, highPriority); //未定义行为!
|
||||
//highPriority包含一个悬置指针!
|
||||
processWidget(w, highPriority); //未定义行为!
|
||||
//highPriority包含一个悬置指针!
|
||||
````
|
||||
|
||||
`std::vector<bool>::reference`是一个代理类(*proxy class*)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,`std::vector<bool>::reference`展示了对`std::vector<bool>`使用`operator[]`来实现引用*bit*这样的行为。另外,C++标准模板库中的智能指针(见[第4章](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md))也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。事实上,“Proxy”设计模式是软件设计这座万神庙中一直都存在的高级会员。
|
||||
@ -70,15 +70,15 @@ auto someVar = expression of "invisible" proxy class type;
|
||||
|
||||
当缺少文档的时候,可以去看看头文件。很少会出现源代码全都用代理对象,它们通常用于一些函数的返回类型,所以通常能从函数签名中看出它们的存在。这里有一份`std::vector<bool>::operator[]`的说明书:
|
||||
````cpp
|
||||
namespace std{ //来自于C++标准库
|
||||
namespace std{ //来自于C++标准库
|
||||
template<class Allocator>
|
||||
class vector<bool, Allocator>{
|
||||
public:
|
||||
...
|
||||
class reference { ... };
|
||||
public:
|
||||
…
|
||||
class reference { … };
|
||||
|
||||
reference operator[](size_type n);
|
||||
...
|
||||
…
|
||||
};
|
||||
}
|
||||
````
|
||||
@ -98,12 +98,12 @@ auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
|
||||
````
|
||||
应用这个惯用法不限制初始化表达式产生一个代理类。它也可以用于强调你声明了一个变量类型,它的类型不同于初始化表达式的类型。举个例子,假设你有这样一个表达式计算公差值:
|
||||
````cpp
|
||||
double calcEpsilon(); //返回公差值
|
||||
double calcEpsilon(); //返回公差值
|
||||
````
|
||||
`calcEpsilon`清楚的表明它返回一个`double`,但是假设你知道对于这个程序来说使用`float`的精度已经足够了,而且你很关心`double`和`float`的大小。你可以声明一个`float`变量储存`calEpsilon`的计算结果。
|
||||
|
||||
````cpp
|
||||
float ep = calcEpsilon(); //double到float隐式转换
|
||||
float ep = calcEpsilon(); //double到float隐式转换
|
||||
````
|
||||
但是这几乎没有表明“我确实要减少函数返回值的精度”。使用显式类型初始器惯用法我们可以这样:
|
||||
````cpp
|
||||
|
@ -30,12 +30,12 @@ std::vector<std::size_t> //func返回x的质因子
|
||||
primeFactors(std::size_t x);
|
||||
|
||||
Color c = red;
|
||||
...
|
||||
…
|
||||
|
||||
if (c < 14.5) { // Color与double比较 (!)
|
||||
auto factors = // 计算一个Color的质因子(!)
|
||||
primeFactors(c);
|
||||
…
|
||||
…
|
||||
}
|
||||
```
|
||||
在`enum`后面写一个`class`就可以将非限域`enum`转换为限域`enum`,接下来就是完全不同的故事展开了。现在不存在任何隐式转换可以将限域`enum`中的枚举名转化为任何其他类型:
|
||||
@ -49,7 +49,7 @@ if (c < 14.5) { //错误!不能比较
|
||||
//Color和double
|
||||
auto factors = //错误!不能向参数为std::size_t
|
||||
primeFactors(c); //的函数传递Color参数
|
||||
...
|
||||
…
|
||||
}
|
||||
```
|
||||
如果你真的很想执行`Color`到其他类型的转换,和平常一样,使用正确的类型转换运算符扭曲类型系统:
|
||||
@ -58,7 +58,7 @@ if (static_cast<double>(c) < 14.5) { //奇怪的代码,
|
||||
//但是有效
|
||||
auto factors = //有问题,但是
|
||||
primeFactors(static_cast<std::size_t>(c)); //能通过编译
|
||||
...
|
||||
…
|
||||
}
|
||||
```
|
||||
似乎比起非限域`enum`而言,限域`enum`有第三个好处,因为限域`enum`可以被前置声明。也就是说,它们可以不指定枚举名直接声明:
|
||||
@ -152,7 +152,7 @@ using UserInfo = //类型别名,参见Item9
|
||||
虽然注释说明了tuple各个字段对应的意思,但当你在另一文件遇到下面的代码那之前的注释就不是那么有用了:
|
||||
```cpp
|
||||
UserInfo uInfo; //tuple对象
|
||||
...
|
||||
…
|
||||
auto val = std::get<1>(uInfo); //获取第一个字段
|
||||
```
|
||||
作为一个程序员,你有很多工作要持续跟进。你应该记住第一个字段代表用户的email地址吗?我认为不。可以使用非限域`enum`将名字和字段编号关联起来以避免上述需求:
|
||||
@ -160,7 +160,7 @@ auto val = std::get<1>(uInfo); //获取第一个字段
|
||||
enum UserInfoFields { uiName, uiEmail, uiReputation };
|
||||
|
||||
UserInfo uInfo; //同之前一样
|
||||
...
|
||||
…
|
||||
auto val = std::get<uiEmail>(uInfo); //啊,获取用户email字段的值
|
||||
```
|
||||
之所以它能正常工作是因为`UserInfoFields`中的枚举名隐式转换成`std::size_t`了,其中`std::size_t`是`std::get`模板实参所需的。
|
||||
@ -171,10 +171,10 @@ auto val = std::get<uiEmail>(uInfo); //啊,获取用户email字段的值
|
||||
enum class UserInfoFields { uiName, uiEmail, uiReputation };
|
||||
|
||||
UserInfo uInfo; //同之前一样
|
||||
...
|
||||
…
|
||||
auto val =
|
||||
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>
|
||||
(uInfo);
|
||||
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>
|
||||
(uInfo);
|
||||
```
|
||||
为避免这种冗长的表示,我们可以写一个函数传入枚举名并返回对应的`std::size_t`值,但这有一点技巧性。`std::get`是一个模板(函数),需要你给出一个`std::size_t`值的模板实参(注意使用`<>`而不是`()`),因此将枚举名变换为`std::size_t`值的函数必须**在编译期**产生这个结果。如[Item15](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item15.md)提到的,那必须是一个`constexpr`函数。
|
||||
|
||||
@ -183,11 +183,11 @@ auto val =
|
||||
```cpp
|
||||
template<typename E>
|
||||
constexpr typename std::underlying_type<E>::type
|
||||
toUType(E enumerator) noexcept
|
||||
toUType(E enumerator) noexcept
|
||||
{
|
||||
return
|
||||
static_cast<typename
|
||||
std::underlying_type<E>::type>(enumerator);
|
||||
return
|
||||
static_cast<typename
|
||||
std::underlying_type<E>::type>(enumerator);
|
||||
}
|
||||
```
|
||||
在C++14中,`toUType`还可以进一步用`std::underlying_type_t`(参见[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md))代替`typename std::underlying_type<E>::type`打磨:
|
||||
@ -195,18 +195,18 @@ constexpr typename std::underlying_type<E>::type
|
||||
```cpp
|
||||
template<typename E> //C++14
|
||||
constexpr std::underlying_type_t<E>
|
||||
toUType(E enumerator) noexcept
|
||||
toUType(E enumerator) noexcept
|
||||
{
|
||||
return static_cast<std::underlying_type_t<E>>(enumerator);
|
||||
return static_cast<std::underlying_type_t<E>>(enumerator);
|
||||
}
|
||||
```
|
||||
还可以再用C++14 `auto`(参见[Item3](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md))打磨一下代码:
|
||||
```cpp
|
||||
template<typename E> //C++14
|
||||
constexpr auto
|
||||
toUType(E enumerator) noexcept
|
||||
toUType(E enumerator) noexcept
|
||||
{
|
||||
return static_cast<std::underlying_type_t<E>>(enumerator);
|
||||
return static_cast<std::underlying_type_t<E>>(enumerator);
|
||||
}
|
||||
```
|
||||
不管它怎么写,`toUType`现在允许这样访问tuple的字段了:
|
||||
|
@ -1,4 +1,4 @@
|
||||
## 条款十一:优先考虑使用deleted函数而非使用未定义的私有声明
|
||||
## 条款十一:优先考虑使用*deleted*函数而非使用未定义的私有声明
|
||||
|
||||
**Item 11: Prefer deleted functions to private undefined ones.**
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
template <class charT, class traits = char_traits<charT> >
|
||||
class basic_ios : public ios_base {
|
||||
public:
|
||||
...
|
||||
…
|
||||
|
||||
private:
|
||||
basic_ios(const basic_ios& ); // not defined
|
||||
@ -27,11 +27,11 @@ private:
|
||||
template <class charT, class traits = char_traits<charT> >
|
||||
class basic_ios : public ios_base {
|
||||
public:
|
||||
...
|
||||
…
|
||||
|
||||
basic_ios(const basic_ios& ) = delete;
|
||||
basic_ios& operator=(const basic_ios&) = delete;
|
||||
...
|
||||
…
|
||||
};
|
||||
```
|
||||
删除这些函数(译注:添加"`= delete`")和声明为私有成员可能看起来只是方式不同,别无其他区别。其实还有一些实质性意义。*deleted*函数不能以任何方式被调用,即使你在成员函数或者友元函数里面调用*deleted*函数也不能通过编译。这是较之C++98行为的一个改进,C++98中不正确的使用这些函数在链接时才被诊断出来。
|
||||
@ -45,9 +45,9 @@ bool isLucky(int number);
|
||||
```
|
||||
C++有沉重的C包袱,使得含糊的、能被视作数值的任何类型都能隐式转换为`int`,但是有一些调用可能是没有意义的:
|
||||
```cpp
|
||||
if (isLucky('a')) ... //字符'a'是幸运数?
|
||||
if (isLucky(true)) ... //"true"是?
|
||||
if (isLucky(3.5)) ... //难道判断它的幸运之前还要先截尾成3?
|
||||
if (isLucky('a')) … //字符'a'是幸运数?
|
||||
if (isLucky(true)) … //"true"是?
|
||||
if (isLucky(3.5)) … //难道判断它的幸运之前还要先截尾成3?
|
||||
```
|
||||
如果幸运数必须真的是整型,我们该禁止这些调用通过编译。
|
||||
|
||||
@ -63,9 +63,9 @@ bool isLucky(double) = delete; //拒绝float和double
|
||||
|
||||
虽然*deleted*函数不能被使用,但它们还是存在于你的程序中。也即是说,重载决议会考虑它们。这也是为什么上面的函数声明导致编译器拒绝一些不合适的函数调用。
|
||||
```cpp
|
||||
if (isLucky('a')) ... //错误!调用deleted函数
|
||||
if (isLucky(true)) ... //错误!
|
||||
if (isLucky(3.5f)) ... //错误!
|
||||
if (isLucky('a')) … //错误!调用deleted函数
|
||||
if (isLucky(true)) … //错误!
|
||||
if (isLucky(3.5f)) … //错误!
|
||||
```
|
||||
另一个*deleted*函数用武之地(`private`成员函数做不到的地方)是禁止一些模板的实例化。假如你要求一个模板仅支持原生指针(尽管[第四章](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md)建议使用智能指针代替原生指针):
|
||||
```cpp
|
||||
@ -98,14 +98,14 @@ void processPointer<const char>(const char*) = delete;
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
...
|
||||
template<typename T>
|
||||
void processPointer(T* ptr)
|
||||
{ ... }
|
||||
…
|
||||
template<typename T>
|
||||
void processPointer(T* ptr)
|
||||
{ … }
|
||||
|
||||
private:
|
||||
template<> //错误!
|
||||
void processPointer<void>(void*);
|
||||
template<> //错误!
|
||||
void processPointer<void>(void*);
|
||||
|
||||
};
|
||||
```
|
||||
@ -113,11 +113,11 @@ private:
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
...
|
||||
template<typename T>
|
||||
void processPointer(T* ptr)
|
||||
{ ... }
|
||||
...
|
||||
…
|
||||
template<typename T>
|
||||
void processPointer(T* ptr)
|
||||
{ … }
|
||||
…
|
||||
|
||||
};
|
||||
|
||||
|
@ -46,7 +46,7 @@ public:
|
||||
void doWork() &&; //只有*this为右值的时候才能被调用
|
||||
};
|
||||
…
|
||||
Widget makeWidget(); //工厂函数(返回右值)
|
||||
Widget makeWidget(); //工厂函数(返回右值)
|
||||
Widget w; //普通对象(左值)
|
||||
…
|
||||
w.doWork(); //调用被左值引用限定修饰的Widget::doWork版本
|
||||
|
@ -10,15 +10,15 @@
|
||||
|
||||
从你的角度看,C++11初始化对象的语法选择既丰富得让人尴尬又混乱得让人糊涂。一般来说,初始化值要用()或者{}括起来或者放到等号"="的右边:
|
||||
````cpp
|
||||
int x(0); //使用小括号初始化
|
||||
int x(0); //使用小括号初始化
|
||||
|
||||
int y = 0; //使用"="初始化
|
||||
int y = 0; //使用"="初始化
|
||||
|
||||
int z{ 0 }; //使用花括号初始化
|
||||
int z{ 0 }; //使用花括号初始化
|
||||
````
|
||||
在很多情况下,你可以使用"="和花括号的组合:
|
||||
````cpp
|
||||
int z = { 0 }; //使用"="和花括号
|
||||
int z = { 0 }; //使用"="和花括号
|
||||
````
|
||||
在这个条款的剩下部分,我通常会忽略"="和花括号组合初始化的语法,因为C++通常把它视作和只有花括号一样。
|
||||
|
||||
@ -39,24 +39,24 @@ C++11使用统一初始化(*uniform initialization*)来整合这些混乱且
|
||||
括号初始化让你可以表达以前表达不出的东西。使用花括号,指定一个容器的元素变得很容易:
|
||||
|
||||
````cpp
|
||||
std::vector<int> v{ 1, 3, 5 }; //v初始内容为1,3,5
|
||||
std::vector<int> v{ 1, 3, 5 }; //v初始内容为1,3,5
|
||||
````
|
||||
括号初始化也能被用于为非静态数据成员指定默认初始值。C++11允许"="初始化不加花括号也拥有这种能力:
|
||||
````cpp
|
||||
class Widget{
|
||||
...
|
||||
…
|
||||
|
||||
private:
|
||||
int x{ 0 }; //没问题,x初始值为0
|
||||
int y = 0; //也可以
|
||||
int z(0); //错误!
|
||||
int x{ 0 }; //没问题,x初始值为0
|
||||
int y = 0; //也可以
|
||||
int z(0); //错误!
|
||||
}
|
||||
````
|
||||
另一方面,不可拷贝的对象(例如`std::atomic`——见[Item40](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md))可以使用花括号初始化或者小括号初始化,但是不能使用"="初始化:
|
||||
````cpp
|
||||
std::atomic<int> ai1{ 0 }; //没问题
|
||||
std::atomic<int> ai2(0); //没问题
|
||||
std::atomic<int> ai3 = 0; //错误!
|
||||
std::atomic<int> ai1{ 0 }; //没问题
|
||||
std::atomic<int> ai2(0); //没问题
|
||||
std::atomic<int> ai3 = 0; //错误!
|
||||
````
|
||||
因此我们很容易理解为什么括号初始化又叫统一初始化,在C++中这三种方式都被指派为初始化表达式,但是只有括号任何地方都能被使用。
|
||||
|
||||
@ -64,26 +64,26 @@ std::atomic<int> ai3 = 0; //错误!
|
||||
````cpp
|
||||
double x, y, z;
|
||||
|
||||
int sum1{ x + y + z }; //错误!double的和可能不能表示为int
|
||||
int sum1{ x + y + z }; //错误!double的和可能不能表示为int
|
||||
````
|
||||
使用小括号和"="的初始化不检查是否转换为变窄转换,因为由于历史遗留问题它们必须要兼容老旧代码:
|
||||
````cpp
|
||||
int sum2(x + y +z); //可以(表达式的值被截为int)
|
||||
int sum2(x + y +z); //可以(表达式的值被截为int)
|
||||
|
||||
int sum3 = x + y + z; //同上
|
||||
int sum3 = x + y + z; //同上
|
||||
````
|
||||
另一个值得注意的特性是括号表达式对于C++最令人头疼的解析问题有天生的免疫性。(译注:所谓最令人头疼的解析即*most vexing parse*,更多信息请参见[https://en.wikipedia.org/wiki/Most_vexing_parse](https://en.wikipedia.org/wiki/Most_vexing_parse)。)C++规定任何能被决议为一个声明的东西必须被决议为声明。这个规则的副作用是让很多程序员备受折磨:当他们想创建一个使用默认构造函数构造的对象,却不小心变成了函数声明。问题的根源是如果你想使用一个实参调用一个构造函数,你可以这样做:
|
||||
|
||||
````cpp
|
||||
Widget w1(10); //使用实参10调用Widget的一个构造函数
|
||||
Widget w1(10); //使用实参10调用Widget的一个构造函数
|
||||
````
|
||||
但是如果你尝试使用相似的语法调用没有参数的`Widget`构造函数,它就会变成函数声明:
|
||||
````cpp
|
||||
Widget w2(); //最令人头疼的解析!声明一个函数w2,返回Widget
|
||||
Widget w2(); //最令人头疼的解析!声明一个函数w2,返回Widget
|
||||
````
|
||||
由于函数声明中形参列表不能使用花括号,所以使用花括号初始化表明你想调用默认构造函数构造对象就没有问题:
|
||||
````cpp
|
||||
Widget w3{}; //调用没有参数的构造函数构造对象
|
||||
Widget w3{}; //调用没有参数的构造函数构造对象
|
||||
````
|
||||
关于括号初始化还有很多要说的。它的语法能用于各种不同的上下文,它防止了隐式的变窄转换,而且对于C++最令人头疼的解析也天生免疫。既然好到这个程度那为什么这个条款不叫“Prefer braced initialization syntax”呢?
|
||||
|
||||
@ -93,23 +93,23 @@ Widget w3{}; //调用没有参数的构造函数构造对象
|
||||
````cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(int i, bool b); //构造函数未声明
|
||||
Widget(int i, double d); //std::initializer_list形参
|
||||
...
|
||||
Widget(int i, bool b); //构造函数未声明
|
||||
Widget(int i, double d); //std::initializer_list形参
|
||||
…
|
||||
};
|
||||
Widget w1(10, true); //调用第一个构造函数
|
||||
Widget w2{10, true}; //也调用第一个构造函数
|
||||
Widget w3(10, 5.0); //调用第二个构造函数
|
||||
Widget w4{10, 5.0}; //也调用第二个构造函数
|
||||
Widget w1(10, true); //调用第一个构造函数
|
||||
Widget w2{10, true}; //也调用第一个构造函数
|
||||
Widget w3(10, 5.0); //调用第二个构造函数
|
||||
Widget w4{10, 5.0}; //也调用第二个构造函数
|
||||
````
|
||||
然而,如果有一个或者多个构造函数的声明一个`std::initializer_list`形参,使用括号初始化语法的调用更倾向于适用`std::initializer_list`重载函数。而且只要某个使用括号表达式的调用能适用接受`std::initializer_list`的构造函数,编译器就会使用它。如果上面的`Widget`类有一个`std::initializer_list<long double>`构造函数并被传入实参,就像这样:
|
||||
````cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(int i, bool b); //同上
|
||||
Widget(int i, double d); //同上
|
||||
Widget(std::initializer_list<long double> il); //新添加的
|
||||
...
|
||||
Widget(int i, bool b); //同上
|
||||
Widget(int i, double d); //同上
|
||||
Widget(std::initializer_list<long double> il); //新添加的
|
||||
…
|
||||
};
|
||||
````
|
||||
`w2`和`w4`将会使用新添加的构造函数构造,即使另一个非`std::initializer_list`构造函数对于实参是更好的选择:
|
||||
@ -133,34 +133,34 @@ Widget w4{10, 5.0}; //使用花括号初始化,但是现在
|
||||
````cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(int i, bool b); //同之前一样
|
||||
Widget(int i, double d); //同之前一样
|
||||
Widget(std::initializer_list<long double> il); //同之前一样
|
||||
operator float() const; //转换为float
|
||||
...
|
||||
Widget(int i, bool b); //同之前一样
|
||||
Widget(int i, double d); //同之前一样
|
||||
Widget(std::initializer_list<long double> il); //同之前一样
|
||||
operator float() const; //转换为float
|
||||
…
|
||||
};
|
||||
|
||||
Widget w5(w4); //使用小括号,调用拷贝构造函数
|
||||
Widget w5(w4); //使用小括号,调用拷贝构造函数
|
||||
|
||||
Widget w6{w4}; //使用花括号,调用std::initializer_list构造
|
||||
//函数(w4转换为float,float转换为double)
|
||||
Widget w6{w4}; //使用花括号,调用std::initializer_list构造
|
||||
//函数(w4转换为float,float转换为double)
|
||||
|
||||
Widget w7(std::move(w4)); //使用小括号,调用移动构造函数
|
||||
Widget w7(std::move(w4)); //使用小括号,调用移动构造函数
|
||||
|
||||
Widget w8{std::move(w4)}; //使用花括号,调用std::initializer_list构造
|
||||
//函数(与w6相同原因)
|
||||
Widget w8{std::move(w4)}; //使用花括号,调用std::initializer_list构造
|
||||
//函数(与w6相同原因)
|
||||
````
|
||||
编译器热衷于把括号初始化与使`std::initializer_list`构造函数匹配了,尽管最佳匹配`std::initializer_list`构造函数不能被调用也会凑上去。比如:
|
||||
````cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(int i, bool b); //同之前一样
|
||||
Widget(int i, double d); //同之前一样
|
||||
Widget(std::initializer_list<bool> il); //现在元素类型为bool
|
||||
... //没有隐式转换函数
|
||||
Widget(int i, bool b); //同之前一样
|
||||
Widget(int i, double d); //同之前一样
|
||||
Widget(std::initializer_list<bool> il); //现在元素类型为bool
|
||||
… //没有隐式转换函数
|
||||
};
|
||||
|
||||
Widget w{10, 5.0}; //错误!要求变窄转换
|
||||
Widget w{10, 5.0}; //错误!要求变窄转换
|
||||
````
|
||||
这里,编译器会直接忽略前面两个构造函数(其中第二个提供了所有实参类型的最佳匹配),然后尝试调用`std::initializer_list<bool>`构造函数。调用这个函数将会把`int(10)`和`double(5.0)`转换为`bool`,由于会产生变窄转换(`bool`不能准确表示其中任何一个值),括号初始化拒绝变窄转换,所以这个调用无效,代码无法通过编译。
|
||||
|
||||
@ -169,11 +169,11 @@ Widget w{10, 5.0}; //错误!要求变窄转换
|
||||
````cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(int i, bool b); //同之前一样
|
||||
Widget(int i, double d); //同之前一样
|
||||
//现在std::initializer_list元素类型为std::string
|
||||
Widget(std::initializer_list<std::string> il);
|
||||
... //没有隐式转换函数
|
||||
Widget(int i, bool b); //同之前一样
|
||||
Widget(int i, double d); //同之前一样
|
||||
//现在std::initializer_list元素类型为std::string
|
||||
Widget(std::initializer_list<std::string> il);
|
||||
… //没有隐式转换函数
|
||||
};
|
||||
|
||||
Widget w1(10, true); // 使用小括号初始化,调用第一个构造函数
|
||||
@ -187,20 +187,20 @@ Widget w4{10, 5.0}; // 使用花括号初始化,现在调用第二个构
|
||||
````cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(); //默认构造函数
|
||||
Widget(std::initializer_list<int> il); //std::initializer_list构造函数
|
||||
Widget(); //默认构造函数
|
||||
Widget(std::initializer_list<int> il); //std::initializer_list构造函数
|
||||
|
||||
... //没有隐式转换函数
|
||||
… //没有隐式转换函数
|
||||
};
|
||||
|
||||
Widget w1; //调用默认构造函数
|
||||
Widget w2{}; //也调用默认构造函数
|
||||
Widget w3(); //最令人头疼的解析!声明一个函数
|
||||
Widget w1; //调用默认构造函数
|
||||
Widget w2{}; //也调用默认构造函数
|
||||
Widget w3(); //最令人头疼的解析!声明一个函数
|
||||
````
|
||||
如果你**想**用空`std::initializer`来调用`std::initializer_list`构造函数,你就得创建一个空花括号作为函数实参——通过把空花括号放在小括号或者另一花括号内来界定你想传递的东西。
|
||||
````cpp
|
||||
Widget w4({}); //使用空花括号列表调用std::initializer_list构造函数
|
||||
Widget w5{{}}; //同上
|
||||
Widget w4({}); //使用空花括号列表调用std::initializer_list构造函数
|
||||
Widget w5{{}}; //同上
|
||||
````
|
||||
此时,括号初始化,`std::initializer_list`和构造函数重载的晦涩规则就会一下子涌进你的脑袋,你可能会想研究了半天这些东西在你的日常编程中到底占多大比例。可能比你想象的要多。因为`std::vector`作为其中之一会直接受到影响。`std::vector`有一个非`std::initializer_list`构造函数允许你去指定容器的初始大小,以及使用一个值填满你的容器。但它也有一个`std::initializer_list`构造函数允许你使用花括号里面的值初始化容器。如果你创建一个数值类型的`std::vector`(比如`std::vector<int>`),然后你传递两个实参,把这两个实参放到小括号和放到花括号中大不相同:
|
||||
|
||||
@ -225,19 +225,19 @@ template<typename T, //要创建的对象类型
|
||||
typename... Ts> //要使用的实参的类型
|
||||
void doSomeWork(Ts&&... params)
|
||||
{
|
||||
create local T object from params...
|
||||
...
|
||||
create local T object from params...
|
||||
…
|
||||
}
|
||||
````
|
||||
在现实中我们有两种方式实现这个伪代码(关于`std::forward`请参见[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)):
|
||||
````cpp
|
||||
T localObject(std::forward<Ts>(params)...); //使用小括号
|
||||
T localObject{std::forward<Ts>(params)...}; //使用花括号
|
||||
T localObject(std::forward<Ts>(params)...); //使用小括号
|
||||
T localObject{std::forward<Ts>(params)...}; //使用花括号
|
||||
````
|
||||
考虑这样的调用代码:
|
||||
````cpp
|
||||
std::vector<int> v;
|
||||
...
|
||||
…
|
||||
doSomeWork<std::vector<int>>(10, 20);
|
||||
````
|
||||
如果`doSomeWork`创建`localObject`时使用的是小括号,`std::vector`就会包含10个元素。如果`doSomeWork`创建`localObject`时使用的是花括号,`std::vector`就会包含2个元素。哪个是正确的?`doSomeWork`的作者不知道,只有调用者知道。
|
||||
|
@ -29,7 +29,7 @@ f(nullptr); //调用重载函数f的f(void*)版本
|
||||
````cpp
|
||||
auto result = findRecord( /* arguments */ );
|
||||
if (result == 0) {
|
||||
...
|
||||
…
|
||||
}
|
||||
````
|
||||
如果你不知道`findRecord`返回了什么(或者不能轻易的找出),那么你就不太清楚到底`result`是一个指针类型还是一个整型。毕竟,`0`(用来测试`result`的值的那个)也可以像我们之前讨论的那样被解析。但是换一种假设如果你看到这样的代码:
|
||||
@ -37,7 +37,7 @@ if (result == 0) {
|
||||
auto result = findRecord( /* arguments */ );
|
||||
|
||||
if (result == nullptr) {
|
||||
...
|
||||
…
|
||||
}
|
||||
````
|
||||
这就没有任何歧义:`result`的结果一定是指针类型。
|
||||
@ -50,22 +50,22 @@ bool f3(Widget* pw); //调用
|
||||
````
|
||||
如果这样传递空指针:
|
||||
````cpp
|
||||
std::mutex f1m, f2m, f3m; //用于f1,f2,f3函数的互斥量
|
||||
std::mutex f1m, f2m, f3m; //用于f1,f2,f3函数的互斥量
|
||||
|
||||
using MuxGuard = //C++11的typedef,参见Item9
|
||||
using MuxGuard = //C++11的typedef,参见Item9
|
||||
std::lock_guard<std::mutex>;
|
||||
...
|
||||
…
|
||||
|
||||
{
|
||||
MuxGuard g(f1m); //为f1m上锁
|
||||
auto result = f1(0); //向f1传递0作为空指针
|
||||
} //解锁
|
||||
...
|
||||
…
|
||||
{
|
||||
MuxGuard g(f2m); //为f2m上锁
|
||||
auto result = f2(NULL); //向f2传递NULL作为空指针
|
||||
} //解锁
|
||||
...
|
||||
…
|
||||
{
|
||||
MuxGuard g(f3m); //为f3m上锁
|
||||
auto result = f3(nullptr); //向f3传递nullptr作为空指针
|
||||
|
@ -21,7 +21,7 @@ using UPtrMapSS =
|
||||
````cpp
|
||||
//FP是一个指向函数的指针的同义词,它指向的函数带有
|
||||
//int和const std::string&形参,不返回任何东西
|
||||
typedef void (*FP)(int, const std::string&); //typedef
|
||||
typedef void (*FP)(int, const std::string&); //typedef
|
||||
|
||||
//含义同上
|
||||
using FP = void (*)(int, const std::string&); //别名声明
|
||||
@ -52,8 +52,8 @@ MyAllocList<Widget>::type lw; //用户代码
|
||||
template<typename T>
|
||||
class Widget { //Widget<T>含有一个
|
||||
private: //MyAllocLIst<T>对象
|
||||
typename MyAllocList<T>::type list; //作为数据成员
|
||||
...
|
||||
typename MyAllocList<T>::type list; //作为数据成员
|
||||
…
|
||||
};
|
||||
````
|
||||
这里`MyAllocList<T>::type`使用了一个类型,这个类型依赖于模板参数`T`。因此`MyAllocList<T>::type`是一个依赖类型(*dependent type*),在C++很多讨人喜欢的规则中的一个提到必须要在依赖类型名前加上`typename`。
|
||||
@ -62,13 +62,13 @@ private: //MyAllocLIst<T>对象
|
||||
|
||||
````cpp
|
||||
template<typename T>
|
||||
using MyAllocList = std::list<T, MyAlloc<T>>; //同之前一样
|
||||
using MyAllocList = std::list<T, MyAlloc<T>>; //同之前一样
|
||||
|
||||
template<typename T>
|
||||
class Widget {
|
||||
private:
|
||||
MyAllocList<T> list; //没有“typename”
|
||||
... //没有“::type”
|
||||
… //没有“::type”
|
||||
};
|
||||
````
|
||||
对你来说,`MyAllocList<T>`(使用了模板别名声明的版本)可能看起来和`MyAllocList<T>::type`(使用`typedef`的版本)一样都应该依赖模板参数`T`,但是你不是编译器。当编译器处理`Widget`模板时遇到`MyAllocList<T>`(使用模板别名声明的版本),它们知道`MyAllocList<T>`是一个类型名,因为`MyAllocList`是一个别名模板:它**一定**是一个类型名。因此`MyAllocList<T>`就是一个**非依赖类型**(*non-dependent type*),就不需要也不允许使用`typename`修饰符。
|
||||
@ -78,7 +78,7 @@ private:
|
||||
举个例子,一个误入歧途的人可能写出这样的代码:
|
||||
|
||||
````cpp
|
||||
class Wine { ... };
|
||||
class Wine { … };
|
||||
|
||||
template<> //当T是Wine
|
||||
class MyAllocList<Wine> { //特化MyAllocList
|
||||
@ -87,7 +87,7 @@ private:
|
||||
{ White, Red, Rose }; //"enum class"
|
||||
|
||||
WineType type; //在这个类中,type是
|
||||
... //一个数据成员!
|
||||
… //一个数据成员!
|
||||
};
|
||||
````
|
||||
就像你看到的,`MyAllocList<Wine>::type`不是一个类型。如果`Widget`使用`Wine`实例化,在`Widget`模板中的`MyAllocList<Wine>::type`将会是一个数据成员,不是一个类型。在`Widget`模板内,`MyAllocList<T>::type`是否表示一个类型取决于`T`是什么,这就是为什么编译器会坚持要求你在前面加上`typename`。
|
||||
@ -108,7 +108,7 @@ std::add_lvalue_reference<T>::type //从T中产出T&
|
||||
关于为什么这么实现是有历史原因的,但是我们跳过它(我认为太无聊了),因为标准委员会没有及时认识到别名声明是更好的选择,所以直到C++14它们才提供了使用别名声明的版本。这些别名声明有一个通用形式:对于C++11的类型转换`std::`transformation`<T>::type`在C++14中变成了`std::`transformation`_t`。举个例子或许更容易理解:
|
||||
|
||||
````cpp
|
||||
std::remove_const<T>::type //C++11: const T → T
|
||||
std::remove_const<T>::type //C++11: const T → T
|
||||
std::remove_const_t<T> //C++14 等价形式
|
||||
|
||||
std::remove_reference<T>::type //C++11: T&/T&& → T
|
||||
|
@ -16,7 +16,7 @@ std::unique_ptr<T> make_unique(Ts&&... params)
|
||||
|
||||
`std::make_unique`和`std::make_shared`是三个**`make`函数**中的两个:接收任意的多参数集合,完美转发到构造函数去动态分配一个对象,然后返回这个指向这个对象的指针。第三个`make`函数是`std::allocate_shared`。它行为和`std::make_shared`一样,只不过第一个参数是用来动态分配内存的*allocator*对象。
|
||||
|
||||
即使是对使用和不使用`make`函数创建智能指针的最简单比较,也揭示了为什么最好使用`make`函数的第一个原因。例如:
|
||||
即使通过用和不用`make`函数来创建智能指针的一个小小比较,也揭示了为何使用`make`函数更好的第一个原因。例如:
|
||||
|
||||
```c++
|
||||
auto upw1(std::make_unique<Widget>()); //使用make函数
|
||||
|
@ -1,54 +1,56 @@
|
||||
# CHAPTER 5 RValue References, Move Semantics and Perfect Forwarding
|
||||
# 第5章 右值引用,移动语意,完美转发
|
||||
|
||||
当你第一次了解到**移动语义**和**完美转发**的时候,它们看起来非常直观:
|
||||
**CHAPTER 5 RValue References, Move Semantics and Perfect Forwarding**
|
||||
|
||||
- **移动语义**使编译器有可能用廉价的移动操作来代替昂贵的复制操作。正如复制构造函数和复制赋值操作符给了你赋值对象的权利一样,移动构造函数和移动赋值操作符也给了控制移动语义的权利。移动语义也允许创建**只可移动**(move-only)的类型,例如`std::unique_ptr`, `std::future` 和 `std::thread`。
|
||||
当你第一次了解到移动语义(*move semantics*)和完美转发(*perfect forwarding*)的时候,它们看起来非常直观:
|
||||
|
||||
- **完美转发**使接收任意数量参数的函数模板成为可能,它可以将参数转发到其他的函数,使目标函数接收到的参数与被传递给转发函数的参数保持一致。
|
||||
- **移动语义**使编译器有可能用廉价的移动操作来代替昂贵的拷贝操作。正如拷贝构造函数和拷贝赋值操作符给了你控制拷贝语义的权力,移动构造函数和移动赋值操作符也给了你控制移动语义的权力。移动语义也允许创建只可移动(*move-only*)的类型,例如`std::unique_ptr`,`std::future`和`std::thread`。
|
||||
|
||||
**右值引用**是连接这两个截然不同的概念的胶合剂。它隐藏在语言机制之下,使移动语义和完美转发变得可能。
|
||||
- **完美转发**使接收任意数量实参的函数模板成为可能,它可以将实参转发到其他的函数,使目标函数接收到的实参与被传递给转发函数的实参保持一致。
|
||||
|
||||
你对这些特点(features)越熟悉,你就越会发现,你的初印象只不过是冰山一角。移动语义、完美转发和右值引用的世界比它所呈现的更加微妙。
|
||||
举个例子,`std::move`并不移动任何东西,完美转发也并不完美。移动操作并不永远比复制操作更廉价;即便如此,它也并不总是像你期望的那么廉价。而且,它也并不总是被调用,即使在当移动操作可用的时候。构造`type&&`也并非总是代表一个右值引用。
|
||||
**右值引用**是连接这两个截然不同的概念的胶合剂。它是使移动语义和完美转发变得可能的基础语言机制。
|
||||
|
||||
无论你挖掘这些特性有多深,它们看起来总是还有更多隐藏起来的部分。幸运的是,它们的深度总是有限的。本章将会带你到最基础的部分。一旦到达,`C++11`的这部分特性将会具有非常大的意义。比如,你会掌握`std::move`和`sd::forward`的惯用法。你能够对`type&&`的歧义性质感到舒服。你会理解移动操作的令人惊奇的不同代价的背后真相。这些片段都会豁然开朗。在这一点上,你会重新回到一开始的状态,因为移动语义、完美转发和右值引用都会又一次显得直截了当。但是这一次,它们不再使人困惑。
|
||||
你对这些特点越熟悉,你就越会发现,你的初印象只不过是冰山一角。移动语义、完美转发和右值引用的世界比它所呈现的更加微妙。举个例子,`std::move`并不移动任何东西,完美转发也并不完美。移动操作并不永远比复制操作更廉价;即便如此,它也并不总是像你期望的那么廉价。而且,它也并不总是被调用,即使在当移动操作可用的时候。构造“`type&&`”也并非总是代表一个右值引用。
|
||||
|
||||
在本章的这些小节中,非常重要的一点是要牢记**参数**(parameter)永远是**左值**(lValue),即使它的类型是一个右值引用。比如,假设
|
||||
```
|
||||
无论你挖掘这些特性有多深,它们看起来总是还有更多隐藏起来的部分。幸运的是,它们的深度总是有限的。本章将会带你到最基础的部分。一旦到达,C++11的这部分特性将会具有非常大的意义。比如,你会掌握`std::move`和`std::forward`的惯用法。你能够适应“`type&&`”的歧义性质。你会理解移动操作的令人惊奇的不同表现的背后真相。这些片段都会豁然开朗。在这一点上,你会重新回到一开始的状态,因为移动语义、完美转发和右值引用都会又一次显得直截了当。但是这一次,它们不再使人困惑。
|
||||
|
||||
在本章的这些小节中,非常重要的一点是要牢记形参永远是**左值**,即使它的类型是一个右值引用。比如,假设
|
||||
```c++
|
||||
void f(Widget&& w);
|
||||
```
|
||||
参数`w`是一个左值,即使它的类型是一个**Widget**的右值引用(如果这里震惊到你了,请重新回顾从本书第二页开始的关于左值和右值的总览。)
|
||||
形参`w`是一个左值,即使它的类型是一个rvalue-reference-to-`Widget`。(如果这里震惊到你了,请重新回顾从本书[简介](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/Introduction.md)开始的关于左值和右值的总览。)
|
||||
|
||||
## Item 23: 理解std::move和std::forward
|
||||
## 条款二十三:理解`std::move`和`std::forward`
|
||||
|
||||
为了了解`std::move`和`std::forward`,一种有用的方式是从*它们不做什么*这个角度来了解它们。`std::move`不移动(move)任何东西,`std::forward`也不转发(forward)任何东西。在运行期间(run-time),它们不做任何事情。它们不产生任何可执行代码,一字节也没有。
|
||||
**Item 23: Understand `std::move` and `std::forward`**
|
||||
|
||||
`std::move`和`std::forward`仅仅是执行转换(cast)的函数(事实上是函数模板)。`std::move`无条件的将它的参数转换为右值,而`std::forward`只在特定情况满足时下进行转换。
|
||||
它们就是如此。这样的解释带来了一些新的问题,但是从根本上而言,这就是全部内容。
|
||||
为了了解`std::move`和`std::forward`,一种有用的方式是从**它们不做什么**这个角度来了解它们。`std::move`不移动(move)任何东西,`std::forward`也不转发(forward)任何东西。在运行时,它们不做任何事情。它们不产生任何可执行代码,一字节也没有。
|
||||
|
||||
`std::move`和`std::forward`仅仅是执行转换(cast)的函数(事实上是函数模板)。`std::move`无条件的将它的实参转换为右值,而`std::forward`只在特定情况满足时下进行转换。它们就是如此。这样的解释带来了一些新的问题,但是从根本上而言,这就是全部内容。
|
||||
|
||||
为了使这个故事更加的具体,这里是一个C++11的`std::move`的示例实现。它并不完全满足标准细则,但是它已经非常接近了。
|
||||
|
||||
```cpp
|
||||
template <typename T> //in namespace std
|
||||
template<typename T> //在std命名空间
|
||||
typename remove_reference<T>::type&&
|
||||
move(T&& param)
|
||||
{
|
||||
using ReturnType = // alias declaration;
|
||||
typename remove_reference<T>::type&&; // see Item 9
|
||||
using ReturnType = //别名声明,见条款9
|
||||
typename remove_reference<T>::type&&;
|
||||
|
||||
return static_cast<ReturnType>(param);
|
||||
}
|
||||
```
|
||||
|
||||
我为你们高亮了这段代码的两部分(译者注:markdown不支持代码段内高亮。高亮的部分为`move`和`static_cast`)。一个是函数名字,因为函数的返回值非常具有干扰性。而且我不想你们被它搞得晕头转向。另外一个高亮的部分是包含这段函数的本质的转换。正如你所见,`std::move`接受一个对象的引用(准确的说,一个通用引用(universal reference),后见Item 24),返回一个指向同对象的引用。
|
||||
我为你们高亮了这段代码的两部分(译者注:高亮的部分为函数名`move`和`static_cast<ReturnType>(param)`)。一个是函数名字,因为函数的返回值非常具有干扰性,而且我不想你们被它搞得晕头转向。另外一个高亮的部分是包含这段函数的本质的转换。正如你所见,`std::move`接受一个对象的引用(准确的说,一个通用引用(universal reference),见[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)),返回一个指向同对象的引用。
|
||||
|
||||
该函数返回类型的`&&`部分表明`std::move`函数返回的是一个右值引用,但是,正如Item 28所解释的那样,如果类型`T`恰好是一个左值引用,那么`T&&`将会成为一个左值引用。为了避免如此,类型萃取器(type trait,见Item 9)`std::remove_reference`应用到了类型`T`上,因此确保了`&&`被正确的应用到了一个不是引用的类型上。这保证了`std::move`返回的真的是右值引用,这很重要,因为函数返回的右值引用是右值(rvalues)。因此,`std::move`将它的参数转换为一个右值,这就是它的全部作用。
|
||||
该函数返回类型的`&&`部分表明`std::move`函数返回的是一个右值引用,但是,正如[Item28](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md)所解释的那样,如果类型`T`恰好是一个左值引用,那么`T&&`将会成为一个左值引用。为了避免如此,*type trait*(见[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md))`std::remove_reference`应用到了类型`T`上,因此确保了`&&`被正确的应用到了一个不是引用的类型上。这保证了`std::move`返回的真的是右值引用,这很重要,因为函数返回的右值引用是右值。因此,`std::move`将它的实参转换为一个右值,这就是它的全部作用。
|
||||
|
||||
此外,`std::move`在C++14中可以被更简单地实现。多亏了函数返回值类型推导(见Item 3)和标准库的模板别名`std::remove_reference_t`(见Item 9),`std::move`可以这样写:
|
||||
此外,`std::move`在C++14中可以被更简单地实现。多亏了函数返回值类型推导(见[Item3](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md))和标准库的模板别名`std::remove_reference_t`(见[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md)),`std::move`可以这样写:
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
decltype(auto) move(T&& param) //C++14;still in namesapce std
|
||||
template<typename T>
|
||||
decltype(auto) move(T&& param) //C++14,仍然在std命名空间
|
||||
{
|
||||
using ReturnType = remove_referece_t<T>&&;
|
||||
return static_cast<ReturnType>(param);
|
||||
@ -57,161 +59,135 @@ decltype(auto) move(T&& param) //C++14;still in namesapce std
|
||||
|
||||
看起来更简单,不是吗?
|
||||
|
||||
因为`std::move`除了转换它的参数到右值以外什么也不做,有一些提议说它的名字叫`rvalue_cast`可能会更好。虽然可能确实是这样,但是它的名字已经是`std::move`,所以记住`std::move`做什么和不做什么很重要。它其实并不移动任何东西。
|
||||
因为`std::move`除了转换它的实参到右值以外什么也不做,有一些提议说它的名字叫`rvalue_cast`之类可能会更好。虽然可能确实是这样,但是它的名字已经是`std::move`,所以记住`std::move`做什么和不做什么很重要。它只进行转换,不移动任何东西。
|
||||
|
||||
当然,右值本来就是移动操作的侯选者,所以对一个对象使用`std::move`就是告诉编译器,这个对象很适合被移动。所以这就是为什么`std::move`叫现在的名字: 更容易指定可以被移动的对象。
|
||||
当然,右值本来就是移动操作的候选者,所以对一个对象使用`std::move`就是告诉编译器,这个对象很适合被移动。所以这就是为什么`std::move`叫现在的名字:更容易指定可以被移动的对象。
|
||||
|
||||
事实上,右值只不过**经常**是移动操作的候选者。假设你有一个类,它用来表示一段注解。这个类的构造函数接受一个包含有注解的`std::string`作为参数,然后它复制该参数到类的数据成员(data member)。假设你了解Item 41,你声明一个值传递(by value)的参数:
|
||||
事实上,右值只不过**经常**是移动操作的候选者。假设你有一个类,它用来表示一段注解。这个类的构造函数接受一个包含有注解的`std::string`作为形参,然后它复制该形参到数据成员。假设你了解[Item41](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md),你声明一个值传递的形参:
|
||||
|
||||
```cpp
|
||||
class Annotation {
|
||||
public:
|
||||
explicit Annotation(std::string text); //将会被复制的参数
|
||||
... //如同 Item 41,
|
||||
explicit Annotation(std::string text); //将会被复制的形参,
|
||||
… //如同条款41所说,
|
||||
}; //值传递
|
||||
```
|
||||
|
||||
但是`Annotation`类的构造函数仅仅是需要读取参数`text`的值,它并不需要修改它。为了和历史悠久的传统:能使用`const`就使用`const`保持一致,你修订了你的声明以使`text`变成`const`,
|
||||
但是`Annotation`类的构造函数仅仅是需要读取`text`的值,它并不需要修改它。为了和历史悠久的传统:能使用`const`就使用`const`保持一致,你修订了你的声明以使`text`变成`const`:
|
||||
|
||||
```cpp
|
||||
class Annotation {
|
||||
public:
|
||||
explicit Annotation(const std::string text);
|
||||
...
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
当复制参数`text`到一个数据成员的时候,为了避免一次复制操作的代价,你仍然记得来自Item 41的建议,把`std::move`应用到参数`text`上,因此产生一个右值,
|
||||
当复制`text`到一个数据成员的时候,为了避免一次复制操作的代价,你仍然记得来自[Item41](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md)的建议,把`std::move`应用到`text`上,因此产生一个右值:
|
||||
|
||||
```cpp
|
||||
class Annotation {
|
||||
public:
|
||||
explicit Annotation(const std::string text)
|
||||
:value(std::move(text)) //"move" text到value上;这段代码执行起来
|
||||
//并不如看起来那样
|
||||
{...}
|
||||
...
|
||||
:value(std::move(text)) //“移动”text到value里;这段代码执行起来
|
||||
{ … } //并不是看起来那样
|
||||
|
||||
…
|
||||
|
||||
private:
|
||||
std::string value;
|
||||
private:
|
||||
std::string value;
|
||||
};
|
||||
```
|
||||
|
||||
这段代码可以编译,可以链接,可以运行。这段代码将数据成员`value`设置为`text`的值。这段代码与你期望中的完美实现的唯一区别,是`text`并不是被移动到`value`,而是被**复制**。诚然,`text`通过`std::move`被转换到右值,但是`text`被声明为`const std::string`,所以在转换之前,`text`是一个左值的`const std::string`,而转换的结果是一个右值的`const std::string`,但是纵观全程,`const`属性一直保留。
|
||||
这段代码可以编译,可以链接,可以运行。这段代码将数据成员`value`设置为`text`的值。这段代码与你期望中的完美实现的唯一区别,是`text`并不是被移动到`value`,而是被**拷贝**。诚然,`text`通过`std::move`被转换到右值,但是`text`被声明为`const std::string`,所以在转换之前,`text`是一个左值的`const std::string`,而转换的结果是一个右值的`const std::string`,但是纵观全程,`const`属性一直保留。
|
||||
|
||||
当编译器决定哪一个`std::string`的构造函数被构造时,考虑它的作用,将会有两种可能性。
|
||||
当编译器决定哪一个`std::string`的构造函数被调用时,考虑它的作用,将会有两种可能性:
|
||||
|
||||
```cpp
|
||||
class string { //std::string事实上是
|
||||
public: //std::basic_string<char>的类型别名
|
||||
...
|
||||
string(const string& rhs); //复制构造函数
|
||||
public: //std::basic_string<char>的类型别名
|
||||
…
|
||||
string(const string& rhs); //拷贝构造函数
|
||||
string(string&& rhs); //移动构造函数
|
||||
}
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
在类`Annotation`的构造函数的成员初始化列表(member initialization list)中,`std::move(text)`的结构是一个`const std::string`的右值。这个右值不能被传递给`std::string`的移动构造函数,因为移动构造函数只接受一个指向非常量(non-const)`std::string`的右值引用。然而,该右值却可以被传递给`std::string`的复制构造函数,因为指向常量的左值引用允许被绑定到一个常量右值上。因此,`std::string`在成员初始化的过程中调用了**复制构造函数**,即使`text`已经被转换成了右值。这样是为了确保维持常量属性的正确性。从一个对象中移动(Moving)出某个值通常代表着修改该对象,所以语言不允许常量对象被传递给可以修改他们的函数(例如移动构造函数)。
|
||||
在类`Annotation`的构造函数的成员初始化列表中,`std::move(text)`的结果是一个`const std::string`的右值。这个右值不能被传递给`std::string`的移动构造函数,因为移动构造函数只接受一个指向**non-`const`**的`std::string`的右值引用。然而,该右值却可以被传递给`std::string`的拷贝构造函数,因为lvalue-reference-to-`const`允许被绑定到一个`const`右值上。因此,`std::string`在成员初始化的过程中调用了**拷贝**构造函数,即使`text`已经被转换成了右值。这样是为了确保维持`const`属性的正确性。从一个对象中移动出某个值通常代表着修改该对象,所以语言不允许`const`对象被传递给可以修改他们的函数(例如移动构造函数)。
|
||||
|
||||
从这个例子中,可以总结出两点。第一,不要在你希望能移动对象的时候,声明他们为常量。对常量对象的移动请求会悄无声息的被转化为复制操作。第二点,`std::move`不仅不移动任何东西,而且它也不保证它执行转换的对象可以被移动。关于`std::move`,你能确保的唯一一件事就是将它应用到一个对象上,你能够得到一个右值。
|
||||
从这个例子中,可以总结出两点。第一,不要在你希望能移动对象的时候,声明他们为`const`。对`const`对象的移动请求会悄无声息的被转化为拷贝操作。第二点,`std::move`不仅不移动任何东西,而且它也不保证它执行转换的对象可以被移动。关于`std::move`,你能确保的唯一一件事就是将它应用到一个对象上,你能够得到一个右值。
|
||||
|
||||
关于`std::forward`的故事与`std::move`是相似的,但是与`std::move`总是**无条件**的将它的参数转换为右值不同,`std::forward`只有在满足一定条件的情况下才执行转换。`std::forward`是**有条件**的转换。要明白什么时候它执行转换,什么时候不,想想`std::forward`的典型用法。
|
||||
最常见的情景是一个模板函数,接收一个通用引用参数(universal reference parameter),并将它传递给另外的函数:
|
||||
关于`std::forward`的故事与`std::move`是相似的,但是与`std::move`总是**无条件**的将它的实参为右值不同,`std::forward`只有在满足一定条件的情况下才执行转换。`std::forward`是**有条件**的转换。要明白什么时候它执行转换,什么时候不,想想`std::forward`的典型用法。最常见的情景是一个模板函数,接收一个通用引用形参,并将它传递给另外的函数:
|
||||
|
||||
```cpp
|
||||
void process(const Widget& lvalArg); //左值处理
|
||||
void process(Widget&& rvalArg); //右值处理
|
||||
void process(const Widget& lvalArg); //处理左值
|
||||
void process(Widget&& rvalArg); //处理右值
|
||||
|
||||
template <typename T> //用以转发参数到process的模板
|
||||
template<typename T> //用以转发param到process的模板
|
||||
void logAndProcess(T&& param)
|
||||
{
|
||||
auto now = //获取现在时间
|
||||
auto now = //获取现在时间
|
||||
std::chrono::system_clock::now();
|
||||
makeLogEntry("calling 'process'",now);
|
||||
|
||||
makeLogEntry("Calling 'process'", now);
|
||||
process(std::forward<T>(param));
|
||||
}
|
||||
```
|
||||
|
||||
考虑两次对`logAndProcess`的调用,一次左值为参数,一次右值为参数,
|
||||
考虑两次对`logAndProcess`的调用,一次左值为实参,一次右值为实参:
|
||||
|
||||
```cpp
|
||||
Widget w;
|
||||
|
||||
logAndProcess(w); //call with lvalue
|
||||
logAndProcess(std::move(w)); //call with rvalue
|
||||
logAndProcess(w); //用左值调用
|
||||
logAndProcess(std::move(w)); //用右值调用
|
||||
```
|
||||
|
||||
在`logAndProcess`函数的内部,参数`param`被传递给函数`process`。函数`process`分别对左值和右值参数做了重载。当我们使用左值来调用`logAndProcess`时,自然我们期望该左值被当作左值转发给`process`函数,而当我们使用右值来调用`logAndProcess`函数时,我们期望`process`函数的右值重载版本被调用。
|
||||
在`logAndProcess`函数的内部,形参`param`被传递给函数`process`。函数`process`分别对左值和右值做了重载。当我们使用左值来调用`logAndProcess`时,自然我们期望该左值被当作左值转发给`process`函数,而当我们使用右值来调用`logAndProcess`函数时,我们期望`process`函数的右值重载版本被调用。
|
||||
|
||||
但是参数`param`,正如所有的其他函数参数一样,是一个左值。每次在函数`logAndProcess`内部对函数`process`的调用,都会因此调用函数`process`的左值重载版本。为防如此,我们需要一种机制(mechanism) : 当且仅当传递给函数`logAndProcess`的用以初始化参数`param`的值是一个右值时,参数`param`会被转换为有一个右值。这就是为什么`std::forward`是一个**有条件**的转换:它只把由右值初始化的参数,转换为右值。
|
||||
但是`param`,正如所有的其他函数形参一样,是一个左值。每次在函数`logAndProcess`内部对函数`process`的调用,都会因此调用函数`process`的左值重载版本。为防如此,我们需要一种机制:当且仅当传递给函数`logAndProcess`的用以初始化`param`的实参是一个右值时,`param`会被转换为一个右值。这就是`std::forward`做的事情。这就是为什么`std::forward`是一个**有条件**的转换:它的实参用右值初始化时,转换为一个右值。
|
||||
|
||||
你也许会想知道`std::forward`是怎么知道它的参数是否是被一个右值初始化的。举个例子,在上述代码中,`std::forward`是怎么分辨参数`param`是被一个左值还是右值初始化的? 简短的说,该信息藏在函数`logAndProcess`的模板参数`T`中。该参数被传递给了函数`std::forward`,它解开了含在其中的信息。该机制工作的细节可以查询 Item 28.
|
||||
你也许会想知道`std::forward`是怎么知道它的实参是否是被一个右值初始化的。举个例子,在上述代码中,`std::forward`是怎么分辨`param`是被一个左值还是右值初始化的? 简短的说,该信息藏在函数`logAndProcess`的模板参数`T`中。该参数被传递给了函数`std::forward`,它解开了含在其中的信息。该机制工作的细节可以查询[Item28](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md)。
|
||||
|
||||
考虑到`std::move`和`std::forward`都可以归结于**转换**,他们唯一的区别就是`std::move`总是执行转换,而`std::forward`偶尔为之。你可能会问是否我们可以免于使用`std::move`而在任何地方只使用`std::forward`。 从纯技术的角度,答案是yes: `std::forward`是可以完全胜任,`std::move`并非必须。当然,其实两者中没有哪一个函数是**真的必须**的,因为我们可以到处直接写转换代码,但是我希望我们能同意:这将相当的,嗯,让人恶心。
|
||||
考虑到`std::move`和`std::forward`都可以归结于转换,它们唯一的区别就是`std::move`总是执行转换,而`std::forward`偶尔为之。你可能会问是否我们可以免于使用`std::move`而在任何地方只使用`std::forward`。 从纯技术的角度,答案是yes:`std::forward`是可以完全胜任,`std::move`并非必须。当然,其实两者中没有哪一个函数是**真的必须**的,因为我们可以到处直接写转换代码,但是我希望我们能同意:这将相当的,嗯,让人恶心。
|
||||
|
||||
`std::move`的吸引力在于它的便利性: 减少了出错的可能性,增加了代码的清晰程度。考虑一个类,我们希望统计有多少次移动构造函数被调用了。我们只需要一个静态的计数器(static counter),它会在移动构造的时候自增。假设在这个类中,唯一一个非静态的数据成员是`std::string`,一种经典的移动构造函数(例如,使用std::move)可以被实现如下:
|
||||
`std::move`的吸引力在于它的便利性:减少了出错的可能性,增加了代码的清晰程度。考虑一个类,我们希望统计有多少次移动构造函数被调用了。我们只需要一个`static`的计数器,它会在移动构造的时候自增。假设在这个类中,唯一一个非静态的数据成员是`std::string`,一种经典的移动构造函数(即,使用`std::move`)可以被实现如下:
|
||||
|
||||
```cpp
|
||||
class Widget{
|
||||
class Widget {
|
||||
public:
|
||||
Widget(Widget&& rhs)
|
||||
: s(std::move(rhs.s))
|
||||
{
|
||||
++moveCtorCalls;
|
||||
}
|
||||
{ ++moveCtorCalls; }
|
||||
|
||||
…
|
||||
|
||||
private:
|
||||
static std::size_t moveCtorCalls;
|
||||
std::string s;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
如果要用`std::forward`来达成同样的效果,代码可能会看起来像
|
||||
如果要用`std::forward`来达成同样的效果,代码可能会看起来像:
|
||||
|
||||
```cpp
|
||||
class Widget{
|
||||
public:
|
||||
Widget(Widget&& rhs) //不自然,不合理的实现
|
||||
: s(std::forward<std::string>(rhs.s))
|
||||
{
|
||||
++moveCtorCalls;
|
||||
}
|
||||
...
|
||||
{ ++moveCtorCalls; }
|
||||
|
||||
…
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
注意,第一,`std::move`只需要一个函数参数(rhs.s),而`std::forward`不但需要一个函数参数(rhs.s),还需要一个模板类型参数`std::string`。其次,我们转发给`std::forward`的参数类型应当是一个**非引用**(non-reference),因为传递的参数应该是一个右值(见 Item 28)。 同样,这意味着`std::move`比起`std::forward`来说需要打更少的字,并且免去了传递一个表示我们正在传递一个右值的类型参数。同样,它根绝了我们传递错误类型的可能性,(例如,`std::string&`可能导致数据成员`s`被复制而不是被移动构造)。
|
||||
注意,第一,`std::move`只需要一个函数实参(`rhs.s`),而`std::forward`不但需要一个函数实参(`rhs.s`),还需要一个模板类型实参`std::string`。其次,我们传递给`std::forward`的类型应当是一个non-reference,因为惯例是传递的实参应该是一个右值(见[Item28](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md))。同样,这意味着`std::move`比起`std::forward`来说需要打更少的字,并且免去了传递一个表示我们正在传递一个右值的类型实参。同样,它根绝了我们传递错误类型的可能性(例如,`std::string&`可能导致数据成员`s`被复制而不是被移动构造)。
|
||||
|
||||
更重要的是,`std::move`的使用代表着无条件向右值的转换,而使用`std::forward`只对绑定了右值的引用进行到右值转换。这是两种完全不同的动作。前者是典型地为了移动操作,而后者只是传递(亦作转发)一个对象到另外一个函数,保留它原有的左值属性或右值属性。因为这些动作实在是差异太大,所以我们拥有两个不同的函数(以及函数名)来区分这些动作。
|
||||
更重要的是,`std::move`的使用代表着无条件向右值的转换,而使用`std::forward`只对绑定了右值的引用进行到右值转换。这是两种完全不同的动作。前者是典型地为了移动操作,而后者只是传递(亦为转发)一个对象到另外一个函数,保留它原有的左值属性或右值属性。因为这些动作实在是差异太大,所以我们拥有两个不同的函数(以及函数名)来区分这些动作。
|
||||
|
||||
**记住**:
|
||||
**请记住:**
|
||||
|
||||
+ `std::move`执行到右值的无条件的转换,但就自身而言,它不移动任何东西。
|
||||
+ `std::forward`只有当它的参数被绑定到一个右值时,才将参数转换为右值。
|
||||
+ `std::move`和`std::forward`在运行期什么也不做。
|
||||
|
||||
### 参考问题(非书籍内容)
|
||||
|
||||
关于move语义的解释
|
||||
https://stackoverflow.com/questions/36827900/what-makes-moving-objects-faster-than-copying
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,196 +1,154 @@
|
||||
## 区分通用引用与右值引用
|
||||
## 条款二十四:区分通用引用与右值引用
|
||||
|
||||
据说,真相使人自由,然而在特定的环境下,一个精心挑选的谎言也同样使人解放。这一节就是这样一个谎言。因为我们在和软件打交道,然而,让我们避开“谎言(lie)”这个词,不妨说,本节包含了一种“**抽象**(abstraction)”。
|
||||
**Item 24: Distinguish universal references from rvalue references**
|
||||
|
||||
为了声明一个指向某个类型T的右值引用(Rvalue Reference), 你写下了`T&&`。由此,一个合理的假设是,当你看到一个`T&&`出现在源码中,你看到的是一个右值引用。唉,事情并不如此简单:
|
||||
据说,真相使人自由,然而在特定的环境下,一个精心挑选的谎言也同样使人解放。这一条款就是这样一个谎言。因为我们在和软件打交道,然而,让我们避开“谎言(lie)”这个词,不妨说,本条款包含了一种“抽象(abstraction)”。
|
||||
|
||||
为了声明一个指向某个类型`T`的右值引用,你写下了`T&&`。由此,一个合理的假设是,当你看到一个“`T&&`”出现在源码中,你看到的是一个右值引用。唉,事情并不如此简单:
|
||||
|
||||
```cpp
|
||||
void f(Widget&& param); //右值引用
|
||||
Widget&& var1 = Widget(); //右值引用
|
||||
auto&& var2 = var1; //不是右值引用
|
||||
void f(Widget&& param); //右值引用
|
||||
Widget&& var1 = Widget(); //右值引用
|
||||
auto&& var2 = var1; //不是右值引用
|
||||
|
||||
template <typename T>
|
||||
void f(std::vector<T>&& param); //右值引用
|
||||
template<typename T>
|
||||
void f(std::vector<T>&& param); //右值引用
|
||||
|
||||
template <typename T>
|
||||
void f(T&& param); //不是右值引用
|
||||
template<typename T>
|
||||
void f(T&& param); //不是右值引用
|
||||
```
|
||||
|
||||
事实上,`T&&`有两种不同的意思。第一种,当然是右值引用。这种引用表现得正如你所期待的那样: 它们只绑定到右值上,并且它们主要的存在原因就是为了声明某个对象可以被移动。
|
||||
事实上,“`T&&`”有两种不同的意思。第一种,当然是右值引用。这种引用表现得正如你所期待的那样:它们只绑定到右值上,并且它们主要的存在原因就是为了声明某个对象可以被移动。
|
||||
|
||||
`T&&`的第二层意思,是它既可以是一个右值引用,也可以是一个左值引用。这种引用在源码里看起来像右值引用(也即`T&&`),但是它们可以表现得它们**像是**左值引用(也即`T&`)。它们的二重性(dual nature)使它们既可以绑定到右值上(就像右值引用),也可以绑定到左值上(就像左值引用)。 此外,它们还可以绑定到常量(const)和非常量(non-const)的对象上,也可以绑定到`volatile`和`non-volatile`的对象上,甚至可以绑定到即`const`又`volatile`的对象上。它们可以绑定到几乎任何东西。这种空前灵活的引用值得拥有自己的名字。我把它叫做**通用引用**(universal references)。(注: Item 25解释了`std::forward`几乎总是可以应用到通用引用上,并且在这本书即将出版之际,一些C++社区的成员已经开始将这种通用引用称之为**转发引用**(forwarding references))。
|
||||
“`T&&`”的第二层意思,是它既可以是一个右值引用,也可以是一个左值引用。这种引用在源码里看起来像右值引用(也即“`T&&`”),但是它们可以表现得像是左值引用(也即“`T&`”)一样。它们的二重性使它们既可以绑定到右值上(就像右值引用),也可以绑定到左值上(就像左值引用)。 此外,它们还可以绑定到`const`或者non-`const`的对象上,也可以绑定到`volatile`或者non-`volatile`的对象上,甚至可以绑定到既`const`又`volatile`的对象上。它们可以绑定到几乎任何东西。这种空前灵活的引用值得拥有自己的名字。我把它叫做**通用引用**(*universal references*)。([Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)解释了`std::forward`几乎总是可以应用到通用引用上,并且在这本书即将出版之际,一些C++社区的成员已经开始将这种通用引用称之为**转发引用**(*forwarding references*))。
|
||||
|
||||
在两种情况下会出现通用引用。最常见的一种是函数模板参数,正如在之前的示例代码中所出现的例子:
|
||||
在两种情况下会出现通用引用。最常见的一种是函数模板形参,正如在之前的示例代码中所出现的例子:
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
void f(T&& param); //param是一个通用引用
|
||||
template<typename T>
|
||||
void f(T&& param); //param是一个通用引用
|
||||
```
|
||||
|
||||
第二种情况是`auto`声明符,包含从以上示例中取得的这个例子:
|
||||
第二种情况是`auto`声明符,它是从以上示例中拿出的:
|
||||
|
||||
```cpp
|
||||
auto&& val2 = var1; //var2是一个通用引用
|
||||
auto&& val2 = var1; //var2是一个通用引用
|
||||
```
|
||||
|
||||
这两种情况的共同之处就是都存在**类型推导(type deduction)**。在模板`f`的内部,参数`param`的类型需要被推导,而在变量`var2`的声明中,`var2`的类型也需要被推导。同以下的例子相比较(同样来自于上面的示例代码),下面的例子不带有类型推导。如果你看见`T&&`不带有类型推导,那么你看到的就是一个右值引用。
|
||||
这两种情况的共同之处就是都存在**类型推导**(*type deduction*)。在模板`f`的内部,`param`的类型需要被推导,而在变量`var2`的声明中,`var2`的类型也需要被推导。同以下的例子相比较(同样来自于上面的示例代码),下面的例子不带有类型推导。如果你看见“`T&&`”不带有类型推导,那么你看到的就是一个右值引用:
|
||||
|
||||
```cpp
|
||||
void f(Widget&& param); //没有类型推导
|
||||
void f(Widget&& param); //没有类型推导,
|
||||
//param是一个右值引用
|
||||
Widget&& var1 = Widget(); //没有类型推导
|
||||
Widget&& var1 = Widget(); //没有类型推导,
|
||||
//var1是一个右值引用
|
||||
```
|
||||
|
||||
因为通用引用是引用,所以他们必须被初始化。一个通用引用的初始值决定了它是代表了右值引用还是左值引用。如果初始值是一个右值,那么通用引用就会是对应的右值引用,如果初始值是一个左值,那么通用引用就会是一个左值引用。对那些是函数参数的通用引用来说,初始值在调用函数的时候被提供:
|
||||
因为通用引用是引用,所以它们必须被初始化。一个通用引用的初始值决定了它是代表了右值引用还是左值引用。如果初始值是一个右值,那么通用引用就会是对应的右值引用,如果初始值是一个左值,那么通用引用就会是一个左值引用。对那些是函数形参的通用引用来说,初始值在调用函数的时候被提供:
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
template<typename T>
|
||||
void f(T&& param); //param是一个通用引用
|
||||
|
||||
Widget w;
|
||||
f(w); //传递给函数f一个左值;参数param的类型
|
||||
//将会是Widget&,也即左值引用
|
||||
f(w); //传递给函数f一个左值;param的类型
|
||||
//将会是Widget&,也即左值引用
|
||||
|
||||
f(std::move(w)); //传递给f一个右值;参数param的类型会是
|
||||
//Widget&&,即右值引用
|
||||
f(std::move(w)); //传递给f一个右值;param的类型会是
|
||||
//Widget&&,即右值引用
|
||||
```
|
||||
|
||||
对一个通用引用而言,类型推导是必要的,但是它还不够。声明引用的格式必须正确,并且这种格式是被限制的。它必须是准确的`T&&`。再看看之前我们已经看过的代码示例:
|
||||
对一个通用引用而言,类型推导是必要的,但是它还不够。声明引用的**格式**必须正确,并且这种格式是被限制的。它必须是准确的“`T&&`”。再看看之前我们已经看过的代码示例:
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
void f(std::vector<T>&& param); //param是一个右值引用
|
||||
```
|
||||
|
||||
当函数`f`被调用的时候,类型`T`会被推导(除非调用者显式地指定它,这种边缘情况我们不考虑)。但是参数`param`的类型声明并不是`T&&`,而是一个`std::vector<T>&&`。这排除了参数`param`是一个通用引用的可能性。`param`因此是一个右值引用——当你向函数`f`传递一个左值时,你的编译器将会开心地帮你确认这一点:
|
||||
当函数`f`被调用的时候,类型`T`会被推导(除非调用者显式地指定它,这种边缘情况我们不考虑)。但是`param`的类型声明并不是`T&&`,而是一个`std::vector<T>&&`。这排除了`param`是一个通用引用的可能性。`param`因此是一个右值引用——当你向函数`f`传递一个左值时,你的编译器将会开心地帮你确认这一点:
|
||||
|
||||
```cpp
|
||||
std::vector<int> v;
|
||||
f(v); //错误!不能将左值绑定到右值引用
|
||||
std::vector<int> v;
|
||||
f(v); //错误!不能将左值绑定到右值引用
|
||||
```
|
||||
|
||||
即使是出现一个简单的`const`修饰符,也足以使一个引用失去成为通用引用的资格:
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
void f(const T&& param); //param是一个右值引用
|
||||
template <typename T>
|
||||
void f(const T&& param); //param是一个右值引用
|
||||
```
|
||||
如果你在一个模板里面看见了一个函数参数类型为`T&&`,你也许觉得你可以假定它是一个通用引用。错!这是由于在模板内部并不保证一定会发生类型推导。考虑如下`push_back`成员函数,来自`std::vector`:
|
||||
如果你在一个模板里面看见了一个函数形参类型为“`T&&`”,你也许觉得你可以假定它是一个通用引用。错!这是由于在模板内部并不保证一定会发生类型推导。考虑如下`push_back`成员函数,来自`std::vector`:
|
||||
|
||||
```cpp
|
||||
template <class T, class Allocator = allocator<T>> //来自C++标准
|
||||
class vector
|
||||
{
|
||||
public:
|
||||
void push_back(T&& x);
|
||||
...
|
||||
}
|
||||
template<class T, class Allocator = allocator<T>> //来自C++标准
|
||||
class vector
|
||||
{
|
||||
public:
|
||||
void push_back(T&& x);
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
`push_back`函数的参数当然有资格成为一个通用引用,然而,在这里并没有发生类型推导。
|
||||
因为`push_back`在一个特有(particular)的`vector`实例化(instantiation)之前不可能存在,而实例化`vector`时的类型已经决定了`push_back`的声明。也就是说,
|
||||
`push_back`函数的形参当然有一个通用引用的正确形式,然而,在这里并没有发生类型推导。因为`push_back`在有一个特定的`vector`实例之前不可能存在,而实例化`vector`时的类型已经决定了`push_back`的声明。也就是说,
|
||||
|
||||
```cpp
|
||||
std::vector<Widget> v;
|
||||
std::vector<Widget> v;
|
||||
```
|
||||
|
||||
将会导致`std::vector`模板被实例化为以下代码:
|
||||
将会导致`std::vector`模板被实例化为以下代码:
|
||||
|
||||
```cpp
|
||||
class vector<Widget, allocator<Widget>>
|
||||
{
|
||||
public:
|
||||
void push_back(Widget&& x); // 右值引用
|
||||
}
|
||||
class vector<Widget, allocator<Widget>> {
|
||||
public:
|
||||
void push_back(Widget&& x); //右值引用
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
现在你可以清楚地看到,函数`push_back`不包含任何类型推导。`push_back`对于`vector<T>`而言(有两个函数——它被重载了)总是声明了一个类型为指向`T`的右值引用的参数。
|
||||
现在你可以清楚地看到,函数`push_back`不包含任何类型推导。`push_back`对于`vector<T>`而言(有两个函数——它被重载了)总是声明了一个类型为rvalue-reference-to-`T`的形参。
|
||||
|
||||
相反,`std::vector`内部的概念上相似的成员函数`emplace_back`,却确实包含类型推导:
|
||||
|
||||
```cpp
|
||||
template <class T,class Allocator = allocator<T>> //依旧来自C++标准
|
||||
class vector
|
||||
{
|
||||
public:
|
||||
template <class... Args>
|
||||
void emplace_back(Args&&... args);
|
||||
...
|
||||
}
|
||||
template<class T, class Allocator = allocator<T>> //依旧来自C++标准
|
||||
class vector {
|
||||
public:
|
||||
template <class... Args>
|
||||
void emplace_back(Args&&... args);
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
这儿,类型参数(type parameter)`Args`是独立于`vector`的类型参数之外的,所以`Args`会在每次`emplace_back`被调用的时候被推导(Okay, `Args`实际上是一个参数包(parameter pack),而不是一个类型参数,但是为了讨论之利,我们可以把它**当作**是一个类型参数)。
|
||||
这儿,类型参数(*type parameter*)`Args`是独立于`vector`的类型参数`T`之外的,所以`Args`会在每次`emplace_back`被调用的时候被推导。(好吧,`Args`实际上是一个[*parameter pack*](https://en.cppreference.com/w/cpp/language/parameter_pack),而不是一个类型参数,但是为了讨论之利,我们可以把它当作是一个类型参数。)
|
||||
|
||||
虽然函数`emplace_back`的类型参数被命名为`Args`,但是它仍然是一个通用引用,这补充了我之前所说的,通用引用的格式必须是`T&&`。 没有任何规定必须使用名字`T`。举个例子,如下模板接受一个通用引用,但是格式(`type&&`)是正确的,并且参数`param`的类型将会被推导(重复一次,不考虑边缘情况,也即当调用者明确给定参数类型的时候)。
|
||||
虽然函数`emplace_back`的类型参数被命名为`Args`,但是它仍然是一个通用引用,这补充了我之前所说的,通用引用的格式必须是“`T&&`”。 没有任何规定必须使用名字`T`。举个例子,如下模板接受一个通用引用,因为格式(“`type&&`”)是正确的,并且`param`的类型将会被推导(重复一次,不考虑边缘情况,也即当调用者明确给定类型的时候)。
|
||||
|
||||
```cpp
|
||||
template <typename MyTemplateType> //param是通用引用
|
||||
template<typename MyTemplateType> //param是通用引用
|
||||
void someFunc(MyTemplateType&& param);
|
||||
```
|
||||
|
||||
我之前提到,类型为`auto`的变量可以是通用引用。更准确地说,类型声明为`auto&&`的变量是通用引用,因为会发生类型推导,并且它们满足正确的格式要求(`T&&`)。`auto`类型的通用引用不如模板函数参数中的通用引用常见,但是它们在`C++11`中常常突然出现。而它们在`C++14`中出现地更多,因为`C++14`的匿名函数表达式(lambda expressions)可以声明`auto&&`类型的参数。举个例子,如果你想写一个`C++14`标准的匿名函数,来记录任意函数调用花费的时间,你可以这样:
|
||||
我之前提到,类型为`auto`的变量可以是通用引用。更准确地说,类型声明为`auto&&`的变量是通用引用,因为会发生类型推导,并且它们满足正确的格式要求(`T&&`)。`auto`类型的通用引用不如函数模板形参中的通用引用常见,但是它们在C++11中常常突然出现。而它们在C++14中出现得更多,因为C++14的*lambda*表达式可以声明`auto&&`类型的形参。举个例子,如果你想写一个C++14标准的*lambda*表达式,来记录任意函数调用花费的时间,你可以这样:
|
||||
|
||||
```cpp
|
||||
auto timeFuncInvocation =
|
||||
[](auto&& func, auto&&... params) //C++14标准
|
||||
auto timeFuncInvocation =
|
||||
[](auto&& func, auto&&... params) //C++14
|
||||
{
|
||||
start timer;
|
||||
std::forward<decltype(func)>(func)( //对参数params调用func
|
||||
std::forward<decltype(func)>(func)( //对params调用func
|
||||
std::forward<delctype(params)>(params)...
|
||||
);
|
||||
stop timer and record elapsed time;
|
||||
};
|
||||
```
|
||||
|
||||
如果你对位于匿名函数里的`std::forward<decltype(blah blah blah)>`反应是"What the ....!", 这只代表着你可能还没有读 Item 33。别担心。在本节,重要的事是匿名函数声明的`auto&&`类型的参数。`func`是一个通用引用,可以被绑定到任何可被调用的对象,无论左值还是右值。`args`是**0个**或者多个通用引用(也就是说,它是个通用引用参数包(a universal reference parameter pack)),它可以绑定到任意数目、任意类型的对象上。
|
||||
多亏了`auto`类型的通用引用,函数`timeFuncInvocation`可以对**近乎任意**(pretty-much any)函数进行计时。(如果你想知道*任意(any)*和*近乎任意(pretty-much any*的区别,往后翻到 Item 30)。
|
||||
如果你对*lambda*里的代码“`std::forward<decltype(blah blah blah)>`”反应是“这什么鬼...?!”,这只代表着你可能还没有读[Item33](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item33.md)。别担心。在本条款,重要的事是*lambda*表达式中声明的`auto&&`类型的形参。`func`是一个通用引用,可以被绑定到任何可调用对象,无论左值还是右值。`args`是0个或者多个通用引用(也就是说,它是个通用引用*parameter pack*),它可以绑定到任意数目、任意类型的对象上。多亏了`auto`类型的通用引用,函数`timeFuncInvocation`可以对**近乎任意**(pretty much any)函数进行计时。(如果你想知道任意(any)和近乎任意(pretty much any)的区别,往后翻到[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md))。
|
||||
|
||||
牢记整个本小节——通用引用的基础——是一个谎言,uhh, 一个“抽象”。隐藏在其底下的真相被称为"**引用折叠(reference collapsing)**",小节Item 28致力于讨论它。但是这个真相并不降低该抽象的有用程度。区分右值引用和通用引用将会帮助你更准确地阅读代码("究竟我眼前的这个`T&&`是只绑定到右值还是可以绑定任意对象呢?"),并且,当你在和你的合作者交流时,它会帮助你避免歧义("在这里我在用一个通用引用,而非右值引用")。它也可以帮助你弄懂Item 25和26,它们依赖于右值引用和通用引用的区别。所以,拥抱这份抽象,陶醉于它吧。就像牛顿的力学定律(本质上不正确),比起爱因斯坦的相对论(这是真相)而言,往往更简单,更易用。所以这份通用引用的概念,相较于穷究引用折叠的细节而言,是更合意之选。
|
||||
牢记整个本条款——通用引用的基础——是一个谎言,啊,一个“抽象”。隐藏在其底下的真相被称为**引用折叠**(*reference collapsing*),[Item28](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md)致力于讨论它。但是这个真相并不降低该抽象的有用程度。区分右值引用和通用引用将会帮助你更准确地阅读代码(“究竟我眼前的这个`T&&`是只绑定到右值还是可以绑定任意对象呢?”),并且,当你在和你的合作者交流时,它会帮助你避免歧义(“在这里我在用一个通用引用,而非右值引用……”)。它也可以帮助你弄懂[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)和[26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md),它们依赖于右值引用和通用引用的区别。所以,拥抱这份抽象,陶醉于它吧。就像牛顿的力学定律(本质上不正确),比起爱因斯坦的广义相对论(这是真相)而言,往往更简单,更易用。所以这份通用引用的概念,相较于穷究引用折叠的细节而言,是更合意之选。
|
||||
|
||||
**请记住:**
|
||||
|
||||
**记住**:
|
||||
|
||||
- 如果一个函数模板参数的类型为`T&&`,并且`T`需要被推导得知,或者如果一个对象被声明为`auto&&`,这个参数或者对象就是一个通用引用。
|
||||
- 如果一个函数模板形参的类型为`T&&`,并且`T`需要被推导得知,或者如果一个对象被声明为`auto&&`,这个形参或者对象就是一个通用引用。
|
||||
- 如果类型声明的形式不是标准的`type&&`,或者如果类型推导没有发生,那么`type&&`代表一个右值引用。
|
||||
- 通用引用,如果它被右值初始化,就会对应地成为右值引用;如果它被左值初始化,就会成为左值引用。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- 通用引用,如果它被右值初始化,就会对应地成为右值引用;如果它被左值初始化,就会成为左值引用。
|
||||
|
@ -1,214 +1,265 @@
|
||||
## Item25: 对右值引用使用`std::move`,对通用引用使用`std::forward`
|
||||
## 条款二十五:对右值引用使用`std::move`,对通用引用使用`std::forward`
|
||||
|
||||
右值引用仅绑定可以移动的对象。如果你有一个右值引用参数,你就知道这个对象可能会被移动:
|
||||
**Item 25: Use `std::move` on rvalue references, `std::forward` on universal references**
|
||||
|
||||
右值引用仅绑定可以移动的对象。如果你有一个右值引用形参就知道这个对象可能会被移动:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
Widget(Widget&& rhs); //rhs definitely refers to an object eligible for moving
|
||||
...
|
||||
Widget(Widget&& rhs); //rhs定义上引用一个有资格移动的对象
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
这是个例子,你将希望通过可以利用该对象右值性的方式传递给其他使用对象的函数。这样做的方法是将绑定次类对象的参数转换为右值。如Item23中所述,这不仅是`std::move`所做,而且是为它创建:
|
||||
这是个例子,你将希望传递这样的对象给其他函数,允许那些函数利用对象的右值性(rvalueness)。这样做的方法是将绑定到此类对象的形参转换为右值。如[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)中所述,这不仅是`std::move`所做,而且它的创建就是为了这个目的:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(Widget&& rhs) :name(std::move(rhs.name)), p(std::move(rhs.p)) {...}
|
||||
...
|
||||
Widget(Widget&& rhs) //rhs是右值引用
|
||||
: name(std::move(rhs.name)),
|
||||
p(std::move(rhs.p))
|
||||
{ … }
|
||||
…
|
||||
private:
|
||||
std::string name;
|
||||
std::shared_ptr<SomeDataStructure> p;
|
||||
std::string name;
|
||||
std::shared_ptr<SomeDataStructure> p;
|
||||
};
|
||||
```
|
||||
|
||||
另一方面(查看Item24),通用引用可能绑定到有资格移动的对象上。通用引用使用右值初始化时,才将其强制转换为右值。Item23阐释了这正是`std::forward`所做的:
|
||||
另一方面(查看[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)),通用引用**可能**绑定到有资格移动的对象上。通用引用使用右值初始化时,才将其强制转换为右值。[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)阐释了这正是`std::forward`所做的:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
template<typename T>
|
||||
void setName(T&& newName) { //newName is universal reference
|
||||
name = std::forward<T>(newName);
|
||||
}
|
||||
...
|
||||
}
|
||||
template<typename T>
|
||||
void setName(T&& newName) //newName是通用引用
|
||||
{ name = std::forward<T>(newName); }
|
||||
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
总而言之,当传递给函数时右值引用应该无条件转换为右值(通过`std::move`),通用引用应该有条件转换为右值(通过`std::forward`)。
|
||||
总而言之,当把右值引用转发给其他函数时,右值引用应该被**无条件转换**为右值(通过`std::move`),因为它们**总是**绑定到右值;当转发通用引用时,通用引用应该**有条件地转换**为右值(通过`std::forward`),因为它们只是**有时**绑定到右值。
|
||||
|
||||
Item23 解释说,可以在右值引用上使用`std::forward`表现出适当的行为,但是代码较长,容易出错,所以应该避免在右值引用上使用`std::forward`。更糟的是在通用引用上使用`std::move`,这可能会意外改变左值。
|
||||
[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)解释说,可以在右值引用上使用`std::forward`表现出适当的行为,但是代码较长,容易出错,所以应该避免在右值引用上使用`std::forward`。更糟的是在通用引用上使用`std::move`,这可能会意外改变左值(比如局部变量):
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
template<typename T>
|
||||
void setName(T&& newName) {
|
||||
name = std::move(newName); //universal reference compiles, but is bad ! bad ! bad !
|
||||
}
|
||||
...
|
||||
|
||||
template<typename T>
|
||||
void setName(T&& newName) //通用引用可以编译,
|
||||
{ name = std::move(newName); } //但是代码太太太差了!
|
||||
…
|
||||
|
||||
private:
|
||||
std::string name;
|
||||
std::shared_ptr<SomeDataStructure> p;
|
||||
std::string name;
|
||||
std::shared_ptr<SomeDataStructure> p;
|
||||
};
|
||||
|
||||
std::string getWidgetName(); // factory function
|
||||
std::string getWidgetName(); //工厂函数
|
||||
|
||||
Widget w;
|
||||
auto n = getWidgetName(); // n is local variiable
|
||||
w.setName(n); // move n into w! n's value now unkown
|
||||
|
||||
auto n = getWidgetName(); //n是局部变量
|
||||
|
||||
w.setName(n); //把n移动进w!
|
||||
|
||||
… //现在n的值未知
|
||||
```
|
||||
|
||||
上面的例子,局部变量n被传递给`w.setName`,可以调用方对n只有只读操作。但是因为`setName`内部使用`std::move`无条件将传递的参数转换为右值,`n`的值被移动给w,n最终变为未定义的值。这种行为使得调用者蒙圈了。
|
||||
上面的例子,局部变量`n`被传递给`w.setName`,调用方可能认为这是对`n`的只读操作——这一点倒是可以被原谅。但是因为`setName`内部使用`std::move`无条件将传递的引用形参转换为右值,`n`的值被移动进`w.name`,调用`setName`返回时`n`最终变为未定义的值。这种行为使得调用者蒙圈了——还有可能变得狂躁。
|
||||
|
||||
你可能争辩说`setName`不应该将其参数声明为通用引用。此类引用不能使用`const`(Item 24),但是`setName`肯定不应该修改其参数。你可能会指出,如果const左值和右值分别进行重载可以避免整个问题,比如这样:
|
||||
你可能争辩说`setName`不应该将其形参声明为通用引用。此类引用不能使用`const`(见[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)),但是`setName`肯定不应该修改其形参。你可能会指出,如果为`const`左值和为右值分别重载`setName`可以避免整个问题,比如这样:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
void setName(const std::string& newName) { // set from const lvalue
|
||||
name = newName;
|
||||
}
|
||||
void setName(std::string&& newName) { // set from rvalue
|
||||
name = std::move(newName);
|
||||
}
|
||||
void setName(const std::string& newName) //用const左值设置
|
||||
{ name = newName; }
|
||||
|
||||
void setName(std::string&& newName) //用右值设置
|
||||
{ name = std::move(newName); }
|
||||
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
这样的话,当然可以工作,但是有缺点。首先编写和维护的代码更多;其次,效率下降。比如,考虑如下场景:
|
||||
这样的话,当然可以工作,但是有缺点。首先编写和维护的代码更多(两个函数而不是单个模板);其次,效率下降。比如,考虑如下场景:
|
||||
|
||||
```cpp
|
||||
w.setName("Adela Novak");
|
||||
```
|
||||
|
||||
使用通用引用的版本,字面字符串"Adela Novak"可以被传递给setName,在w内部使用了`std::string`的赋值运算符。w的name的数据成员直接通过字面字符串直接赋值,没有中间对象被创建。但是,重载版本,会有一个中间对象被创建。一次setName的调用会包括`std::string`的构造器调用(中间对象),`std::string`的赋值运算调用,`std::string`的析构调用(中间对象)。这比直接通过const char*赋值给`std::string`开销昂贵许多。实际的开销可能因为库的实现而有所不同,但是事实上,将通用引用模板替换成多个函数重载在某些情况下会导致运行时的开销。如果例子中的`Widget`数据成员是任意类型(不一定是`std::string`),性能差距可能会变得更大,因为不是所有类型的移动操作都像`std::string`开销较小(参看Item29)。
|
||||
使用通用引用的版本的`setName`,字面字符串“`Adela Novak`”可以被传递给`setName`,再传给`w`内部`std::string`的赋值运算符。`w`的`name`的数据成员通过字面字符串直接赋值,没有临时`std::string`对象被创建。但是,`setName`重载版本,会有一个临时`std::string`对象被创建,`setName`形参绑定到这个对象,然后这个临时`std::string`移动到`w`的数据成员中。一次`setName`的调用会包括`std::string`构造函数调用(创建中间对象),`std::string`赋值运算符调用(移动`newName`到`w.name`),`std::string`析构函数调用(析构中间对象)。这比调用接受`const char*`指针的`std::string`赋值运算符开销昂贵许多。增加的开销根据实现不同而不同,这些开销是否值得担心也跟应用和库的不同而有所不同,但是事实上,将通用引用模板替换成对左值引用和右值引用的一对函数重载在某些情况下会导致运行时的开销。如果把例子泛化,`Widget`数据成员是任意类型(而不是知道是个`std::string`),性能差距可能会变得更大,因为不是所有类型的移动操作都像`std::string`开销较小(参看[Item29](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item29.md))。
|
||||
|
||||
但是,关于重载函数最重要的问题不是源代码的数量,也不是代码的运行时性能。而是设计的可扩展性差。`Widget::setName`接受一个参数,可以是左值或者右值,因此需要两种重载实现,`n`个参数的话,就要实现$2^n$种重载。这还不是最坏的。有的函数---函数模板----接受无限制参数,每个参数都可以是左值或者右值。此类函数的例子比如`std::make_unique`或者`std::make_shared`。查看他们的的重载声明:
|
||||
但是,关于对左值和右值的重载函数最重要的问题不是源代码的数量,也不是代码的运行时性能。而是设计的可扩展性差。`Widget::setName`有一个形参,因此需要两种重载实现,但是对于有更多形参的函数,每个都可能是左值或右值,重载函数的数量几何式增长:n个参数的话,就要实现2<sup>n</sup>种重载。这还不是最坏的。有的函数——实际上是函数模板——接受**无限制**个数的参数,每个参数都可以是左值或者右值。此类函数的典型代表是`std::make_shared`,还有对于C++14的`std::make_unique`(见[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md))。查看他们的的重载声明:
|
||||
|
||||
```cpp
|
||||
template<class T, class... Args>
|
||||
template<class T, class... Args> //来自C++11标准
|
||||
shared_ptr<T> make_shared(Args&&... args);
|
||||
|
||||
template<class T, class... Args>
|
||||
template<class T, class... Args> //来自C++14标准
|
||||
unique_ptr<T> make_unique(Args&&... args);
|
||||
```
|
||||
|
||||
对于这种函数,对于左值和右值分别重载就不能考虑了:通用引用是仅有的实现方案。对这种函数,我向你保证,肯定使用`std::forward`传递通用引用给其他函数。
|
||||
对于这种函数,对于左值和右值分别重载就不能考虑了:通用引用是仅有的实现方案。对这种函数,我向你保证,肯定使用`std::forward`传递通用引用形参给其他函数。这也是你应该做的。
|
||||
|
||||
好吧,通常,最终。但是不一定最开始就是如此。在某些情况,你可能需要在一个函数中多次使用绑定到右值引用或者通用引用的对象,并且确保在完成其他操作前,这个对象不会被移动。这时,你只想在最后一次使用时,使用`std::move`或者`std::forward`。比如:
|
||||
好吧,通常,最终。但是不一定最开始就是如此。在某些情况,你可能需要在一个函数中多次使用绑定到右值引用或者通用引用的对象,并且确保在完成其他操作前,这个对象不会被移动。这时,你只想在最后一次使用时,使用`std::move`(对右值引用)或者`std::forward`(对通用引用)。比如:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void setSignText(T&& text)
|
||||
void setSignText(T&& text) //text是通用引用
|
||||
{
|
||||
sign.setText(text);
|
||||
sign.setText(text); //使用text但是不改变它
|
||||
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto now =
|
||||
std::chrono::system_clock::now(); //获取现在的时间
|
||||
|
||||
signHistory.add(now, std::forward<T>(text));
|
||||
signHistory.add(now,
|
||||
std::forward<T>(text)); //有条件的转换为右值
|
||||
}
|
||||
```
|
||||
|
||||
这里,我们想要确保`text`的值不会被`sign.setText`改变,因为我们想要在`signHistory.add`中继续使用。因此`std::forward`只在最后使用。
|
||||
|
||||
对于`std::move`,同样的思路,但是需要注意,在有些稀少的情况下,你需要调用`std::move_if_noexcept`代替`std::move`。要了解何时以及为什么,参考Item 14。
|
||||
对于`std::move`,同样的思路(即最后一次用右值引用的时候再调用`std::move`),但是需要注意,在有些稀少的情况下,你需要调用`std::move_if_noexcept`代替`std::move`。要了解何时以及为什么,参考[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md)。
|
||||
|
||||
如果你使用的按值返回的函数,并且返回值绑定到右值引用或者通用引用上,需要对返回的引用使用`std::move`或者`std::forward`。要了解原因,考虑`+`操作两个矩阵的函数,左侧的矩阵参数为右值(可以被用来保存求值之后的和)
|
||||
如果你在**按值**返回的函数中,返回值绑定到右值引用或者通用引用上,需要对返回的引用使用`std::move`或者`std::forward`。要了解原因,考虑两个矩阵相加的`operator+`函数,左侧的矩阵为右值(可以被用来保存求值之后的和):
|
||||
|
||||
```cpp
|
||||
Matrix operator+(Matrix&& lhs, const Matrix& rhs){
|
||||
lhs += rhs;
|
||||
return std::move(lhs); // move lhs into return value
|
||||
Matrix //按值返回
|
||||
operator+(Matrix&& lhs, const Matrix& rhs)
|
||||
{
|
||||
lhs += rhs;
|
||||
return std::move(lhs); //移动lhs到返回值中
|
||||
}
|
||||
```
|
||||
|
||||
通过在返回语句中将lhs转换为右值,lhs可以移动到返回值的内存位置。如果`std::move`省略了
|
||||
通过在`return`语句中将`lhs`转换为右值(通过`std::move`),`lhs`可以移动到返回值的内存位置。如果省略了`std::move`调用,
|
||||
|
||||
```cpp
|
||||
Matrix operator+(Matrix&& lhs, const Matrix& rhs){
|
||||
lhs += rhs;
|
||||
return lhs; // copy lhs into return value
|
||||
Matrix //同之前一样
|
||||
operator+(Matrix&& lhs, const Matrix& rhs)
|
||||
{
|
||||
lhs += rhs;
|
||||
return lhs; //拷贝lhs到返回值中
|
||||
}
|
||||
```
|
||||
|
||||
事实上,lhs作为左值,会被编译器拷贝到返回值的内存空间。假定Matrix支持移动操作,并且比拷贝操作效率更高,使用`std::move`的代码效率更高。
|
||||
`lhs`是个左值的事实,会强制编译器拷贝它到返回值的内存空间。假定`Matrix`支持移动操作,并且比拷贝操作效率更高,在`return`语句中使用`std::move`的代码效率更高。
|
||||
|
||||
如果Matrix不支持移动操作,将其转换为左值不会变差,因为右值可以直接被Matrix的拷贝构造器使用。如果Matrix随后支持了移动操作,`+`操作符的定义将在下一次编译时受益。就是这种情况,通过将`std::move`应用到返回语句中,不会损失什么,还可能获得收益。
|
||||
如果`Matrix`不支持移动操作,将其转换为右值不会变差,因为右值可以直接被`Matrix`的拷贝构造函数拷贝(见[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md))。如果`Matrix`随后支持了移动操作,`operator+`将在下一次编译时受益。就是这种情况,通过将`std::move`应用到按值返回的函数中要返回的右值引用上,不会损失什么(还可能获得收益)。
|
||||
|
||||
使用通用引用和`std::forward`的情况类似。考虑函数模板`reduceAndCopy`收到一个未规约对象`Fraction`,将其规约,并返回一个副本。如果原始对象是右值,可以将其移动到返回值中,避免拷贝开销,但是如果原始对象是左值,必须创建副本,因此如下代码:
|
||||
使用通用引用和`std::forward`的情况类似。考虑函数模板`reduceAndCopy`收到一个未规约(unreduced)对象`Fraction`,将其规约,并返回一个规约后的副本。如果原始对象是右值,可以将其移动到返回值中(避免拷贝开销),但是如果原始对象是左值,必须创建副本,因此如下代码:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
Fraction reduceAndCopy(T&& frac) {
|
||||
frac.reduce();
|
||||
return std::forward<T>(frac); // move rvalue into return value, copy lvalue
|
||||
Fraction //按值返回
|
||||
reduceAndCopy(T&& frac) //通用引用的形参
|
||||
{
|
||||
frac.reduce();
|
||||
return std::forward<T>(frac); //移动右值,或拷贝左值到返回值中
|
||||
}
|
||||
```
|
||||
|
||||
如果`std::forward`被忽略,frac就是无条件复制到返回值内存空间。
|
||||
如果`std::forward`被忽略,`frac`就被无条件复制到`reduceAndCopy`的返回值内存空间。
|
||||
|
||||
有些开发者获取到上面的知识后,并尝试将其扩展到不适用的情况。
|
||||
有些开发者获取到上面的知识后,并尝试将其扩展到不适用的情况。“如果对要被拷贝到返回值的右值引用形参使用`std::move`,会把拷贝构造变为移动构造,”他们想,“我也可以对我要返回的局部对象应用同样的优化。”换句话说,他们认为有个按值返回局部对象的函数,像这样,
|
||||
|
||||
```cpp
|
||||
Widget makeWidget() {
|
||||
Widget w; //local variable
|
||||
... // configure w
|
||||
return w; // "copy" w into return value
|
||||
Widget makeWidget() //makeWidget的“拷贝”版本
|
||||
{
|
||||
Widget w; //局部对象
|
||||
… //配置w
|
||||
return w; //“拷贝”w到返回值中
|
||||
}
|
||||
```
|
||||
|
||||
想要优化copy的动作为如下代码:
|
||||
他们想要“优化”代码,把“拷贝”变为移动:
|
||||
|
||||
```cpp
|
||||
Widget makeWidget() {
|
||||
Widget w; //local variable
|
||||
... // configure w
|
||||
return std::move(w); // move w into return value(don't do this!)
|
||||
Widget makeWidget() //makeWidget的移动版本
|
||||
{
|
||||
Widget w;
|
||||
…
|
||||
return std::move(w); //移动w到返回值中(不要这样做!)
|
||||
}
|
||||
```
|
||||
|
||||
这种用法是有问题的,但是问题在哪?
|
||||
我的注释告诉你这种想法是有问题的,但是问题在哪?
|
||||
|
||||
在进行优化时,标准化委员会远领先于开发者,第一个版本的makeWidget可以在分配给函数返回值的内存中构造局部变量w来避免复制局部变量w的需要。这就是所谓的返回值优化(RVO),这在C++标准中已经实现了。
|
||||
这是错的,因为对于这种优化,标准化委员会远领先于开发者。早就为人认识到的是,`makeWidget`的“拷贝”版本可以避免复制局部变量`w`的需要,通过在分配给函数返回值的内存中构造`w`来实现。这就是所谓的**返回值优化**(*return value optimization*,RVO),这在C++标准中已经实现了。
|
||||
|
||||
所以"copy"版本的makeWidget在编译时都避免了拷贝局部变量w,进行了返回值优化。(返回值优化的条件:1. 局部变量与返回值的类型相同;2. 局部变量就是返回值)。
|
||||
对这种好事遣词表达是个讲究活,因为你想只在那些不影响软件外在行为的地方允许这样的**拷贝消除**(copy elision)。对标准中教条的(也可以说是有毒的)絮叨做些解释,这个特定的好事就是说,编译器可能会在按值返回的函数中消除对局部对象的拷贝(或者移动),如果满足(1)局部对象与函数返回值的类型相同;(2)局部对象就是要返回的东西。(适合的局部对象包括大多数局部变量(比如`makeWidget`里的`w`),还有作为`return`语句的一部分而创建的临时对象。函数形参不满足要求。一些人将RVO的应用区分为命名的和未命名的(即临时的)局部对象,限制了RVO术语应用到未命名对象上,并把对命名对象的应用称为**命名返回值优化**(*named return value optimization*,NRVO)。)把这些记在脑子里,再看看`makeWidget`的“拷贝”版本:
|
||||
|
||||
移动版本的makeWidget行为与其名称一样,将w的内容移动到makeWidget的返回值位置。但是为什么编译器不使用RVO消除这种移动,而是在分配给函数返回值的内存中再次构造w呢?条件2中规定,仅当返回值为局部对象时,才进行RVO,但是move版本不满足这条件,再次看一下返回语句:
|
||||
```cpp
|
||||
Widget makeWidget() //makeWidget的“拷贝”版本
|
||||
{
|
||||
Widget w;
|
||||
…
|
||||
return w; //“拷贝”w到返回值中
|
||||
}
|
||||
```
|
||||
|
||||
这里两个条件都满足,你一定要相信我,对于这些代码,每个合适的C++编译器都会应用RVO来避免拷贝`w`。那意味着`makeWidget`的“拷贝”版本实际上不拷贝任何东西。
|
||||
|
||||
移动版本的`makeWidget`行为与其名称一样(假设`Widget`有移动构造函数),将`w`的内容移动到`makeWidget`的返回值位置。但是为什么编译器不使用RVO消除这种移动,而是在分配给函数返回值的内存中再次构造`w`呢?答案很简单:它们不能。条件(2)中规定,仅当返回值为局部对象时,才进行RVO,但是`makeWidget`的移动版本不满足这条件,再次看一下返回语句:
|
||||
|
||||
```cpp
|
||||
return std::move(w);
|
||||
```
|
||||
|
||||
返回的已经不是局部对象w,而是局部对象w的引用。返回局部对象的引用不满足RVO的第二个条件,所以编译器必须移动w到函数返回值的位置。开发者试图帮助编译器优化反而限制了编译器的优化选项。
|
||||
返回的已经不是局部对象`w`,而是**`w`的引用**——`std::move(w)`的结果。返回局部对象的引用不满足RVO的第二个条件,所以编译器必须移动`w`到函数返回值的位置。开发者试图对要返回的局部变量用`std::move`帮助编译器优化,反而限制了编译器的优化选项。
|
||||
|
||||
(译者注:本段即绕又长,大意为即使开发者非常熟悉编译器,坚持要在局部变量上使用`std::move`返回)
|
||||
但是RVO就是个优化。编译器不被**要求**消除拷贝和移动操作,即使他们被允许这样做。或许你会疑惑,并担心编译器用拷贝操作惩罚你,因为它们确实可以这样。或者你可能有足够的了解,意识到有些情况很难让编译器实现RVO,比如当函数不同控制路径返回不同局部变量时。(编译器必须产生一些代码在分配的函数返回值的内存中构造适当的局部变量,但是编译器如何确定哪个变量是合适的呢?)如果这样,你可能会愿意以移动的代价来保证不会产生拷贝。那就是,极可能仍然认为应用`std::move`到一个要返回的局部对象上是合理的,只因为可以不再担心拷贝的代价。
|
||||
|
||||
这仍然是一个坏主意。C++标准关于RVO的部分表明,如果满足RVO的条件,但是编译器选择不执行复制忽略,则必须将返回的对象视为右值。实际上,标准要求RVO,忽略复制或者将`sdt::move`隐式应用于返回的本地对象。因此,在makeWidget的"copy"版本中,编译器要不执行复制忽略的优化,要不自动将`std::move`隐式执行。
|
||||
|
||||
按值传递参数的情形与此类似。他们没有资格进行RVO,但是如果作为返回值的话编译器会将其视作右值。结果就是,如果代码如下:
|
||||
那种情况下,应用`std::move`到一个局部对象上**仍然**是一个坏主意。C++标准关于RVO的部分表明,如果满足RVO的条件,但是编译器选择不执行拷贝消除,则返回的对象**必须被视为右值**。实际上,标准要求当RVO被允许时,或者实行拷贝消除,或者将`std::move`隐式应用于返回的局部对象。因此,在`makeWidget`的“拷贝”版本中,
|
||||
|
||||
```cpp
|
||||
Widget makeWidget(Widget w) {
|
||||
...
|
||||
return w;
|
||||
Widget makeWidget() //同之前一样
|
||||
{
|
||||
Widget w;
|
||||
…
|
||||
return w;
|
||||
}
|
||||
```
|
||||
|
||||
实际上,编译器的代码如下:
|
||||
编译器要不消除`w`的拷贝,要不把函数看成像下面写的一样:
|
||||
|
||||
```cpp
|
||||
Widget makeWidget(Widget w){
|
||||
...
|
||||
return std::move(w);
|
||||
Widget makeWidget()
|
||||
{
|
||||
Widget w;
|
||||
…
|
||||
return std::move(w); //把w看成右值,因为不执行拷贝消除
|
||||
}
|
||||
```
|
||||
|
||||
这意味着,如果对从按值返回局部对象的函数使用`std::move`,你并不能帮助编译器,而是阻碍其执行优化选项。在某些情况下,将`std::move`应用于局部变量可能是一件合理的事,但是不要阻碍编译器RVO。
|
||||
这种情况与按值返回函数形参的情况很像。形参们没资格参与函数返回值的拷贝消除,但是如果作为返回值的话编译器会将其视作右值。结果就是,如果代码如下:
|
||||
|
||||
### 需要记住的点
|
||||
```cpp
|
||||
Widget makeWidget(Widget w) //传值形参,与函数返回的类型相同
|
||||
{
|
||||
…
|
||||
return w;
|
||||
}
|
||||
```
|
||||
|
||||
- 在右值引用上使用`std::move`,在通用引用上使用`std::forward`
|
||||
- 对按值返回的函数返回值,无论返回右值引用还是通用引用,执行相同的操作
|
||||
- 当局部变量就是返回值是,不要使用`std::move`或者`std::forward`
|
||||
编译器必须看成像下面这样写的代码:
|
||||
|
||||
```cpp
|
||||
Widget makeWidget(Widget w)
|
||||
{
|
||||
…
|
||||
return std::move(w);
|
||||
}
|
||||
```
|
||||
|
||||
这意味着,如果对从按值返回的函数返回来的局部对象使用`std::move`,你并不能帮助编译器(如果不能实行拷贝消除的话,他们必须把局部对象看做右值),而是阻碍其执行优化选项(通过阻止RVO)。在某些情况下,将`std::move`应用于局部变量可能是一件合理的事(即,你把一个变量传给函数,并且知道不会再用这个变量),但是满足RVO的`return`语句或者返回一个传值形参并不在此列。
|
||||
|
||||
**请记住:**
|
||||
|
||||
- 最后一次使用时,在右值引用上使用`std::move`,在通用引用上使用`std::forward`。
|
||||
- 对按值返回的函数要返回的右值引用和通用引用,执行相同的操作。
|
||||
- 如果局部对象可以被返回值优化消除,就绝不使用`std::move`或者`std::forward`。
|
||||
|
@ -1,186 +1,208 @@
|
||||
## Item 26: Avoid overloading on universal references
|
||||
## 条款二十六:避免在通用引用上重载
|
||||
|
||||
## Item 26: 避免在通用引用上重载
|
||||
**Item 26: Avoid overloading on universal references**
|
||||
|
||||
假定你需要写一个函数,它使用name这样一个参数,打印当前日期和具体时间到日志中,然后将name加入到一个全局数据结构中。你可能写出来这样的代码:
|
||||
假定你需要写一个函数,它使用名字作为形参,打印当前日期和时间到日志中,然后将名字加入到一个全局数据结构中。你可能写出来这样的代码:
|
||||
|
||||
```cpp
|
||||
std::multiset<std::string> names; // global data structure
|
||||
std::multiset<std::string> names; //全局数据结构
|
||||
void logAndAdd(const std::string& name)
|
||||
{
|
||||
auto now = std::chrono::system_lock::now(); // get current time
|
||||
log(now, "logAndAdd"); // make log entry
|
||||
names.emplace(name); // add name to global data structure; see Item 42 for info on emplace
|
||||
}
|
||||
auto now = //获取当前时间
|
||||
std::chrono::system_clock::now();
|
||||
log(now, "logAndAdd"); //志记信息
|
||||
names.emplace(name); //把name加到全局数据结构中;
|
||||
} //emplace的信息见条款42
|
||||
```
|
||||
|
||||
这份代码没有问题,但是同样的也没有效率。考虑这三个调用:
|
||||
|
||||
```cpp
|
||||
std::string petName("Darla");
|
||||
logAndAdd(petName); // pass lvalue std::string
|
||||
logAndAdd(std::string("Persephone")); // pass rvalue std::string
|
||||
logAndAdd("Patty Dog"); // pass string literal
|
||||
logAndAdd(petName); //传递左值std::string
|
||||
logAndAdd(std::string("Persephone")); //传递右值std::string
|
||||
logAndAdd("Patty Dog"); //传递字符串字面值
|
||||
```
|
||||
|
||||
在第一个调用中,`logAndAdd`使用变量作为参数。在`logAndAdd`中`name`最终也是通过`emplace`传递给`names`。因为`name`是左值,会拷贝到`names`中。没有方法避免拷贝,因为是左值传递的。
|
||||
在第一个调用中,`logAndAdd`的形参`name`绑定到变量`petName`。在`logAndAdd`中`name`最终传给`names.emplace`。因为`name`是左值,会拷贝到`names`中。没有方法避免拷贝,因为是左值(`petName`)传递给`logAndAdd`的。
|
||||
|
||||
在第三个调用中,参数`name`绑定一个右值,但是这次是通过"Patty Dog"隐式创建的临时`std::string`变量。在第二个调用中,`name`被拷贝到`names`,但是这里,传递的是一个字符串字面量。直接将字符串字面量传递给`emplace`,不会创建`std::string`的临时变量,而是直接在`std::multiset`中通过字面量构建`std::string`。在第三个调用中,我们会消耗`std::string`的拷贝开销,但是连移动开销都不想有,更别说拷贝的。
|
||||
在第二个调用中,形参`name`绑定到右值(显式从“`Persephone`”创建的临时`std::string`)。`name`本身是个左值,所以它被拷贝到`names`中,但是我们意识到,原则上,它的值可以被移动到`names`中。本次调用中,我们有个拷贝代价,但是我们应该能用移动勉强应付。
|
||||
|
||||
我们可以通过使用通用引用(参见Item 24)重写第二个和第三个调用来使效率提升,按照Item 25的说法,`std::forward`转发引用到`emplace`。代码如下:
|
||||
在第三个调用中,形参`name`也绑定一个右值,但是这次是通过“`Patty Dog`”隐式创建的临时`std::string`变量。就像第二个调用中,`name`被拷贝到`names`,但是这里,传递给`logAndAdd`的实参是一个字符串字面量。如果直接将字符串字面量传递给`emplace`,就不会创建`std::string`的临时变量,而是直接在`std::multiset`中通过字面量构建`std::string`。在第三个调用中,我们有个`std::string`拷贝开销,但是我们连移动开销都不想要,更别说拷贝的。
|
||||
|
||||
我们可以通过使用通用引用(参见[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md))重写`logAndAdd`来使第二个和第三个调用效率提升,按照[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)的说法,`std::forward`转发这个引用到`emplace`。代码如下:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void logAndAdd(T&& name)
|
||||
{
|
||||
auto now = std::chrono::system_lock::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(std::forward<T>(name));
|
||||
auto now = std::chrono::system_lock::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(std::forward<T>(name));
|
||||
}
|
||||
std::string petName("Darla"); // as before
|
||||
logAndAdd(petName); // as before , copy
|
||||
logAndAdd(std::string("Persephone")); // move rvalue instead of copying it
|
||||
logAndAdd("Patty Dog"); // create std::string in multiset instead of copying a temporary std::string
|
||||
|
||||
std::string petName("Darla"); //跟之前一样
|
||||
logAndAdd(petName); //跟之前一样,拷贝右值到multiset
|
||||
logAndAdd(std::string("Persephone")); //移动右值而不是拷贝它
|
||||
logAndAdd("Patty Dog"); //在multiset直接创建std::string
|
||||
//而不是拷贝一个临时std::string
|
||||
```
|
||||
|
||||
非常好,效率优化了!
|
||||
|
||||
在故事的最后,我们可以骄傲的交付这个代码,但是我没有告诉你client不总是有访问`logAndAdd`要求的`names`的权限。有些clients只有`names`的下标。为了支持这种client,`logAndAdd`需要重载为:
|
||||
在故事的最后,我们可以骄傲的交付这个代码,但是我还没有告诉你客户不总是有直接访问`logAndAdd`要求的名字的权限。有些客户只有索引,`logAndAdd`拿着索引在表中查找相应的名字。为了支持这些客户,`logAndAdd`需要重载为:
|
||||
|
||||
```cpp
|
||||
std::string nameFromIdx(int idx); // return name corresponding to idx
|
||||
void logAndAdd(int idx)
|
||||
std::string nameFromIdx(int idx); //返回idx对应的名字
|
||||
|
||||
void logAndAdd(int idx) //新的重载
|
||||
{
|
||||
auto now = std::chrono::system_lock::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(nameFromIdx(idx));
|
||||
auto now = std::chrono::system_lock::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(nameFromIdx(idx));
|
||||
}
|
||||
```
|
||||
|
||||
之后的两个调用按照预期工作:
|
||||
|
||||
```cpp
|
||||
std::string petName("Darla");
|
||||
logAndAdd(petName);
|
||||
logAndAdd(std::string("Persephone"));
|
||||
logAndAdd("Patty Dog"); // these calls all invoke the T&& overload
|
||||
std::string petName("Darla"); //跟之前一样
|
||||
|
||||
logAndAdd(22); // calls int overload
|
||||
logAndAdd(petName); //跟之前一样,
|
||||
logAndAdd(std::string("Persephone")); //这些调用都去调用
|
||||
logAndAdd("Patty Dog"); //T&&重载版本
|
||||
|
||||
logAndAdd(22); //调用int重载版本
|
||||
```
|
||||
|
||||
事实上,这只能基本按照预期工作,假定一个client将`short`类型当做下标传递给`logAndAdd`:
|
||||
事实上,这只能基本按照预期工作,假定一个客户将`short`类型索引传递给`logAndAdd`:
|
||||
|
||||
```cpp
|
||||
short nameIdx;
|
||||
... // give nameIdx a value
|
||||
logAndAdd(nameIdx); // error!
|
||||
… //给nameIdx一个值
|
||||
logAndAdd(nameIdx); //错误!
|
||||
```
|
||||
|
||||
之后一行的error说明并不清楚,下面让我来说明发生了什么。
|
||||
最后一行的注释并不清楚明白,下面让我来说明发生了什么。
|
||||
|
||||
有两个重载的`logAndAdd`。一个使用通用应用推导出T的类型是`short`,因此可以精确匹配。对于`int`参数类型的重载`logAndAdd`也可以`short`类型提升后匹配成功。根据正常的重载解决规则,精确匹配优先于类型提升的匹配,所以被调用的是通用引用的重载。
|
||||
有两个重载的`logAndAdd`。使用通用引用的那个推导出`T`的类型是`short`,因此可以精确匹配。对于`int`类型参数的重载也可以在`short`类型提升后匹配成功。根据正常的重载解决规则,精确匹配优先于类型提升的匹配,所以被调用的是通用引用的重载。
|
||||
|
||||
在通用引用中的实现中,将`short`类型`emplace`到`std::string`的容器中,发生了错误。所有这一切的原因就是对于`short`类型通用引用重载优先于`int`类型的重载。
|
||||
在通用引用那个重载中,`name`形参绑定到要传入的`short`上,然后`name`被`std::forward`给`names`(一个`std::multiset<std::string>`)的`emplace`成员函数,然后又被转发给`std::string`构造函数。`std::string`没有接受`short`的构造函数,所以`logAndAdd`调用里的`multiset::emplace`调用里的`std::string`构造函数调用失败。(译者注:这句话比较绕,实际上就是调用链。)所有这一切的原因就是对于`short`类型通用引用重载优先于`int`类型的重载。
|
||||
|
||||
使用通用引用类型的函数在C++中是贪婪函数。他们几乎可以精确匹配任何类型的参数(极少不适用的类型在Item 30中介绍)。这也是组合重载和通用引用使用是糟糕主意的原因:通用引用的实现会匹配比开发者预期要多得多的参数类型。
|
||||
使用通用引用的函数在C++中是最贪婪的函数。它们几乎可以精确匹配任何类型的实参(极少不适用的实参在[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)中介绍)。这也是把重载和通用引用组合在一块是糟糕主意的原因:通用引用的实现会匹配比开发者预期要多得多的实参类型。
|
||||
|
||||
一个更容易掉入这种陷阱的例子是完美转发构造函数。简单对`logAndAdd`例子进行改造就可以说明这个问题。将使用`std::string`类型改为自定义`Person`类型即可:
|
||||
|
||||
```cpp
|
||||
class Person
|
||||
{
|
||||
public:
|
||||
template<typename T>
|
||||
explicit Person(T&& n) :name(std::forward<T>(n)) {} // perfect forwarding ctor; initializes data member
|
||||
explicit Person(int idx): name(nameFromIdx(idx)) {}
|
||||
...
|
||||
private:
|
||||
std::string name;
|
||||
};
|
||||
```
|
||||
|
||||
在`logAndAdd`的例子中,传递一个不是int的整型变量(比如`std::size_t, short, long`等)会调用通用引用的构造函数而不是int的构造函数,这会导致编译错误。这里这个问题甚至更糟糕,因为`Person`中存在的重载比肉眼看到的更多。在Item 17中说明,在适当的条件下,C++会生成拷贝和移动构造函数,即使类包含了模板构造也在合适的条件范围内。如果拷贝和移动构造被生成,Person类看起来就像这样:
|
||||
|
||||
```cpp
|
||||
class Person
|
||||
{
|
||||
public:
|
||||
template<typename T>
|
||||
explicit Person(T&& n) :name(std::forward<T>(n)) {} // perfect forwarding ctor
|
||||
explicit Person(int idx); // int ctor
|
||||
|
||||
Person(const Person& rhs); // copy ctor(complier-generated)
|
||||
Person(Person&& rhs); // move ctor (compiler-generated)
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
只有你在花了很多时间在编译器领域时,下面的行为才变得直观(译者注:这里意思就是这种实现会导致不符合人类直觉的结果,下面就解释了这种现象的原因)
|
||||
|
||||
```cpp
|
||||
Person p("Nancy");
|
||||
auto cloneOfP(p); // create new Person from p; this won't compile!
|
||||
```
|
||||
|
||||
这里我们视图通过一个`Person`实例创建另一个`Person`,显然应该调用拷贝构造即可(p是左值,我们可以思考通过移动操作来消除拷贝的开销)。但是这份代码不是调用拷贝构造,而是调用完美转发构造。然后,该函数将尝试使用Person对象p初始化`Person`的`std::string`的数据成员,编译器就会报错。
|
||||
|
||||
“为什么?”你可能会疑问,“为什么拷贝构造会被完美转发构造替代?我们显然想拷贝Person到另一个Person”。确实我们是这样想的,但是编译器严格遵循C++的规则,这里的相关规则就是控制对重载函数调用的解析规则。
|
||||
|
||||
编译器的理由如下:`cloneOfP`被`non-const`左值p初始化,这意味着可以实例化模板构造函数为采用`Person`的`non-const`左值。实例化之后,`Person`类看起来是这样的:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
explicit Person(Person& n) // instantiated from
|
||||
: name(std::forward<Person&>(n)) {} // perfect-forwarding // template
|
||||
explicit Person(int idx); // as before
|
||||
Person(const Person& rhs); // copy ctor (compiler-generated)
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
在`auto cloneOfP(p);`语句中,p被传递给拷贝构造或者完美转发构造。调用拷贝构造要求在p前加上const的约束,而调用完美转发构造不需要任何条件,所以编译器按照规则:采用最佳匹配,这里调用了完美转发的实例化的构造函数。
|
||||
|
||||
如果我们将本例中的传递的参数改为const的,会得到完全不同的结果:
|
||||
|
||||
```cpp
|
||||
const Person cp("Nancy");
|
||||
auto cloneOfP(cp); // call copy constructor!
|
||||
```
|
||||
|
||||
因为被拷贝的对象是const,是拷贝构造函数的精确匹配。虽然模板参数可以实例化为完全一样的函数签名:
|
||||
一个更容易掉入这种陷阱的例子是写一个完美转发构造函数。简单对`logAndAdd`例子进行改造就可以说明这个问题。不用写接受`std::string`或者用索引查找`std::string`的自由函数,只是想一个构造函数有着相同操作的`Person`类:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
explicit Person(const Person& n); // instantiated from template
|
||||
|
||||
Person(const Person& rhs); // copy ctor(compiler-generated)
|
||||
...
|
||||
template<typename T>
|
||||
explicit Person(T&& n) //完美转发的构造函数,初始化数据成员
|
||||
: name(std::forward<T>(n)) {}
|
||||
|
||||
explicit Person(int idx) //int的构造函数
|
||||
: name(nameFromIdx(idx)) {}
|
||||
…
|
||||
|
||||
private:
|
||||
std::string name;
|
||||
};
|
||||
```
|
||||
|
||||
但是无所谓,因为重载规则规定当模板实例化函数和非模板函数(或者称为“正常”函数)匹配优先级相当时,优先使用“正常”函数。拷贝构造函数(正常函数)因此胜过具有相同签名的模板实例化函数。
|
||||
就像在`logAndAdd`的例子中,传递一个不是`int`的整型变量(比如`std::size_t`,`short`,`long`等)会调用通用引用的构造函数而不是`int`的构造函数,这会导致编译错误。这里这个问题甚至更糟糕,因为`Person`中存在的重载比肉眼看到的更多。在[Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)中说明,在适当的条件下,C++会生成拷贝和移动构造函数,即使类包含了模板化的构造函数,模板函数能实例化产生与拷贝和移动构造函数一样的签名,也在合适的条件范围内。如果拷贝和移动构造被生成,`Person`类看起来就像这样:
|
||||
|
||||
(如果你想知道为什么编译器在生成一个拷贝构造函数时还会模板实例化一个相同签名的函数,参考Item17)
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
template<typename T> //完美转发的构造函数
|
||||
explicit Person(T&& n)
|
||||
: name(std::forward<T>(n)) {}
|
||||
|
||||
当继承纳入考虑范围时,完美转发的构造函数与编译器生成的拷贝、移动操作之间的交互会更加复杂。尤其是,派生类的拷贝和移动操作会表现的非常奇怪。来看一下:
|
||||
explicit Person(int idx); //int的构造函数
|
||||
|
||||
Person(const Person& rhs); //拷贝构造函数(编译器生成)
|
||||
Person(Person&& rhs); //移动构造函数(编译器生成)
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
只有你在花了很多时间在编译器领域时,下面的行为才变得直观(译者注:这里意思就是这种实现会导致不符合人类直觉的结果,下面就解释了这种现象的原因):
|
||||
|
||||
```cpp
|
||||
Person p("Nancy");
|
||||
auto cloneOfP(p); //从p创建新Person;这通不过编译!
|
||||
```
|
||||
|
||||
这里我们试图通过一个`Person`实例创建另一个`Person`,显然应该调用拷贝构造即可。(`p`是左值,我们可以把通过移动操作来完成“拷贝”的想法请出去了。)但是这份代码不是调用拷贝构造函数,而是调用完美转发构造函数。然后,完美转发的函数将尝试使用`Person`对象`p`初始化`Person`的`std::string`数据成员,编译器就会报错。
|
||||
|
||||
“为什么?”你可能会疑问,“为什么拷贝构造会被完美转发构造替代?我们显然想拷贝`Person`到另一个`Person`”。确实我们是这样想的,但是编译器严格遵循C++的规则,这里的相关规则就是控制对重载函数调用的解析规则。
|
||||
|
||||
编译器的理由如下:`cloneOfP`被non-`const`左值`p`初始化,这意味着模板化构造函数可被实例化为采用`Person`类型的non-`const`左值。实例化之后,`Person`类看起来是这样的:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
explicit Person(Person& n) //由完美转发模板初始化
|
||||
: name(std::forward<Person&>(n)) {}
|
||||
|
||||
explicit Person(int idx); //同之前一样
|
||||
|
||||
Person(const Person& rhs); //拷贝构造函数(编译器生成的)
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
在这个语句中,
|
||||
|
||||
```cpp
|
||||
auto cloneOfP(p);
|
||||
```
|
||||
|
||||
其中`p`被传递给拷贝构造函数或者完美转发构造函数。调用拷贝构造函数要求在`p`前加上`const`的约束来满足函数形参的类型,而调用完美转发构造不需要加这些东西。从模板产生的重载函数是更好的匹配,所以编译器按照规则:调用最佳匹配的函数。“拷贝”non-`const`左值类型的`Person`交由完美转发构造函数处理,而不是拷贝构造函数。
|
||||
|
||||
如果我们将本例中的传递的对象改为`const`的,会得到完全不同的结果:
|
||||
|
||||
```cpp
|
||||
const Person cp("Nancy"); //现在对象是const的
|
||||
auto cloneOfP(cp); //调用拷贝构造函数!
|
||||
```
|
||||
|
||||
因为被拷贝的对象是`const`,是拷贝构造函数的精确匹配。虽然模板化的构造函数可以被实例化为有完全一样的函数签名,
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
explicit Person(const Person& n); //从模板实例化而来
|
||||
|
||||
Person(const Person& rhs); //拷贝构造函数(编译器生成的)
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
但是没啥影响,因为重载规则规定当模板实例化函数和非模板函数(或者称为“正常”函数)匹配优先级相当时,优先使用“正常”函数。拷贝构造函数(正常函数)因此胜过具有相同签名的模板实例化函数。
|
||||
|
||||
(如果你想知道为什么编译器在生成一个拷贝构造函数时还会模板实例化一个相同签名的函数,参考[Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)。)
|
||||
|
||||
当继承纳入考虑范围时,完美转发的构造函数与编译器生成的拷贝、移动操作之间的交互会更加复杂。尤其是,派生类的拷贝和移动操作的传统实现会表现得非常奇怪。来看一下:
|
||||
|
||||
```cpp
|
||||
class SpecialPerson: public Person {
|
||||
public:
|
||||
SpecialPerson(const SpecialPerson& rhs) :Person(rhs)
|
||||
{...} // copy ctor; calls base class forwarding ctor!
|
||||
SpecialPerson(SpecialPerson&& rhs): Person(std::move(rhs))
|
||||
{...} // move ctor; calls base class forwarding ctor!
|
||||
SpecialPerson(const SpecialPerson& rhs) //拷贝构造函数,调用基类的
|
||||
: Person(rhs) //完美转发构造函数!
|
||||
{ … }
|
||||
|
||||
SpecialPerson(SpecialPerson&& rhs) //移动构造函数,调用基类的
|
||||
: Person(std::move(rhs)) //完美转发构造函数!
|
||||
{ … }
|
||||
};
|
||||
```
|
||||
|
||||
如同注释表示的,派生类的拷贝和移动构造函数没有调用基类的拷贝和移动构造函数,而是调用了基类的完美转发构造函数!为了理解原因,要知道派生类使用`SpecialPerson`作为参数传递给其基类,然后通过模板实例化和重载解析规则作用于基类。最终,代码无法编译,因为`std::string`没有`SpecialPerson`的构造函数。
|
||||
如同注释表示的,派生类的拷贝和移动构造函数没有调用基类的拷贝和移动构造函数,而是调用了基类的完美转发构造函数!为了理解原因,要知道派生类将`SpecialPerson`类型的实参传递给其基类,然后通过模板实例化和重载解析规则作用于基类`Person`。最终,代码无法编译,因为`std::string`没有接受一个`SpecialPerson`的构造函数。
|
||||
|
||||
我希望到目前为止,已经说服了你,如果可能的话,避免对通用引用的函数进行重载。但是,如果在通用引用上重载是糟糕的主意,那么如果需要可转发大多数类型的参数,但是对于某些类型又要特殊处理应该怎么办?存在多种办法。实际上,下一个Item,Item27专门来讨论这个问题,敬请阅读。
|
||||
我希望到目前为止,已经说服了你,如果可能的话,避免对通用引用形参的函数进行重载。但是,如果在通用引用上重载是糟糕的主意,那么如果需要可转发大多数实参类型的函数,但是对于某些实参类型又要特殊处理应该怎么办?存在多种办法。实际上,下一个条款,[Item27](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item27.md)专门来讨论这个问题,敬请阅读。
|
||||
|
||||
### 需要记住的事
|
||||
**请记住:**
|
||||
|
||||
- 对通用引用参数的函数进行重载,调用机会会比你期望的多得多
|
||||
- 完美转发构造函数是糟糕的实现,因为对于`non-const`左值不会调用拷贝构造而是完美转发构造,而且会劫持派生类对于基类的拷贝和移动构造
|
||||
- 对通用引用形参的函数进行重载,通用引用函数的调用机会几乎总会比你期望的多得多。
|
||||
- 完美转发构造函数是糟糕的实现,因为对于non-`const`左值,它们比拷贝构造函数而更匹配,而且会劫持派生类对于基类的拷贝和移动构造函数的调用。
|
||||
|
@ -1,61 +1,63 @@
|
||||
## Item 27: Familiarize yourself with alternatives to overloading on universal references
|
||||
## 条款二十七:熟悉通用引用重载的替代方法
|
||||
|
||||
## Item27:熟悉通用引用重载的替代方法
|
||||
**Item 27: Familiarize yourself with alternatives to overloading on universal references**
|
||||
|
||||
Item 26中说明了对使用通用引用参数的函数,无论是独立函数还是成员函数(尤其是构造函数),进行重载都会导致一系列问题。但是也提供了一些示例,如果能够按照我们期望的方式运行,重载可能也是有用的。这个Item探讨了几种通过避免在通用引用上重载的设计或者通过限制通用引用可以匹配的参数类型的方式来实现所需行为的方案。
|
||||
[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中说明了对使用通用引用形参的函数,无论是独立函数还是成员函数(尤其是构造函数),进行重载都会导致一系列问题。但是也提供了一些示例,如果能够按照我们期望的方式运行,重载可能也是有用的。这个条款探讨了几种,通过避免在通用引用上重载的设计,或者通过限制通用引用可以匹配的参数类型,来实现所期望行为的方法。
|
||||
|
||||
讨论基于Item 26中的示例,如果你还没有阅读Item 26,请先阅读在继续本Item的阅读。
|
||||
讨论基于[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中的示例,如果你还没有阅读那个条款,请先阅读那个条款再继续。
|
||||
|
||||
### Abandon overloading
|
||||
### 放弃重载
|
||||
|
||||
在Item 26中的第一个例子中,`logAndAdd`代表了许多函数,这些函数可以使用不同的名字来避免在通用引用上的重载的弊端。例如两个重载的`logAndAdd`函数,可以分别改名为`logAndAddName`和`logAndAddNameIdx`。但是,这种方式不能用在第二个例子,Person构造函数中,因为构造函数的名字本类名固定了。此外谁愿意放弃重载呢?
|
||||
在[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中的第一个例子中,`logAndAdd`是许多函数的代表,这些函数可以使用不同的名字来避免在通用引用上的重载的弊端。例如两个重载的`logAndAdd`函数,可以分别改名为`logAndAddName`和`logAndAddNameIdx`。但是,这种方式不能用在第二个例子,`Person`构造函数中,因为构造函数的名字被语言固定了(译者注:即构造函数名与类名相同)。此外谁愿意放弃重载呢?
|
||||
|
||||
### Pass by const T&
|
||||
### 传递const T&
|
||||
|
||||
一种替代方案是退回到C++98,然后将通用引用替换为const的左值引用。事实上,这是Item 26中首先考虑的方法。缺点是效率不高,会有拷贝的开销。现在我们知道了通用引用和重载的组合会导致问题,所以放弃一些效率来确保行为正确简单可能也是一种不错的折中。
|
||||
一种替代方案是退回到C++98,然后将传递通用引用替换为传递lvalue-refrence-to-`const`。事实上,这是[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中首先考虑的方法。缺点是效率不高。现在我们知道了通用引用和重载的相互关系,所以放弃一些效率来确保行为正确简单可能也是一种不错的折中。
|
||||
|
||||
### Pass by value
|
||||
### 传值
|
||||
|
||||
通常在不增加复杂性的情况下提高性能的一种方法是,将按引用传递参数替换为按值传递,这是违反直觉的。该设计遵循Item 41中给出的建议,即在你知道要拷贝时就按值传递,因此会参考Item 41来详细讨论如何设计与工作,效率如何。这里,在Person的例子中展示:
|
||||
通常在不增加复杂性的情况下提高性能的一种方法是,将按传引用形参替换为按值传递,这是违反直觉的。该设计遵循[Item41](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md)中给出的建议,即在你知道要拷贝时就按值传递,因此会参考那个条款来详细讨论如何设计与工作,效率如何。这里,在`Person`的例子中展示:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
explicit Person(std::string p) // replace T&& ctor; see
|
||||
: name(std::move(n)) {} // Item 41 for use of std::move
|
||||
explicit Person(std::string n) //代替T&&构造函数,
|
||||
: name(std::move(n)) {} //std::move的使用见条款41
|
||||
|
||||
explicit Person(int idx)
|
||||
: name(nameFromIdx(idx)) {}
|
||||
...
|
||||
explicit Person(int idx) //同之前一样
|
||||
: name(nameFromIdx(idx)) {}
|
||||
…
|
||||
|
||||
private:
|
||||
std::string name;
|
||||
std::string name;
|
||||
};
|
||||
```
|
||||
|
||||
因为没有`std::string`构造器可以接受整型参数,所有`int`或者其他整型变量(比如`std::size_t、short、long`等)都会使用`int`类型重载的构造函数。相似的,所有`std::string`类似的参数(字面量等)都会使用`std::string`类型的重载构造函数。没有意外情况。我想你可能会说有些人想要使用0或者NULL会调用`int`重载的构造函数,但是这些人应该参考Item 8反复阅读指导使用0或者NULL作为空指针让他们恶心。
|
||||
因为没有`std::string`构造函数可以接受整型参数,所有`int`或者其他整型变量(比如`std::size_t`、`short`、`long`等)都会使用`int`类型重载的构造函数。相似的,所有`std::string`类似的实参(还有可以用来创建`std::string`的东西,比如字面量“`Ruth`”等)都会使用`std::string`类型的重载构造函数。没有意外情况。我想你可能会说有些人使用`0`或者`NULL`指代空指针会调用`int`重载的构造函数让他们很吃惊,但是这些人应该参考[Item8](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item8.md)反复阅读直到使用`0`或者`NULL`作为空指针让他们恶心。
|
||||
|
||||
### Use Tag dispatch
|
||||
### 使用*tag dispatch*
|
||||
|
||||
传递`const`左值引用参数以及按值传递都不支持完美转发。如果使用通用引用的动机是完美转发,我们就只能使用通用引用了,没有其他选择。但是又不想放弃重载。所以如果不放弃重载又不放弃通用引用,如何避免咋通用引用上重载呢?
|
||||
传递lvalue-reference-to-`const`以及按值传递都不支持完美转发。如果使用通用引用的动机是完美转发,我们就只能使用通用引用了,没有其他选择。但是又不想放弃重载。所以如果不放弃重载又不放弃通用引用,如何避免在通用引用上重载呢?
|
||||
|
||||
实际上并不难。通过查看重载的所有参数以及调用的传入参数,然后选择最优匹配的函数----计算所有参数和变量的组合。通用引用通常提供了最优匹配,但是如果通用引用是包含其他非通用引用参数列表的一部分,则不是通用引用的部分会影响整体。这基本就是tag dispatch 方法,下面的示例会使这段话更容易理解。
|
||||
实际上并不难。通过查看所有重载的所有形参以及调用点的所有传入实参,然后选择最优匹配的函数——考虑所有形参/实参的组合。通用引用通常提供了最优匹配,但是如果通用引用是包含其他**非**通用引用的形参列表的一部分,则非通用引用形参的较差匹配会使有一个通用引用的重载版本不被运行。这就是*tag dispatch*方法的基础,下面的示例会使这段话更容易理解。
|
||||
|
||||
我们将tag dispatch应用于`logAndAdd`例子,下面是原来的代码,以免你找不到Item 26的代码位置:
|
||||
我们将标签分派应用于`logAndAdd`例子,下面是原来的代码,以免你再分心回去查看:
|
||||
|
||||
```cpp
|
||||
std::multiset<std::string> names; // global data structure
|
||||
template<typename T> // make log entry and add
|
||||
std::multiset<std::string> names; //全局数据结构
|
||||
|
||||
template<typename T> //志记信息,将name添加到数据结构
|
||||
void logAndAdd(T&& name)
|
||||
{
|
||||
auto now = std::chrono::system_clokc::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(std::forward<T>(name));
|
||||
auto now = std::chrono::system_clokc::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(std::forward<T>(name));
|
||||
}
|
||||
```
|
||||
|
||||
就其本身而言,功能执行没有问题,但是如果引入一个`int`类型的重载,就会重新陷入Item 26中描述的麻烦。这个Item的目标是避免它。不通过重载,我们重新实现`logAndAdd`函数分拆为两个函数,一个针对整型值,一个针对其他。`logAndAdd`本身接受所有的类型。
|
||||
就其本身而言,功能执行没有问题,但是如果引入一个`int`类型的重载来用索引查找对象,就会重新陷入[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中描述的麻烦。这个条款的目标是避免它。不通过重载,我们重新实现`logAndAdd`函数分拆为两个函数,一个针对整型值,一个针对其他。`logAndAdd`本身接受所有实参类型,包括整型和非整型。
|
||||
|
||||
这两个真正执行逻辑的函数命名为`logAndAddImpl`使用重载。一个函数接受通用引用参数。所以我们同时使用了重载和通用引用。但是每个函数接受第二个参数,表征传入的参数是否为整型。这第二个参数可以帮助我们避免陷入到Item 26中提到的麻烦中,因为我们将其安排为第二个参数决定选择哪个重载函数。
|
||||
这两个真正执行逻辑的函数命名为`logAndAddImpl`,即我们使用重载。其中一个函数接受通用引用。所以我们同时使用了重载和通用引用。但是每个函数接受第二个形参,表征传入的实参是否为整型。这第二个形参可以帮助我们避免陷入到[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中提到的麻烦中,因为我们将其安排为第二个实参决定选择哪个重载函数。
|
||||
|
||||
是的,我知道,“不要在啰嗦了,赶紧亮出代码”。没有问题,代码如下,这是最接近正确版本的:
|
||||
|
||||
@ -63,257 +65,273 @@ void logAndAdd(T&& name)
|
||||
template<typename T>
|
||||
void logAndAdd(T&& name)
|
||||
{
|
||||
logAndAddImpl(std::forward<T>(name),
|
||||
std::is_integral<T>()); // not quite correct
|
||||
logAndAddImpl(std::forward<T>(name),
|
||||
std::is_integral<T>()); //不那么正确
|
||||
}
|
||||
```
|
||||
|
||||
这个函数转发它的参数给`logAndAddImpl`函数,但是多传递了一个表示是否T为整型的变量。至少,这就是应该做的。对于右值的整型参数来说,这也是正确的。但是如同Item 28中说明,如果左值参数传递给通用引用`name`,类型推断会使左值引用。所以如果左值int被传入`logAndAdd`,T将被推断为`int&`。这不是一个整型类型,因为引用不是整型类型。这意味着`std::is_integral<T>`对于左值参数返回false,即使确实传入了整型值。
|
||||
这个函数转发它的形参给`logAndAddImpl`函数,但是多传递了一个表示形参`T`是否为整型的实参。至少,这就是应该做的。对于右值的整型实参来说,这也是正确的。但是如同[Item28](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md)中说明,如果左值实参传递给通用引用`name`,对`T`类型推断会得到左值引用。所以如果左值`int`被传入`logAndAdd`,`T`将被推断为`int&`。这不是一个整型类型,因为引用不是整型类型。这意味着`std::is_integral<T>`对于任何左值实参返回false,即使确实传入了整型值。
|
||||
|
||||
意识到这个问题基本相当于解决了它,因为C++标准库有一个类型trait(参见Item 9),`std::remove_reference`,函数名字就说明做了我们希望的:移除引用。所以正确实现的代码应该是这样:
|
||||
意识到这个问题基本相当于解决了它,因为C++标准库有一个*type trait*(参见[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md)),`std::remove_reference`,函数名字就说明做了我们希望的:移除类型的引用说明符。所以正确实现的代码应该是这样:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void logAndAdd(T&& name)
|
||||
{
|
||||
logAndAddImpl(std::forward<T>(name),
|
||||
std::is_instegral<typename std::remove_reference<T>::type>());
|
||||
logAndAddImpl(
|
||||
std::forward<T>(name),
|
||||
std::is_instegral<typename std::remove_reference<T>::type>()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这个代码很巧妙。(在C++14中,你可以通过`std::remove_reference_t<T>`来简化写法,参看Item 9)
|
||||
这个代码很巧妙。(在C++14中,你可以通过`std::remove_reference_t<T>`来简化写法,参看[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md))
|
||||
|
||||
处理完之后,我们可以将注意力转移到名为`logAndAddImpl`的函数上了。有两个重载函数,第一个仅用于非整型类型(即`std::is_instegral<typename std::remove_reference<T>::type>()`是`false`):
|
||||
处理完之后,我们可以将注意力转移到名为`logAndAddImpl`的函数上了。有两个重载函数,第一个仅用于非整型类型(即`std::is_instegral<typename std::remove_reference<T>::type>`是false):
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void logAndAddImpl(T&& name, std::false_type) // 高亮为std::false_type
|
||||
template<typename T> //非整型实参:添加到全局数据结构中
|
||||
void logAndAddImpl(T&& name, std::false_type) //译者注:高亮std::false_type
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(std::forward<T>(name));
|
||||
auto now = std::chrono::system_clock::now();
|
||||
log(now, "logAndAdd");
|
||||
names.emplace(std::forward<T>(name));
|
||||
}
|
||||
```
|
||||
|
||||
一旦你理解了高亮参数的含义代码就很直观。概念上,`logAndAdd`传递一个布尔值给`logAndAddImpl`表明是否传入了一个整型类型,但是`true`和`false`是运行时值,我们需要使用编译时决策来选择正确的`logAndAddImpl`重载。这意味着我们需要一个类型对应`true`,`false`同理。这个需要是经常出现的,所以标准库提供了这样两个命名`std::true_type and std::false_type`。`logAndAdd`传递给`logAndAddImpl`的参数类型取决于T是否整型,如果T是整型,它的类型就继承自`std::true_type`,反之继承自`std::false_type`。最终的结果就是,当T不是整型类型时,这个`logAndAddImpl`重载会被调用。
|
||||
一旦你理解了高亮参数的含义,代码就很直观。概念上,`logAndAdd`传递一个布尔值给`logAndAddImpl`表明是否传入了一个整型类型,但是`true`和`false`是**运行时**值,我们需要使用重载决议——**编译时**决策——来选择正确的`logAndAddImpl`重载。这意味着我们需要一个**类型**对应`true`,另一个不同的类型对应`false`。这个需要是经常出现的,所以标准库提供了这样两个命名`std::true_type`和`std::false_type`。`logAndAdd`传递给`logAndAddImpl`的实参是个对象,如果`T`是整型,对象的类型就继承自`std::true_type`,反之继承自`std::false_type`。最终的结果就是,当`T`不是整型类型时,这个`logAndAddImpl`重载是个可供调用的候选者。
|
||||
|
||||
第二个重载覆盖了相反的场景:当T是整型类型。在这个场景中,`logAndAddImpl`简单找到下标处的`name`,然后传递给`logAndAdd`:
|
||||
第二个重载覆盖了相反的场景:当`T`是整型类型。在这个场景中,`logAndAddImpl`简单找到对应传入索引的名字,然后传递给`logAndAdd`:
|
||||
|
||||
```cpp
|
||||
std::string nameFromIdx(int idx); // as in item 26
|
||||
void logAndAddImpl(int idx, std::true_type) // 高亮:std::true_type
|
||||
std::string nameFromIdx(int idx); //与条款26一样,整型实参:查找名字并用它调用logAndAdd
|
||||
void logAndAddImpl(int idx, std::true_type) //译者注:高亮std::true_type
|
||||
{
|
||||
logAndAdd(nameFromIdx(idx));
|
||||
}
|
||||
```
|
||||
|
||||
通过下标找到对应的`name`,然后让`logAndAddImpl`传递给`logAndAdd`,我们避免了将日志代码放入这个`logAndAddImpl`重载中。
|
||||
通过索引找到对应的`name`,然后让`logAndAddImpl`传递给`logAndAdd`(名字会被再`std::forward`给另一个`logAndAddImpl`重载),我们避免了将日志代码放入这个`logAndAddImpl`重载中。
|
||||
|
||||
在这个设计中,类型`std::true_type`和`std::false_type`是“标签”,其唯一目的就是强制重载解析按照我们的想法来执行。注意到我们甚至没有对这些参数进行命名。他们在运行时毫无用处,事实上我们希望编译器可以意识到这些tag参数是无用的然后在程序执行时优化掉它们(至少某些时候有些编译器会这样做)。这种在`logAndAdd`内部的通过tag来实现重载实现函数的“分发”,因此这个设计名称为:**tag dispatch**。这是模板元编程的标准构建模块,你对现代C++库中的代码了解越多,你就会越多遇到这种设计。
|
||||
在这个设计中,类型`std::true_type`和`std::false_type`是“标签”(tag),其唯一目的就是强制重载解析按照我们的想法来执行。注意到我们甚至没有对这些参数进行命名。他们在运行时毫无用处,事实上我们希望编译器可以意识到这些标签形参没被使用,然后在程序执行时优化掉它们。(至少某些时候有些编译器会这样做。)通过创建标签对象,在`logAndAdd`内部将重载实现函数的调用“分发”(*dispatch*)给正确的重载。因此这个设计名称为:*tag dispatch*。这是模板元编程的标准构建模块,你对现代C++库中的代码了解越多,你就会越多遇到这种设计。
|
||||
|
||||
就我们的目的而言,tag dispatch的重要之处在于它可以允许我们组合重载和通用引用使用,而没有Item 26中提到的问题。分发函数---`logAndAdd`----接受一个没有约束的通用引用参数,但是这个函数没有重载。实现函数---`logAndAddImpl`----是重载的,一个接受通用引用参数,但是重载规则不仅依赖通用引用参数,还依赖新引入的tag参数。结果是tag来决定采用哪个重载函数。通用引用参数可以生成精确匹配的事实在这里并不重要。(译者注:这里确实比较啰嗦,如果理解了上面的内容,这段完全可以没有。)
|
||||
就我们的目的而言,*tag dispatch*的重要之处在于它可以允许我们组合重载和通用引用使用,而没有[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中提到的问题。分发函数——`logAndAdd`——接受一个没有约束的通用引用参数,但是这个函数没有重载。实现函数——`logAndAddImpl`——是重载的,一个接受通用引用参数,但是重载规则不仅依赖通用引用形参,还依赖新引入的标签形参,标签值设计来保证有不超过一个的重载是合适的匹配。结果是标签来决定采用哪个重载函数。通用引用参数可以生成精确匹配的事实在这里并不重要。(译者注:这里确实比较啰嗦,如果理解了上面的内容,这段完全可以没有。)
|
||||
|
||||
### Constraining templates that take universal references(约束使用通用引用的模板)
|
||||
### 约束使用通用引用的模板
|
||||
|
||||
tag dispatch的关键是存在单独一个函数(没有重载)给客户端API。这个单独的函数分发给具体的实现函数。创建一个没有重载的分发函数通常是容易的,但是Item 26中所述第二个问题案例是`Person`类的完美转发构造函数,是个例外。编译器可能会自行生成拷贝和移动构造函数,所以即使你只写了一个构造函数并在其中使用tag dispatch,编译器生成的构造函数也打破了你的期望。
|
||||
*tag dispatch*的关键是存在单独一个函数(没有重载)给客户端API。这个单独的函数分发给具体的实现函数。创建一个没有重载的分发函数通常是容易的,但是[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中所述第二个问题案例是`Person`类的完美转发构造函数,是个例外。编译器可能会自行生成拷贝和移动构造函数,所以即使你只写了一个构造函数并在其中使用*tag dispatch*,有一些对构造函数的调用也被编译器生成的函数处理,绕过了分发机制。
|
||||
|
||||
实际上,真正的问题不是编译器生成的函数会绕过tag diapatch设计,而是不总会绕过tag dispatch。你希望类的拷贝构造总是处理该类型的`non-const`左值构造请求,但是如同Item 26中所述,提供具有通用引用的构造函数会使通用引用构造函数被调用而不是拷贝构造函数。还说明了当一个基类声明了完美转发构造函数,派生类实现自己的拷贝和移动构造函数时会发生错误的调用(调用基类的完美转发构造函数而不是基类的拷贝或者移动构造)
|
||||
实际上,真正的问题不是编译器生成的函数会绕过*tag diapatch*设计,而是不**总**会绕过去。你希望类的拷贝构造函数总是处理该类型的左值拷贝请求,但是如同[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中所述,提供具有通用引用的构造函数,会使通用引用构造函数在拷贝non-`const`左值时被调用(而不是拷贝构造函数)。那个条款还说明了当一个基类声明了完美转发构造函数,派生类实现自己的拷贝和移动构造函数时会调用那个完美转发构造函数,尽管正确的行为是调用基类的拷贝或者移动构造。
|
||||
|
||||
这种情况,采用通用引用的重载函数通常比期望的更加贪心,但是有不满足使用tag dispatch的条件。你需要不同的技术,可以让你确定允许使用通用引用模板的条件。朋友你需要的就是`std::enable_if`。
|
||||
这种情况,采用通用引用的重载函数通常比期望的更加贪心,虽然不像单个分派函数一样那么贪心,而又不满足使用*tag dispatch*的条件。你需要另外的技术,可以让你确定允许使用通用引用模板的条件。朋友,你需要的就是`std::enable_if`。
|
||||
|
||||
`std::enable_if`可以给你提供一种强制编译器执行行为的方法,即使特定模板不存在。这种模板也会被禁止。默认情况下,所有模板是启用的,但是使用`std::enable_if`可以使得仅在条件满足时模板才启用。在这个例子中,我们只在传递的参数类型不是`Person`使用`Person`的完美转发构造函数。如果传递的参数是`Person`,我们要禁止完美转发构造函数(即让编译器忽略它),因此就是拷贝或者移动构造函数处理,这就是我们想要使用`Person`初始化另一个`Person`的初衷。
|
||||
`std::enable_if`可以给你提供一种强制编译器执行行为的方法,像是特定模板不存在一样。这种模板被称为被**禁止**(disabled)。默认情况下,所有模板是**启用**的(enabled),但是使用`std::enable_if`可以使得仅在`std::enable_if`指定的条件满足时模板才启用。在这个例子中,我们只在传递的类型不是`Person`时使用`Person`的完美转发构造函数。如果传递的类型是`Person`,我们要禁止完美转发构造函数(即让编译器忽略它),因为这会让拷贝或者移动构造函数处理调用,这是我们想要使用`Person`初始化另一个`Person`的初衷。
|
||||
|
||||
这个主意听起来并不难,但是语法比较繁杂,尤其是之前没有接触过的话,让我慢慢引导你。有一些使用`std::enbale_if`的样板,让我们从这里开始。下面的代码是`Person`完美转发构造函数的声明,我仅展示声明,因为实现部分跟Item 26中没有区别。
|
||||
这个主意听起来并不难,但是语法比较繁杂,尤其是之前没有接触过的话,让我慢慢引导你。有一些`std::enbale_if`的contidion(条件)部分的样板,让我们从这里开始。下面的代码是`Person`完美转发构造函数的声明,多展示`std::enable_if`的部分来简化使用难度。我仅展示构造函数的声明,因为`std::enable_if`的使用对函数实现没影响。实现部分跟[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中没有区别。
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
template<typename T,
|
||||
typename = typename std::enable_if<condition>::type> // 本行高亮
|
||||
explicit Person(T&& n);
|
||||
...
|
||||
template<typename T,
|
||||
typename = typename std::enable_if<condition>::type> //译者注:本行高亮,condition为某其他特定条件
|
||||
explicit Person(T&& n);
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
为了理解高亮部分发生了什么,我很遗憾的表示你要自行查询语法含义,因为详细解释需要花费一定空间和时间,而本书并没有足够的空间(在你自行学习过程中,请研究"SFINAE"以及`std::enable_if`,因为“SFINAE”就是使`std::enable_if`起作用的技术)。这里我想要集中讨论条件的表示,该条件表示此构造函数是否启用。
|
||||
为了理解高亮部分发生了什么,我很遗憾的表示你要自行参考其他代码,因为详细解释需要花费一定空间和时间,而本书并没有足够的空间(在你自行学习过程中,请研究“SFINAE”以及`std::enable_if`,因为“SFINAE”就是使`std::enable_if`起作用的技术)。这里我想要集中讨论条件的表示,该条件表示此构造函数是否启用。
|
||||
|
||||
这里我们想表示的条件是确认T不是`Person`类型,即模板构造函数应该在T不是`Person`类型的时候启用。因为type trait可以确定两个对象类型是否相同(`std::is_same`),看起来我们需要的就是`!std::is_same<Person, T>::value`(注意语句开始的!,我们想要的是不相同)。这很接近我们想要的了,但是不完全正确,因为如同Item 28中所述,对于通用引用的类型推导,如果是左值的话会推导成左值引用,比如这个代码:
|
||||
这里我们想表示的条件是确认`T`不是`Person`类型,即模板构造函数应该在`T`不是`Person`类型的时候启用。多亏了*type trait*可以确定两个对象类型是否相同(`std::is_same`),看起来我们需要的就是`!std::is_same<Person, T>::value`(注意语句开始的`!`,我们想要的是**不**相同)。这很接近我们想要的了,但是不完全正确,因为如同[Item28](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item28.md)中所述,使用左值来初始化通用引用的话会推导成左值引用,比如这个代码:
|
||||
|
||||
```cpp
|
||||
Person p("Nancy");
|
||||
auto cloneOfP(p); // initialize from lvalue
|
||||
auto cloneOfP(p); //用左值初始化
|
||||
```
|
||||
|
||||
T的类型在通用引用的构造函数中被推导为`Person&`。`Person`和`Person&`类型是不同的,`std::is_same`对比`std::is_same<Person, Person&>::value`会是`false`。
|
||||
`T`的类型在通用引用的构造函数中被推导为`Person&`。`Person`和`Person&`类型是不同的,`std::is_same`的结果也反映了:`std::is_same<Person, Person&>::value`是false。
|
||||
|
||||
如果我们更精细考虑仅当T不是`Person`类型才启用模板构造函数,我们会意识到当我们查看T时,应该忽略:
|
||||
如果我们更精细考虑仅当`T`不是`Person`类型才启用模板构造函数,我们会意识到当我们查看`T`时,应该忽略:
|
||||
|
||||
- **是否引用**。对于决定是否通用引用构造器启用的目的来说,`Person, Person&, Person&&`都是跟`Person`一样的。
|
||||
- **是不是`const`或者`volatile`**。如上所述,`const Person , volatile Person , const volatile Person`也是跟`Person`一样的。
|
||||
- **是否是个引用**。对于决定是否通用引用构造函数启用的目的来说,`Person`,`Person&`,`Person&&`都是跟`Person`一样的。
|
||||
- **是不是`const`或者`volatile`**。如上所述,`const Person`,`volatile Person` ,`const volatile Person`也是跟`Person`一样的。
|
||||
|
||||
这意味着我们需要一种方法消除对于`T`的`引用,const, volatile`修饰。再次,标准库提供了这样的功能type trait,就是`std::decay`。`std::decay<T>::value`与`T`是相同的,只不过会移除`引用, const, volatile`的修饰。(这里我没有说出另外的真相,`std::decay`如同其名一样,可以将array或者function退化成指针,参考Item 1,但是在这里讨论的问题中,它刚好合适)。我们想要控制构造器是否启用的条件可以写成:
|
||||
这意味着我们需要一种方法消除对于`T`的引用,`const`,`volatile`修饰。再次,标准库提供了这样功能的*type trait*,就是`std::decay`。`std::decay<T>::value`与`T`是相同的,只不过会移除引用和cv限定符(*cv-qualifiers*,即`const`或`volatile`标识符)的修饰。(这里我没有说出另外的真相,`std::decay`如同其名一样,可以将数组或者函数退化成指针,参考[Item1](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item1.md),但是在这里讨论的问题中,它刚好合适)。我们想要控制构造函数是否启用的条件可以写成:
|
||||
|
||||
```cpp
|
||||
!std::is_same<Person, typename std::decay<T>::type>::value
|
||||
```
|
||||
|
||||
表示`Person`与`T`的类型不同。
|
||||
即`Person`和`T`的类型不同,忽略了所有引用和cv限定符。(如[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md)所述,`std::decay`前的“`typename`”是必需的,因为`std::decay<T>::type`的类型取决于模板形参`T`。)
|
||||
|
||||
将其带回整体代码中,`Person`的完美转发构造函数的声明如下:
|
||||
将其带回上面`std::enable_if`样板的代码中,加上调整一下格式,让各部分如何组合在一起看起来更容易,`Person`的完美转发构造函数的声明如下:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
template<typename T,
|
||||
typename = typename std::enable_if<
|
||||
!std::is_same<Person, typename std::decay<T>::type>::value
|
||||
>::type> // 本行高亮
|
||||
explicit Person(T&& n);
|
||||
...
|
||||
template<
|
||||
typename T,
|
||||
typename = typename std::enable_if<
|
||||
!std::is_same<Person,
|
||||
typename std::decay<T>::type
|
||||
>::value
|
||||
>::type
|
||||
>
|
||||
explicit Person(T&& n);
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
如果你之前从没有看到过这种类型的代码,那你可太幸福了。最后是这种设计是有原因的。当你使用其他机制来避免同时使用重载和通用引用时(你总会这样做),确实应该那样做。不过,一旦你习惯了使用函数语法和尖括号的使用,也不坏。此外,这可以提供你一直想要的行为表现。在上面的声明中,使用`Person`初始化一个`Person`----无论是左值还是右值,`const`还是`volatile`都不会调用到通用引用构造函数。
|
||||
如果你之前从没有看到过这种类型的代码,那你可太幸福了。最后才放出这种设计是有原因的。当你有其他机制来避免同时使用重载和通用引用时(你总会这样做),确实应该那样做。不过,一旦你习惯了使用函数语法和尖括号的使用,也不坏。此外,这可以提供你一直想要的行为表现。在上面的声明中,使用`Person`初始化一个`Person`——无论是左值还是右值,`const`还是non-`const`,`volatile`还是non-`volatile`——都不会调用到通用引用构造函数。
|
||||
|
||||
成功了,对吗?确实!
|
||||
|
||||
当然没有。等会再庆祝。Item 26还有一个情景需要解决,我们需要继续探讨下去。
|
||||
啊,不对。等会再庆祝。[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)还有一个情景需要解决,我们需要继续探讨下去。
|
||||
|
||||
假定从`Person`派生的类以常规方式实现拷贝和移动操作:
|
||||
|
||||
```cpp
|
||||
class SpecialPerson: public Person {
|
||||
public:
|
||||
SpecialPerson(const SpecialPerson& rhs): Person(rhs)
|
||||
{...} // copy ctor; calls base class forwarding ctor!
|
||||
SpecialPerson(SpecialPerson&& rhs): Person(std::move(rhs))
|
||||
{...} // move ctor; calls base class forwarding ctor!
|
||||
SpecialPerson(const SpecialPerson& rhs) //拷贝构造函数,调用基类的
|
||||
: Person(rhs) //完美转发构造函数!
|
||||
{ … }
|
||||
|
||||
SpecialPerson(SpecialPerson&& rhs) //移动构造函数,调用基类的
|
||||
: Person(std::move(rhs)) //完美转发构造函数!
|
||||
{ … }
|
||||
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
这和Item 26中的代码是一样的,包括注释也是一样。当我们拷贝或者移动一个`SpecialPerson`对象时,我们希望调用基类对应的拷贝和移动构造函数,但是这里,我们将`SpecialPerson`传递给基类的构造器,因为`SpecialPerson`和`Person`类型不同,所以完美转发构造函数是启用的,会实例化为精确匹配的构造函数。生成的精确匹配的构造函数之于重载规则比基类的拷贝或者移动构造函数更优,所以这里的代码,拷贝或者移动`SpecialPerson`对象就会调用`Person`类的完美转发构造函数来执行基类的部分。跟Item 26的困境一样。
|
||||
这和[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)中的代码是一样的,包括注释也是一样。当我们拷贝或者移动一个`SpecialPerson`对象时,我们希望调用基类对应的拷贝和移动构造函数,来拷贝或者移动基类部分,但是这里,我们将`SpecialPerson`传递给基类的构造函数,因为`SpecialPerson`和`Person`类型不同(在应用`std::decay`后也不同),所以完美转发构造函数是启用的,会实例化为精确匹配`SpecialPerson`实参的构造函数。相比于派生类到基类的转化——这个转化对于在`Person`拷贝和移动构造函数中把`SpecialPerson`对象绑定到`Person`形参非常重要,生成的精确匹配是更优的,所以这里的代码,拷贝或者移动`SpecialPerson`对象就会调用`Person`类的完美转发构造函数来执行基类的部分。跟[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)的困境一样。
|
||||
|
||||
派生类仅仅是按照常规的规则生成了自己的移动和拷贝构造函数,所以这个问题的解决还要落实在在基类,尤其是控制是否使用`Person`通用引用构造函数启用的条件。现在我们意识到不只是禁止`Person`类型启用模板构造器,而是禁止`Person`以及任何派生自`Person`的类型启用模板构造器。讨厌的继承!
|
||||
派生类仅仅是按照常规的规则生成了自己的移动和拷贝构造函数,所以这个问题的解决还要落实在基类,尤其是控制是否使用`Person`通用引用构造函数启用的条件。现在我们意识到不只是禁止`Person`类型启用模板构造函数,而是禁止`Person`**以及任何派生自`Person`**的类型启用模板构造函数。讨厌的继承!
|
||||
|
||||
你应该不意外在这里看到标准库中也有type trait判断一个类型是否继承自另一个类型,就是`std::is_base_of`。如果`std::is_base_of<T1, T2>`是`true`表示`T2`派生自`T1`。类型系统是自派生的,表示`std::is_base_of<T, T>::value`总是`true`。这就很方便了,我们想要修正关于我们控制`Person`完美转发构造器的启用条件,只有当`T`在消除`引用,const, volatile`修饰之后,并且既不是`Person`又不是`Person`的派生类,才满足条件。所以使用`std::is_base_of`代替`std::is_same`就可以了:
|
||||
你应该不意外在这里看到标准库中也有*type trait*判断一个类型是否继承自另一个类型,就是`std::is_base_of`。如果`std::is_base_of<T1, T2>`是true就表示`T2`派生自`T1`。类型也可被认为是从他们自己派生,所以`std::is_base_of<T, T>::value`总是true。这就很方便了,我们想要修正控制`Person`完美转发构造函数的启用条件,只有当`T`在消除引用和cv限定符之后,并且既不是`Person`又不是`Person`的派生类时,才满足条件。所以使用`std::is_base_of`代替`std::is_same`就可以了:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
template<
|
||||
typename T,
|
||||
typename = typename std::enable_if<
|
||||
!std::is_base_of<Person,
|
||||
typename std::decay<T>::type
|
||||
>::value
|
||||
>::type
|
||||
>
|
||||
explicit Person(T&& n);
|
||||
...
|
||||
template<
|
||||
typename T,
|
||||
typename = typename std::enable_if<
|
||||
!std::is_base_of<Person,
|
||||
typename std::decay<T>::type
|
||||
>::value
|
||||
>::type
|
||||
>
|
||||
explicit Person(T&& n);
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
现在我们终于完成了最终版本。这是C++11版本的代码,如果我们使用C++14,这份代码也可以工作,但是有更简洁一些的写法如下:
|
||||
现在我们终于完成了最终版本。这是C++11版本的代码,如果我们使用C++14,这份代码也可以工作,但是可以使用`std::enable_if`和`std::decay`的别名模板来少写“`typename`”和“`::type`”这样的麻烦东西,产生了下面这样看起来舒爽的代码:
|
||||
|
||||
```cpp
|
||||
class Person { // C++14
|
||||
class Person { //C++14
|
||||
public:
|
||||
template<
|
||||
typename T,
|
||||
typename = std::enable_if_t< // less code here
|
||||
!std::is_base_of<Person,
|
||||
std::decay_t<T> // and here
|
||||
>::value
|
||||
> // and here
|
||||
>
|
||||
explicit Person(T&& n);
|
||||
...
|
||||
template<
|
||||
typename T,
|
||||
typename = std::enable_if_t< //这儿更少的代码
|
||||
!std::is_base_of<Person,
|
||||
std::decay_t<T> //还有这儿
|
||||
>::value
|
||||
> //还有这儿
|
||||
>
|
||||
explicit Person(T&& n);
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
好了,我承认,我又撒谎了。我们还没有完成,但是越发接近最终版本了。非常接近,我保证。
|
||||
|
||||
我们已经知道如何使用`std::enable_if`来选择性禁止`Person`通用引用构造器来使得一些参数确保使用到拷贝或者移动构造器,但是我们还是不知道将其应用于区分整型参数和非整型参数。毕竟,我们的原始目标是解决构造函数模糊性问题。
|
||||
我们已经知道如何使用`std::enable_if`来选择性禁止`Person`通用引用构造函数,来使得一些实参类型确保使用到拷贝或者移动构造函数,但是我们还没将其应用于区分整型参数和非整型参数。毕竟,我们的原始目标是解决构造函数模糊性问题。
|
||||
|
||||
我们需要的工具都介绍过了,我保证都介绍了,
|
||||
(1)加入一个`Person`构造函数重载来处理整型参数
|
||||
(2)约束模板构造器使其对于某些参数禁止
|
||||
使用这些我们讨论过的技术组合起来,就能解决这个问题了:
|
||||
|
||||
```cpp
|
||||
class Person { // C++14
|
||||
public:
|
||||
template<
|
||||
typename T,
|
||||
typename = std::enable_if_t<
|
||||
!std::is_base_of<Person, std::decay_t<T>::value
|
||||
&&
|
||||
!std::is_integral<std::remove_reference_t<T>>::value
|
||||
>
|
||||
>
|
||||
explicit Person(T&& n): name(std::forward<T>(n))
|
||||
{...} // ctor for std::strings and args convertible to strings
|
||||
|
||||
explicit Person(int idx): name(nameFromIdx(idx))
|
||||
{...} // ctor for integral args
|
||||
|
||||
... // copy and move ctors, etc
|
||||
private:
|
||||
std::string name;
|
||||
};
|
||||
```
|
||||
|
||||
看!多么优美!好吧,优美之处只是对于那些迷信模板元编程之人,但是事实却是提出了不仅能工作的方法,而且极具技巧。因为使用了完美转发,所以具有最大效率,因为控制了使用通用引用的范围,可以避免对于大多数参数能实例化精确匹配的滥用问题。
|
||||
|
||||
### Trade-offs (权衡,折中)
|
||||
|
||||
本Item提到的前三个技术---abandoning overloading, passing by const T&, passing by value---在函数调用中指定每个参数的类型。后两个技术----tag dispatch和 constraing template eligibility----使用完美转发,因此不需要指定参数类型。这一基本决定(是否指定类型)有一定后果。
|
||||
|
||||
通常,完美转发更有效率,因为它避免了仅处于符合参数类型而创建临时对象。在`Person`构造函数的例子中,完美转发允许将`Nancy`这种字符串字面量转发到容器内部的`std::string`构造器,不使用完美转发的技术则会创建一个临时对象来满足传入的参数类型。
|
||||
|
||||
但是完美转发也有缺点。·即使某些类型的参数可以传递给特定类型的参数的函数,也无法完美转发。Item 30中探索了这方面的例子。
|
||||
|
||||
第二个问题是当client传递无效参数时错误消息的可理解性。例如假如创建一个`Person`对象的client传递了一个由`char16_t`(一种C++11引入的类型表示16位字符)而不是`char`(`std::string`包含的):
|
||||
|
||||
```cpp
|
||||
Person p(u"Konrad Zuse"); // "Konrad Zuse" consists of characters of type const char16_t
|
||||
```
|
||||
|
||||
使用本Item中讨论的前三种方法,编译器将看到可用的采用`int`或者`std::string`的构造函数,并且它们或多或少会产生错误消息,表示没有可以从`const char16_t`转换为`int`或者`std::string`的方法。
|
||||
|
||||
但是,基于完美转发的方法,`const char16_t`不受约束地绑定到构造函数的参数。从那里将转发到`Person`的`std::string`的构造函数,在这里,调用者传入的内容(`const char16_t`数组)与所需内容(`std::string`构造器可接受的类型)发生的不匹配会被发现。由此产生的错误消息会让人更容易理解,在我使用的编译器上,会产生超过160行错误信息。
|
||||
|
||||
在这个例子中,通用引用仅被转发一次(从`Person`构造器到`std::string`构造器),但是更复杂的系统中,在最终通用引用到达最终判断是否可接受的函数之前会有多层函数调用。通用引用被转发的次数越多,产生的错误消息偏差就越大。许多开发者发现仅此问题就是在性能优先的接口使用通用引用的障碍。(译者注:最后一句话可能翻译有误,待确认)
|
||||
|
||||
在`Person`这个例子中,我们知道转发函数的通用引用参数要支持`std::string`的初始化,所以我们可以用`static_assert`来确认是不是支持。`std::is_constructible` type trait执行编译时测试一个类型的对象是否可以构造另一个不同类型的对象,所以代码可以这样:
|
||||
我们需要的所有东西——我确实意思是所有——是(1)加入一个`Person`构造函数重载来处理整型参数;(2)约束模板构造函数使其对于某些实参禁用。使用这些我们讨论过的技术组合起来,就能解决这个问题了:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
template<typename T,
|
||||
typename = std::enable_if_t<
|
||||
!std::is_base_of<Person, std::decay_t<T>>::value
|
||||
&&
|
||||
!std::is_integral<std::remove_reference_t<T>>::value
|
||||
template<
|
||||
typename T,
|
||||
typename = std::enable_if_t<
|
||||
!std::is_base_of<Person, std::decay_t<T>>::value
|
||||
&&
|
||||
!std::is_integral<std::remove_reference_t<T>>::value
|
||||
>
|
||||
>
|
||||
>
|
||||
explicit Person(T&& n) :name(std::forward<T>(n))
|
||||
{
|
||||
//assert that a std::string can be created from a T object(这里到...高亮)
|
||||
static_assert(
|
||||
std::is_constructible<std::string, T>::value,
|
||||
"Parameter n can't be used to construct a std::string"
|
||||
);
|
||||
... // the usual ctor work goes here
|
||||
}
|
||||
... // remainder of Person class (as before)
|
||||
explicit Person(T&& n) //对于std::strings和可转化为
|
||||
: name(std::forward<T>(n)) //std::strings的实参的构造函数
|
||||
{ … }
|
||||
|
||||
explicit Person(int idx) //对于整型实参的构造函数
|
||||
: name(nameFromIdx(idx))
|
||||
{ … }
|
||||
|
||||
… //拷贝、移动构造函数等
|
||||
|
||||
private:
|
||||
std::string name;
|
||||
};
|
||||
```
|
||||
|
||||
如果client代码尝试使用无法构造`std::string`的类型创建`Person`,会导致指定的错误消息。不幸的是,在这个例子中,`static_assert`在构造函数体中,但是作为成员初始化列表的部分在检查之前。所以我使用的编译器,结果是由`static_assert`产生的清晰的错误消息在常规错误消息(最多160行以上那个)后出现。
|
||||
看!多么优美!好吧,优美之处只是对于那些迷信模板元编程之人,但是确实提出了不仅能工作的方法,而且极具技巧。因为使用了完美转发,所以具有最大效率,因为控制了通用引用与重载的结合而不是禁止它,这种技术可以被用于不可避免要用重载的情况(比如构造函数)。
|
||||
|
||||
### 需要记住的事
|
||||
### 折中
|
||||
|
||||
- 通用引用和重载的组合替代方案包括使用不同的函数名,通过const左值引用传参,按值传递参数,使用tag dispatch
|
||||
- 通过`std::enable_if`约束模板,允许组合通用引用和重载使用,`std::enable_if`可以控制编译器哪种条件才使用通用引用的实例
|
||||
- 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌
|
||||
本条款提到的前三个技术——放弃重载、传递const T&、传值——在函数调用中指定每个形参的类型。后两个技术——*tag dispatch*和限制模板适用范围——使用完美转发,因此不需要指定形参类型。这一基本决定(是否指定类型)有一定后果。
|
||||
|
||||
通常,完美转发更有效率,因为它避免了仅仅去为了符合形参声明的类型而创建临时对象。在`Person`构造函数的例子中,完美转发允许将“`Nancy`”这种字符串字面量转发到`Person`内部的`std::string`的构造函数,不使用完美转发的技术则会从字符串字面值创建一个临时`std::string`对象,来满足`Person`构造函数指定的形参要求。
|
||||
|
||||
但是完美转发也有缺点。即使某些类型的实参可以传递给接受特定类型的函数,也无法完美转发。[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)中探索了完美转发失败的例子。
|
||||
|
||||
第二个问题是当客户传递无效参数时错误消息的可理解性。例如假如客户传递了一个由`char16_t`(一种C++11引入的类型表示16位字符)而不是`char`(`std::string`包含的)组成的字符串字面值来创建一个`Person`对象:
|
||||
|
||||
```cpp
|
||||
Person p(u"Konrad Zuse"); //“Konrad Zuse”由const char16_t类型字符组成
|
||||
```
|
||||
|
||||
使用本条款中讨论的前三种方法,编译器将看到可用的采用`int`或者`std::string`的构造函数,它们或多或少会产生错误消息,表示没有可以从`const char16_t[12]`转换为`int`或者`std::string`的方法。
|
||||
|
||||
但是,基于完美转发的方法,`const char16_t`不受约束地绑定到构造函数的形参。从那里将转发到`Person`的`std::string`数据成员的构造函数,在这里,调用者传入的内容(`const char16_t`数组)与所需内容(`std::string`构造函数可接受的类型)发生的不匹配会被发现。由此产生的错误消息会让人更印象深刻,在我使用的编译器上,会产生超过160行错误信息。
|
||||
|
||||
在这个例子中,通用引用仅被转发一次(从`Person`构造函数到`std::string`构造函数),但是更复杂的系统中,在最终到达判断实参类型是否可接受的地方之前,通用引用会被多层函数调用转发。通用引用被转发的次数越多,产生的错误消息偏差就越大。许多开发者发现,这种特殊问题是发生在留有通用引用形参的接口上,这些接口以性能作为首要考虑点。
|
||||
|
||||
在`Person`这个例子中,我们知道完美转发函数的通用引用形参要作为`std::string`的初始化器,所以我们可以用`static_assert`来确认它可以起这个作用。`std::is_constructible`这个*type trait*执行编译时测试,确定一个类型的对象是否可以用另一个不同类型(或多个类型)的对象(或多个对象)来构造,所以代码可以这样:
|
||||
|
||||
```cpp
|
||||
class Person {
|
||||
public:
|
||||
template< //同之前一样
|
||||
typename T,
|
||||
typename = std::enable_if_t<
|
||||
!std::is_base_of<Person, std::decay_t<T>>::value
|
||||
&&
|
||||
!std::is_integral<std::remove_reference_t<T>>::value
|
||||
>
|
||||
>
|
||||
explicit Person(T&& n)
|
||||
: name(std::forward<T>(n))
|
||||
{
|
||||
//断言可以用T对象创建std::string
|
||||
static_assert(
|
||||
std::is_constructible<std::string, T>::value,
|
||||
"Parameter n can't be used to construct a std::string"
|
||||
);
|
||||
|
||||
… //通常的构造函数的工作写在这
|
||||
|
||||
}
|
||||
|
||||
… //Person类的其他东西(同之前一样)
|
||||
};
|
||||
```
|
||||
|
||||
如果客户代码尝试使用无法构造`std::string`的类型创建`Person`,会导致指定的错误消息。不幸的是,在这个例子中,`static_assert`在构造函数体中,但是转发的代码作为成员初始化列表的部分在检查之前。所以我使用的编译器,结果是由`static_assert`产生的清晰的错误消息在常规错误消息(多达160行以上那个)后出现。
|
||||
|
||||
**请记住:**
|
||||
|
||||
- 通用引用和重载的组合替代方案包括使用不同的函数名,通过lvalue-reference-to-`const`传递形参,按值传递形参,使用*tag dispatch*。
|
||||
- 通过`std::enable_if`约束模板,允许组合通用引用和重载使用,但它也控制了编译器在哪种条件下才使用通用引用重载。
|
||||
- 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌。
|
||||
|
@ -1,188 +1,203 @@
|
||||
## Item28:理解引用折叠
|
||||
## 条款二十八:理解引用折叠
|
||||
|
||||
Item23中指出,当参数传递给模板函数时,模板参数的类型是左值还是右值被推导出来。但是并没有提到只有当参数被声明为通用引用时,上述推导才会发生,但是有充分的理由忽略这一点:因为通用引用是Item24中才提到。回过头来看,通用引用和左值/右值编码意味着:
|
||||
**Item 28: Understand reference collapsing**
|
||||
|
||||
[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)中指出,当实参传递给模板函数时,被推导的模板形参`T`根据实参是左值还是右值来编码。但是那条款并没有提到只有当实参被用来实例化通用引用形参时,上述推导才会发生,但是有充分的理由忽略这一点:因为通用引用是[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)中才提到。回过头来看,对通用引用和左值/右值编码的观察意味着对于这个模板,
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void func(T&& param);
|
||||
```
|
||||
|
||||
被推导的模板参数T将根据被传入参数类型被编码为左值或者右值。
|
||||
被推导的模板形参`T`将根据被传给`param`的实参被编码为左值或者右值。
|
||||
|
||||
编码机制是简单的。当左值被传入时,T被推导为左值。当右值被传入时,T被推导为非引用(请注意不对称性:左值被编码为左值引用,右值被编码为非引用),因此:
|
||||
编码机制是简单的。当左值实参被传入时,`T`被推导为左值引用。当右值被传入时,`T`被推导为非引用。(请注意不对称性:左值被编码为左值引用,右值被编码为**非引用**。)因此:
|
||||
|
||||
```cpp
|
||||
Widget widgetFactory(); // function returning rvalue
|
||||
Widget w; // a variable(an lvalue)
|
||||
func(w); // call func with lvalue; T deduced to be Widget&
|
||||
func(widgetFactory()); // call func with rvalue; T deduced to be Widget
|
||||
Widget widgetFactory(); //返回右值的函数
|
||||
Widget w; //一个变量(左值)
|
||||
func(w); //用左值调用func;T被推导为Widget&
|
||||
func(widgetFactory()); //用又值调用func;T被推导为Widget
|
||||
```
|
||||
|
||||
上面的两种调用中,Widget被传入,因为一个是左值,一个是右值,模板参数T被推导为不同的类型。正如我们很快看到的,这决定了通用引用成为左值还是右值,也是`std::forward`的工作基础。
|
||||
上面的两种`func`调用中,`Widget`被传入,因为一个是左值,一个是右值,模板形参`T`被推导为不同的类型。正如我们很快看到的,这决定了通用引用成为左值还是右值,也是`std::forward`的工作基础。
|
||||
|
||||
在我们更加深入`std::forward`和通用引用之前,必须明确在C++中引用的引用是非法的。不知道你是否尝试过下面的写法,编译器会报错:
|
||||
|
||||
```cpp
|
||||
int x;
|
||||
...
|
||||
auto& & rx = x; //error! can't declare reference to reference
|
||||
…
|
||||
auto& & rx = x; //错误!不能声明引用的引用
|
||||
```
|
||||
|
||||
考虑下,如果一个左值传给模板函数的通用引用会发生什么:
|
||||
考虑下,如果一个左值传给接受通用引用的模板函数会发生什么:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void func(T&& param);
|
||||
void func(T&& param); //同之前一样
|
||||
|
||||
func(w); // invoke func with lvalue; T deduced as Widget&
|
||||
func(w); //用左值调用func;T被推导为Widget&
|
||||
```
|
||||
|
||||
如果我们把推导出来的类型带入回代码中看起来就像是这样:
|
||||
如果我们用`T`推导出来的类型(即`Widget&`)初始化模板,会得到:
|
||||
|
||||
```cpp
|
||||
void func(Widget& && param);
|
||||
```
|
||||
|
||||
引用的引用!但是编译器没有报错。我们从Item24中了解到因为通用引用param被传入一个左值,所以param的类型被推导为左值引用,但是编译器如何采用T的推导类型的结果,这是最终的函数签名?
|
||||
引用的引用!但是编译器没有报错。我们从[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)中了解到因为通用引用`param`被传入一个左值,所以`param`的类型应该为左值引用,但是编译器如何把`T`推导的类型带入模板变成如下的结果,也就是最终的函数签名?
|
||||
|
||||
```cpp
|
||||
void func(Widget& param);
|
||||
```
|
||||
|
||||
答案是引用折叠。是的,禁止你声明引用的引用,但是编译器会在特定的上下文中使用,包括模板实例的例子。当编译器生成引用的引用时,引用折叠指导下一步发生什么。
|
||||
答案是**引用折叠**(*reference collapsing*)。是的,禁止**你**声明引用的引用,但是**编译器**会在特定的上下文中产生这些,模板实例化就是其中一种情况。当编译器生成引用的引用时,引用折叠指导下一步发生什么。
|
||||
|
||||
存在两种类型的引用(左值和右值),所以有四种可能的引用组合(左值的左值,左值的右值,右值的右值,右值的左值)。如果一个上下文中允许引用的引用存在(比如,模板函数的实例化),引用根据规则折叠为单个引用:
|
||||
存在两种类型的引用(左值和右值),所以有四种可能的引用组合(左值的左值,左值的右值,右值的右值,右值的左值)。如果一个上下文中允许引用的引用存在(比如,模板的实例化),引用根据规则**折叠**为单个引用:
|
||||
|
||||
> 如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用
|
||||
> 如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用。
|
||||
|
||||
在我们上面的例子中,将推导类型Widget&替换模板func会产生对左值引用的右值引用,然后引用折叠规则告诉我们结果就是左值引用。
|
||||
在我们上面的例子中,将推导类型`Widget&`替换进模板`func`会产生对左值引用的右值引用,然后引用折叠规则告诉我们结果就是左值引用。
|
||||
|
||||
引用折叠是`std::forward`工作的一种关键机制。就像Item25中解释的一样,`std::forward`应用在通用引用参数上,所以经常能看到这样使用:
|
||||
引用折叠是`std::forward`工作的一种关键机制。就像[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)中解释的一样,`std::forward`应用在通用引用参数上,所以经常能看到这样使用:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void f(T&& fParam)
|
||||
{
|
||||
... // do some work
|
||||
someFunc(std::forward<T>(fParam)); // forward fParam to someFunc
|
||||
… //做些工作
|
||||
someFunc(std::forward<T>(fParam)); //转发fParam到someFunc
|
||||
}
|
||||
```
|
||||
|
||||
因为fParam是通用引用,我们知道参数T的类型将在传入具体参数时被编码。`std::forward`的作用是当传入参数为右值时,即T为非引用类型,才将fParam(左值)转化为一个右值。
|
||||
因为`fParam`是通用引用,我们知道类型参数`T`的类型根据`f`被传入实参(即用来实例化`fParam`的表达式)是左值还是右值来编码。`std::forward`的作用是当且仅当传给`f`的实参为右值时,即`T`为非引用类型,才将`fParam`(左值)转化为一个右值。
|
||||
|
||||
`std::forward`可以这样实现:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
T&& forward(typename remove_reference<T>::type& param)
|
||||
template<typename T> //在std命名空间
|
||||
T&& forward(typename
|
||||
remove_reference<T>::type& param)
|
||||
{
|
||||
return static_cast<T&&>(param);
|
||||
return static_cast<T&&>(param);
|
||||
}
|
||||
```
|
||||
|
||||
这不是标准库版本的实现(忽略了一些接口描述),但是为了理解`std::forward`的行为,这些差异无关紧要。
|
||||
|
||||
假设传入到f的Widget的左值类型。T被推导为Widget&,然后调用`std::forward`将初始化为`std::forward<Widget&>`。带入到上面的`std::forward`的实现中:
|
||||
假设传入到`f`的实参是`Widget`的左值类型。`T`被推导为`Widget&`,然后调用`std::forward`将实例化为`std::forward<Widget&>`。`Widget&`带入到上面的`std::forward`的实现中:
|
||||
|
||||
```cpp
|
||||
Widget& && forward(typename remove_reference<Widget&>::type& param)
|
||||
{
|
||||
return static_cast<Widget& &&>(param);
|
||||
}
|
||||
Widget& && forward(typename
|
||||
remove_reference<Widget&>::type& param)
|
||||
{ return static_cast<Widget& &&>(param); }
|
||||
```
|
||||
|
||||
`std::remove_reference<Widget&>::type`表示Widget(查看Item9),所以`std::forward`成为:
|
||||
`std::remove_reference<Widget&>::type`这个*type trait*产生`Widget`(查看[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md)),所以`std::forward`成为:
|
||||
|
||||
```cpp
|
||||
Widget& && forward(Widget& param)
|
||||
{
|
||||
return static_cast<Widget& &&>(param);
|
||||
}
|
||||
{ return static_cast<Widget& &&>(param); }
|
||||
```
|
||||
|
||||
根据引用折叠规则,返回值和static_cast可以化简,最终版本的`std::forward`就是
|
||||
根据引用折叠规则,返回值和强制转换可以化简,最终版本的`std::forward`调用就是:
|
||||
|
||||
```cpp
|
||||
Widget& forward(Widget& param)
|
||||
{
|
||||
return static_cast<Widget&>(param);
|
||||
}
|
||||
{ return static_cast<Widget&>(param); }
|
||||
```
|
||||
|
||||
正如你所看到的,当左值被传入到函数模板f时,`std::forward`转发和返回的都是左值引用。内部的转换不做任何事,因为param的类型已经是`Widget&`,所以转换没有影响。左值传入会返回左值引用。通过定义,左值引用就是左值,因此将左值传递给`std::forward`会返回左值,就像说的那样,完美转发。
|
||||
正如你所看到的,当左值实参被传入到函数模板`f`时,`std::forward`被实例化为接受和返回左值引用。内部的转换不做任何事,因为`param`的类型已经是`Widget&`,所以转换没有影响。左值实参传入`std::forward`会返回左值引用。通过定义,左值引用就是左值,因此将左值传递给`std::forward`会返回左值,就像期待的那样。
|
||||
|
||||
现在假设一下,传递给f的是一个`Widget`的右值。在这个例子中,T的类型推导就是Widget。内部的`std::forward`因此转发`std::forward<Widget>`,带入回`std::forward`实现中:
|
||||
现在假设一下,传递给`f`的实参是一个`Widget`的右值。在这个例子中,`f`的类型参数`T`的推导类型就是`Widget`。`f`内部的`std::forward`调用因此为`std::forward<Widget>`,`std::forward`实现中把`T`换为`Widget`得到:
|
||||
|
||||
```cpp
|
||||
Widget&& forward(typename remove_reference<Widget>::type& param)
|
||||
{
|
||||
return static_cast<Widget&&>(param);
|
||||
}
|
||||
Widget&& forward(typename
|
||||
remove_reference<Widget>::type& param)
|
||||
{ return static_cast<Widget&&>(param); }
|
||||
```
|
||||
|
||||
将`remove_reference`引用到非引用的类型上还是相同的类型,所以化简如下
|
||||
将`std::remove_reference`引用到非引用类型`Widget`上还是相同的类型(`Widget`),所以`std::forward`变成:
|
||||
|
||||
```cpp
|
||||
Widget&& forward(Widget& param)
|
||||
{
|
||||
return static_cast<Widget&&>(param);
|
||||
}
|
||||
{ return static_cast<Widget&&>(param); }
|
||||
```
|
||||
|
||||
这里没有引用的引用,所以不需要引用折叠,这就是最终版本。
|
||||
这里没有引用的引用,所以不需要引用折叠,这就是`std::forward`的最终实例化版本。
|
||||
|
||||
从函数返回的右值引用被定义为右值,因此在这种情况下,`std::forward`会将f的参数fParam(左值)转换为右值。最终结果是,传递给f的右值参数将作为右值转发给someFunc,完美转发。
|
||||
从函数返回的右值引用被定义为右值,因此在这种情况下,`std::forward`会将`f`的形参`fParam`(左值)转换为右值。最终结果是,传递给`f`的右值参数将作为右值转发给`someFunc`,正是想要的结果。
|
||||
|
||||
在C++14中,`std::remove_reference_t`的存在使得实现变得更简单:
|
||||
在C++14中,`std::remove_reference_t`的存在使得实现变得更简洁:
|
||||
|
||||
```cpp
|
||||
template<typename T> // C++ 14; still in namepsace std
|
||||
template<typename T> //C++14;仍然在std命名空间
|
||||
T&& forward(remove_reference_t<T>& param)
|
||||
{
|
||||
return static_cast<T&&>(param);
|
||||
}
|
||||
```
|
||||
|
||||
引用折叠发生在四种情况下。**第一**,也是最常见的就是模板实例化。**第二**,是auto变量的类型生成,具体细节类似模板实例化的分析,因为类型推导基本与模板实例化雷同(参见Item2)。考虑下面的例子:
|
||||
引用折叠发生在四种情况下。第一,也是最常见的就是模板实例化。第二,是`auto`变量的类型生成,具体细节类似于模板,因为`auto`变量的类型推导基本与模板类型推导雷同(参见[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md))。考虑本条款前面的例子:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void func(T&& param);
|
||||
Widget widgetFactory(); // function returning rvalue
|
||||
Widget w; // a variable(an lvalue)
|
||||
func(w); // call func with lvalue; T deduced to be Widget&
|
||||
func(widgetFactory()); // call func with rvalue; T deduced to be Widget
|
||||
Widget widgetFactory(); //返回右值的函数
|
||||
Widget w; //一个变量(左值)
|
||||
func(w); //用左值调用func;T被推导为Widget&
|
||||
func(widgetFactory()); //用又值调用func;T被推导为Widget
|
||||
```
|
||||
|
||||
在auto的写法中,规则是类似的:`auto&& w1 = w;`初始化`w1`为一个左值,因此为auto推导出类型`Widget&`。带回去就是`Widget& && w1 = w`,应用引用折叠规则,就是`Widget& w1 = w`,结果就是`w1`是一个左值引用。
|
||||
在auto的写法中,规则是类似的。声明
|
||||
|
||||
另一方面,`auto&& w2 = widgetFactory();`使用右值初始化`w2`,非引用带回`Widget&& w2 = widgetFactory()`。没有引用的引用,这就是最终结果。
|
||||
```cpp
|
||||
auto&& w1 = w;
|
||||
```
|
||||
|
||||
现在我们真正理解了Item24中引入的通用引用。通用引用不是一种新的引用,它实际上是满足两个条件下的右值引用:
|
||||
用一个左值初始化`w1`,因此为`auto`推导出类型`Widget&`。把`Widget&`代回`w1`声明中的`auto`里,产生了引用的引用,
|
||||
```cpp
|
||||
Widget& && w1 = w;
|
||||
```
|
||||
应用引用折叠规则,就是
|
||||
```cpp
|
||||
Widget& w1 = w
|
||||
```
|
||||
结果就是`w1`是一个左值引用。
|
||||
|
||||
- **通过类型推导将左值和右值区分**。T类型的左值被推导为&类型,T类型的右值被推导为T
|
||||
- **引用折叠的发生**
|
||||
另一方面,这个声明,
|
||||
```cpp
|
||||
auto&& w2 = widgetFactory();
|
||||
```
|
||||
使用右值初始化`w2`,为`auto`推导出非引用类型`Widget`。把`Widget`代入`auto`得到:
|
||||
```cpp
|
||||
Widget&& w2 = widgetFactory()
|
||||
```
|
||||
没有引用的引用,这就是最终结果,`w2`是个右值引用。
|
||||
|
||||
通用引用的概念是有用的,因为它使你不必一定意识到引用折叠的存在,从直觉上判断左值和右值的推导即可。
|
||||
现在我们真正理解了[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)中引入的通用引用。通用引用不是一种新的引用,它实际上是满足以下两个条件下的右值引用:
|
||||
|
||||
我说了有四种情况会发生引用折叠,但是只讨论了两种:模板实例化和auto的类型生成。**第三**,是使用typedef和别名声明(参见Item9),如果,在创建或者定义typedef过程中出现了引用的引用,则引用折叠就会起作用。举例子来说,假设我们有一个Widget的类模板,该模板具有右值引用类型的嵌入式typedef:
|
||||
- **类型推导区分左值和右值**。`T`类型的左值被推导为`T&`类型,`T`类型的右值被推导为`T`。
|
||||
- **发生引用折叠**。
|
||||
|
||||
通用引用的概念是有用的,因为它使你不必一定意识到引用折叠的存在,从直觉上推导左值和右值的不同类型,在凭直觉把推导的类型代入到它们出现的上下文中之后应用引用折叠规则。
|
||||
|
||||
我说了有四种情况会发生引用折叠,但是只讨论了两种:模板实例化和`auto`的类型生成。第三种情况是`typedef`和别名声明的产生和使用中(参见[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md))。如果,在创建或者评估`typedef`过程中出现了引用的引用,则引用折叠就会起作用。举例子来说,假设我们有一个`Widget`的类模板,该模板具有右值引用类型的嵌入式`typedef`:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
class Widget {
|
||||
public:
|
||||
typedef T&& RvalueRefToT;
|
||||
...
|
||||
typedef T&& RvalueRefToT;
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
假设我们使用左值引用实例化Widget:
|
||||
假设我们使用左值引用实例化`Widget`:
|
||||
|
||||
```cpp
|
||||
Widget<int&> w;
|
||||
```
|
||||
|
||||
就会出现
|
||||
`Widget`模板中把`T`替换为`int&`得到:
|
||||
|
||||
```cpp
|
||||
typedef int& && RvalueRefToT;
|
||||
@ -194,12 +209,12 @@ typedef int& && RvalueRefToT;
|
||||
typedef int& RvalueRefToT;
|
||||
```
|
||||
|
||||
这清楚表明我们为typedef选择的name可能不是我们希望的那样:RvalueRefToT是左值引用的typedef,当使用Widget被左值引用实例化时。
|
||||
这清楚表明我们为`typedef`选择的名字可能不是我们希望的那样:当使用左值引用类型实例化`Widget`时,`RvalueRefToT`是**左值引用**的`typedef`。
|
||||
|
||||
最后,**也是第四**种情况是,decltype使用的情况,如果在分析decltype期间,出现了引用的引用,引用折叠规则就会起作用(关于decltype,参见Item3)
|
||||
最后一种引用折叠发生的情况是,`decltype`使用的情况。如果在分析`decltype`期间,出现了引用的引用,引用折叠规则就会起作用(关于`decltype`,参见[Item3](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md))
|
||||
|
||||
### 需要记住的事
|
||||
**请记住:**
|
||||
|
||||
- 引用折叠发生在四种情况:模板实例化;auto类型推导;typedef的创建和别名声明;decltype
|
||||
- 当编译器生成了引用的引用时,结果通过引用折叠就是单个引用。有左值引用就是左值引用,否则就是右值引用
|
||||
- 通用引用就是通过类型推导区分左值还是右值,并且引用折叠出现的右值引用
|
||||
- 引用折叠发生在四种情况下:模板实例化,`auto`类型推导,`typedef`与别名声明的创建和使用,`decltype`。
|
||||
- 当编译器在引用折叠环境中生成了引用的引用时,结果就是单个引用。有左值引用折叠结果就是左值引用,否则就是右值引用。
|
||||
- 通用引用就是在特定上下文的右值引用,上下文是通过类型推导区分左值还是右值,并且发生引用折叠的那些地方。
|
||||
|
@ -1,54 +1,66 @@
|
||||
# Item29: Assume that move operations are not present, not cheap, and not used
|
||||
## 条款二十九:假定移动操作不存在,成本高,未被使用
|
||||
|
||||
移动语义可以说是C++11最主要的特性。你可能会见过这些类似的描述“移动容器和拷贝指针一样开销小”, “拷贝临时对象现在如此高效,编码避免这种情况简直就是过早优化”这种情绪很容易理解。移动语义确实是这样重要的特性。它不仅允许编译器使用开销小的移动操作代替大开销的复制操作,而且默认这么做。以C++98的代码为基础,使用C++11重新编译你的代码,然后,哇,你的软件运行的更快了。
|
||||
**Item 29: Assume that move operations are not present, not cheap, and not used**
|
||||
|
||||
移动语义确实令人振奋,但是有很多夸大的说法,这个Item的目的就是给你泼一瓢冷水,保持理智看待移动语义。
|
||||
移动语义可以说是C++11最主要的特性。你可能会见过这些类似的描述“移动容器和拷贝指针一样开销小”, “拷贝临时对象现在如此高效,写代码避免这种情况简直就是过早优化”。这种情绪很容易理解。移动语义确实是这样重要的特性。它不仅允许编译器使用开销小的移动操作代替大开销的复制操作,而且默认这么做(当特定条件满足的时候)。以C++98的代码为基础,使用C++11重新编译你的代码,然后,哇,你的软件运行的更快了。
|
||||
|
||||
让我们从已知很多类型不支持移动操作开始这个过程。为了升级到C++11,C++98的很多标准库做了大修改,为很多类型提供了移动的能力,这些类型的移动实现比复制操作更快,并且对库的组件实现修改以利用移动操作。但是很有可能你工作中的代码没有完整地利用C++11。对于你的应用中(或者代码库中),没有适配C++11的部分,编译器即使支持移动语义也是无能为力的。的确,C++11倾向于为缺少移动操作定义的类默认生成,但是只有在没有声明复制操作,移动操作,或析构函数的类中才会生成移动操作(参考Item17)。禁止移动操作的类中(通过delete move operation 参考Item11),编译器不生成移动操作的支持。对于没有明确支持移动操作的类型,并且不符合编译器默认生成的条件的类,没有理由期望C++11会比C++98进行任何性能上的提升。
|
||||
移动语义确实可以做这些事,这把这个特性封为一代传说。但是传说总有些夸大成分。这个条款的目的就是给你泼一瓢冷水,保持理智看待移动语义。
|
||||
|
||||
即使显式支持了移动操作,结果可能也没有你希望的那么好。比如,所有C++11的标准库都支持了移动操作,但是认为移动所有容器的开销都非常小是个错误。对于某些容器来说,压根就不存在开销小的方式来移动它所包含的内容。对另一些容器来说,开销真正小的移动操作却使得容器元素移动含义事与愿违。
|
||||
让我们从已知很多类型不支持移动操作开始这个过程。为了升级到C++11,C++98的很多标准库做了大修改,为很多类型提供了移动的能力,这些类型的移动实现比复制操作更快,并且对库的组件实现修改以利用移动操作。但是很有可能你工作中的代码没有完整地利用C++11。对于你的应用中(或者代码库中)的类型,没有适配C++11的部分,编译器即使支持移动语义也是无能为力的。的确,C++11倾向于为缺少移动操作的类生成它们,但是只有在没有声明复制操作,移动操作,或析构函数的类中才会生成移动操作(参考[Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md))。数据成员或者某类型的基类禁止移动操作(比如通过delete移动操作,参考[Item11](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item11.md)),编译器不生成移动操作的支持。对于没有明确支持移动操作的类型,并且不符合编译器默认生成的条件的类,没有理由期望C++11会比C++98进行任何性能上的提升。
|
||||
|
||||
考虑一下`std::array`,这是C++11中的新容器。`std::array`本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器,本身只保存了指向堆内存数据的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这种实现使得在常数时间移动整个容器成为可能的,只需要拷贝容器中保存的指针到目标容器,然后将原容器的指针置为空指针就可以了。
|
||||
即使显式支持了移动操作,结果可能也没有你希望的那么好。比如,所有C++11的标准库容器都支持了移动操作,但是认为移动所有容器的开销都非常小是个错误。对于某些容器来说,压根就不存在开销小的方式来移动它所包含的内容。对另一些容器来说,容器的开销真正小的移动操作会有些容器元素不能满足的注意条件。
|
||||
|
||||
考虑一下`std::array`,这是C++11中的新容器。`std::array`本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器,本身只保存了指向堆内存中容器内容的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这个指针的存在使得在常数时间移动整个容器成为可能,只需要从源容器拷贝保存指向容器内容的指针到目标容器,然后将源指针置为空指针就可以了:
|
||||
|
||||
```cpp
|
||||
std::vector<Widget> vm1;
|
||||
|
||||
auto vm2 = std::move(vm1); // move vm1 into vm2. Runs in constant time. Only ptrs in vm1 and vm2 are modified
|
||||
//把数据存进vw1
|
||||
…
|
||||
|
||||
//把vw1移动到vw2。以常数时间运行。只有vw1和vw2中的指针被改变
|
||||
auto vm2 = std::move(vm1);
|
||||
```
|
||||
|
||||
`std::array`没有这种指针实现,数据就保存在`std::array`容器中
|
||||

|
||||
|
||||
`std::array`没有这种指针实现,数据就保存在`std::array`对象中:
|
||||
|
||||
```cpp
|
||||
std::array<Widget, 10000> aw1;
|
||||
|
||||
auto aw2 = std::move(aw1); // move aw1 into aw2. Runs in linear time. All elements in aw1 are moved into aw2.
|
||||
//把数据存进aw1
|
||||
…
|
||||
|
||||
//把aw1移动到aw2。以线性时间运行。aw1中所有元素被移动到aw2
|
||||
auto aw2 = std::move(aw1);
|
||||
```
|
||||
|
||||
注意`aw1`中的元素被移动到了`aw2`中,这里假定`Widget`类的移动操作比复制操作快。但是使用`std::array`的移动操作还是复制操作都将花费线性时间的开销,因为每个容器中的元素终归需要拷贝一次,这与“移动一个容器就像操作几个指针一样方便”的含义想去甚远。
|
||||

|
||||
|
||||
另一方面,`std::strnig`提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了*small string optimization (SSO)*。"small"字符串(比如长度小于15个字符的)存储在了`std::string`的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
|
||||
注意`aw1`中的元素被**移动**到了`aw2`中。假定`Widget`类的移动操作比复制操作快,移动`Widget`的`std::array`就比复制要快。所以`std::array`确实支持移动操作。但是使用`std::array`的移动操作还是复制操作都将花费线性时间的开销,因为每个容器中的元素终归需要拷贝或移动一次,这与“移动一个容器就像操作几个指针一样方便”的含义相去甚远。
|
||||
|
||||
SSO的动机是大量证据表明,短字符串是大量应用使用的习惯。使用内存缓冲区存储而不分配堆内存空间,是为了更好的效率。然而这种内存管理的效率导致移动的效率并不必复制操作高。
|
||||
另一方面,`std::strnig`提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了小字符串优化(*small string optimization*,SSO)。“小”字符串(比如长度小于15个字符的)存储在了`std::string`的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
|
||||
|
||||
即使对于支持快速移动操作的类型,某些看似可靠的移动操作最终也会导致复制。Item14解释了原因,标准库中的某些容器操作提供了强大的异常安全保证,确保C++98的代码直接升级C++11编译器不会不可运行,仅仅确保移动操作不会抛出异常,才会替换为移动操作。结果就是,即使类提供了更具效率的移动操作,编译器仍可能被迫使用复制操作来避免移动操作导致的异常。
|
||||
SSO的动机是大量证据表明,短字符串是大量应用使用的习惯。使用内存缓冲区存储而不分配堆内存空间,是为了更好的效率。然而这种内存管理的效率导致移动的效率并不必复制操作高,即使一个半吊子程序员也能看出来对于这样的字符串,拷贝并不比移动慢。
|
||||
|
||||
即使对于支持快速移动操作的类型,某些看似可靠的移动操作最终也会导致复制。[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md)解释了原因,标准库中的某些容器操作提供了强大的异常安全保证,确保依赖那些保证的C++98的代码在升级到C++11且仅当移动操作不会抛出异常,从而可能替换操作时,不会不可运行。结果就是,即使类提供了更具效率的移动操作,而且即使移动操作更合适(比如源对象是右值),编译器仍可能被迫使用复制操作,因为移动操作没有声明`noexcept`。
|
||||
|
||||
因此,存在几种情况,C++11的移动语义并无优势:
|
||||
|
||||
- **No move operations**:类没有提供移动操作,所以移动的写法也会变成复制操作
|
||||
- **Move not faster**:类提供的移动操作并不必复制效率更高
|
||||
- **Move not usable**:进行移动的上下文要求移动操作不会抛出异常,但是该操作没有被声明为`noexcept`
|
||||
- **没有移动操作**:要移动的对象没有提供移动操作,所以移动的写法也会变成复制操作。
|
||||
- **移动不会更快**:要移动的对象提供的移动操作并不比复制速度更快。
|
||||
- **移动不可用**:进行移动的上下文要求移动操作不会抛出异常,但是该操作没有被声明为`noexcept`。
|
||||
|
||||
值得一提的是,还有另一个场景,会使得移动并没有那么有效率:
|
||||
|
||||
- **Source object is lvalue**:除了极少数的情况外(例如 Item25),只有右值可以作为移动操作的来源
|
||||
- **源对象是左值**:除了极少数的情况外(例如[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)),只有右值可以作为移动操作的来源。
|
||||
|
||||
但是该Item的标题是假定不存在移动操作,或者开销不小,不使用移动操作。存在典型的场景,就是编写模板代码,因为你不清楚你处理的具体类型是什么。在这种情况下,你必须像出现移动语义之前那样,保守地考虑复制操作。不稳定的代码也是如此,类的特性经常被修改导致可能移动操作会有问题。
|
||||
但是该条款的标题是假定移动操作不存在,成本高,未被使用。这就是通用代码中的典型情况,比如编写模板代码,因为你不清楚你处理的具体类型是什么。在这种情况下,你必须像出现移动语义之前那样,像在C++98里一样保守地去复制对象。“不稳定的”代码也是如此,即那些由于经常被修改导致类型特性变化的源代码。
|
||||
|
||||
但是,通常,你了解你代码里使用的类,并且知道是否支持快速移动操作。这种情况,你无需这个Item的假设,只需要查找所用类的移动操作详细信息,并且调用移动操作的上下文中,可以安全的使用快速移动操作替换复制操作。
|
||||
但是,通常,你了解你代码里使用的类型,依赖他们的特性不变性(比如是否支持快速移动操作)。这种情况,你无需这个条款的假设,只需要查找所用类型的移动操作详细信息。如果类型提供了快速移动操作,并且在调用移动操作的上下文中使用对象,可以安全的使用快速移动操作替换复制操作。
|
||||
|
||||
**请记住:**
|
||||
|
||||
|
||||
## 需要记住的事
|
||||
|
||||
- Assume that move operations are not present, not cheap, and not used.
|
||||
- 完全了解的代码可以忽略本Item
|
||||
- 假定移动操作不存在,成本高,未被使用。
|
||||
- 在已知的类型或者支持移动语义的代码中,就不需要上面的假设。
|
||||
|
@ -1,236 +1,246 @@
|
||||
## Item30:熟悉完美转发的失败 case
|
||||
## 条款三十:熟悉完美转发失败的情况
|
||||
|
||||
C++11最显眼的功能之一就是完美转发功能。完美转发,太棒了!哎,开始使用,你就发现“完美”,理想与现实还是有差距。C++11 的完美转发是非常好用,但是只有当你愿意忽略一些失败情况,这个 Item 就是使你熟悉这些情形。
|
||||
**Item 30: Familiarize yourself with perfect forwarding failure cases**
|
||||
|
||||
在我们开始 epsilon 探索之前,有必要回顾一下“完美转发”的含义。“转发”仅表示将一个函数的参数传递给另一个函数。对于被传递的第二个函数目标是收到与第一个函数完全相同的对象。这就排除了按值传递参数,因为它们是原始调用者传入内容的副本。我们希望被转发的函数能够可以与原始函数一起使用对象。指针参数也被排除在外,因为我们不想强迫调用者传入指针。关于通用转发,我们将处理引用参数。
|
||||
C++11最显眼的功能之一就是完美转发功能。**完美**转发,太**完美**了!哎,开始使用,你就发现“完美”,理想与现实还是有差距。C++11的完美转发是非常好用,但是只有当你愿意忽略一些误差情况(译者注:就是完美转发失败的情况),这个条款就是使你熟悉这些情形。
|
||||
|
||||
完美转发意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是 `const` 还是 `volatile`。结合到我们会处理引用参数,这意味着我们将使用通用引用(参见Item24),因为通用引用参数被传入参数时才确定是左值还是右值。
|
||||
在我们开始误差探索之前,有必要回顾一下“完美转发”的含义。“转发”仅表示将一个函数的形参传递——就是**转发**——给另一个函数。对于第二个函数(被传递的那个)目标是收到与第一个函数(执行传递的那个)完全相同的对象。这规则排除了按值传递的形参,因为它们是原始调用者传入内容的**拷贝**。我们希望被转发的函数能够使用最开始传进来的那些对象。指针形参也被排除在外,因为我们不想强迫调用者传入指针。关于通常目的的转发,我们将处理引用形参。
|
||||
|
||||
假定我们有一些函数f,然后想编写一个转发给它的函数(就使用一个函数模板)。我们需要的核心看起来像是这样:
|
||||
**完美转发**(*perfect forwarding*)意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是`const`还是`volatile`。结合到我们会处理引用形参,这意味着我们将使用通用引用(参见[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)),因为通用引用形参被传入实参时才确定是左值还是右值。
|
||||
|
||||
假定我们有一些函数`f`,然后想编写一个转发给它的函数(事实上是一个函数模板)。我们需要的核心看起来像是这样:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
void fwd(T&& param) // accept any argument
|
||||
void fwd(T&& param) //接受任何实参
|
||||
{
|
||||
f(std::forward<T>(param)); // forward it to f
|
||||
f(std::forward<T>(param)); //转发给f
|
||||
}
|
||||
```
|
||||
|
||||
从本质上说,转发功能是通用的。例如 `fwd` 模板,接受任何类型的参数,并转发得到的任何参数。这种通用性的逻辑扩展是转发函数不仅是模板,而且是可变模板,因此可以接受任何数量的参数。fwd的可变个时如下:
|
||||
从本质上说,转发函数是通用的。例如`fwd`模板,接受任何类型的实参,并转发得到的任何东西。这种通用性的逻辑扩展是,转发函数不仅是模板,而且是可变模板,因此可以接受任何数量的实参。`fwd`的可变形式如下:
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
void fwd(Ts&&... params) // accept any arguments
|
||||
void fwd(Ts&&... params) //接受任何实参
|
||||
{
|
||||
f(std::forward<Ts>(params)...); // forward them to f
|
||||
f(std::forward<Ts>(params)...); //转发给f
|
||||
}
|
||||
```
|
||||
|
||||
这种形式你会在标准化容器emplace中(参见 Item42)和智能指针的工厂函数`std::make_unique` 和 `std::make_shared`中(参见 Item21)看到。
|
||||
这种形式你会在标准化容器置入函数(emplace functions)中(参见[Item42](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item42.md))和智能指针的工厂函数`std::make_unique`和`std::make_shared`中(参见[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md))看到,当然还有其他一些地方。
|
||||
|
||||
给定我们的目标函数f和被转发的函数fwd,如果f使用特定参数做一件事,但是fwd使用相同的参数做另一件事,完美转发就会失败:
|
||||
给定我们的目标函数`f`和转发函数`fwd`,如果`f`使用某特定实参做一件事,但是`fwd`使用相同的实参做另一件事,完美转发就会失败:
|
||||
|
||||
```cpp
|
||||
f(expression); // if this does one thing
|
||||
fwd(expression); // but this does something else, fwd fails to perfectly forward expression to f
|
||||
f( expression ); //如果这个做某件事,
|
||||
fwd( expression ); //但是这个做另外的某件事,fwd完美转发expression给f会失败
|
||||
```
|
||||
|
||||
导致这种失败的原因有很多。知道它们是什么以及如何解决它们很重要,因此让我们来看看那种参数无法做到完美转发。
|
||||
导致这种失败的实参种类有很多。知道它们是什么以及如何解决它们很重要,因此让我们来看看无法做到完美转发的实参类型。
|
||||
|
||||
### Braced initializers(花括号初始化器)
|
||||
### 花括号初始化器
|
||||
|
||||
假定f这样声明:
|
||||
假定`f`这样声明:
|
||||
|
||||
```cpp
|
||||
void f(const std::vector<int>& v);
|
||||
```
|
||||
|
||||
在这个例子中,通过列表初始化器,
|
||||
在这个例子中,用花括号初始化调用`f`通过编译,
|
||||
|
||||
```cpp
|
||||
f({1,2,3}); // fine "{1,2,3}" implicitly converted to std::vector<int>
|
||||
f({ 1, 2, 3 }); //可以,“{1, 2, 3}”隐式转换为std::vector<int>
|
||||
```
|
||||
|
||||
但是传递相同的列表初始化器给fwd不能编译
|
||||
但是传递相同的列表初始化给fwd不能编译
|
||||
|
||||
```cpp
|
||||
fwd({1,2,3}); // error! doesn't compile
|
||||
fwd({ 1, 2, 3 }); //错误!不能编译
|
||||
```
|
||||
|
||||
这是因为这是完美转发失效的一种情况。
|
||||
|
||||
所有这种错误有相同的原因。在对f的直接调用(例如 `f({1,2,3})`),编译器看到传入的参数是声明中的类型。如果类型不匹配,就会执行隐式转换操作使得调用成功。在上面的例子中,从`{1,2,3}`生成了临时变量`std::vector<int>`对象,因此f的参数会绑定到`std::vector<int>`对象上。
|
||||
所有这种错误有相同的原因。在对`f`的直接调用(例如`f({ 1, 2, 3 })`),编译器看看调用地传入的实参,看看`f`声明的形参类型。它们把调用地的实参和声明的实参进行比较,看看是否匹配,并且必要时执行隐式转换操作使得调用成功。在上面的例子中,从`{ 1, 2, 3 }`生成了临时`std::vector<int>`对象,因此`f`的形参`v`会绑定到`std::vector<int>`对象上。
|
||||
|
||||
当通过调用函数模板 `fwd` 调用f时,编译器不再比较传入给 `fwd` 的参数和f的声明中参数的类型。代替的是,推导传入给fwd的参数类型,然后比较推导后的参数类型和f的声明类型。当下面情况任何一个发生时,完美转发就会失败:
|
||||
当通过调用函数模板`fwd`间接调用`f`时,编译器不再把调用地传入给`fwd`的实参和`f`的声明中形参类型进行比较。而是**推导**传入给`fwd`的实参类型,然后比较推导后的实参类型和`f`的形参声明类型。当下面情况任何一个发生时,完美转发就会失败:
|
||||
|
||||
- **编译器不能推导出一个或者多个 `fwd` 的参数类型**,编译器就会报错
|
||||
- **编译器将一个或者多个 `fwd` 的参数类型推导错误**。在这里,“错误”可能意味着fwd将无法使用推导出的类型进行编译,但是也可能意味着调用者f使用fwd的推导类型对比直接传入参数类型表现出不一致的行为。这种不同行为的原因可能是因为f的函数重载定义,并且由于是“不正确的”类型推导,在fwd内部调用f和直接调用f将重载不同的函数。
|
||||
- **编译器不能推导出`fwd`的一个或者多个形参类型。**这种情况下代码无法编译。
|
||||
- **编译器推导“错”了`fwd`的一个或者多个形参类型。**在这里,“错误”可能意味着`fwd`的实例将无法使用推导出的类型进行编译,但是也可能意味着使用`fwd`的推导类型调用`f`,与用传给`fwd`的实参直接调用`f`表现出不一致的行为。这种不同行为的原因可能是因为`f`是个重载函数的名字,并且由于是“不正确的”类型推导,在`fwd`内部调用的`f`重载和直接调用的`f`重载不一样。
|
||||
|
||||
在上面的`f({1,2,3})`例子中,问题在于,如标准所言,将括号初始化器传递给未声明为`std::initializer_list`的函数模板参数,该标准规定为“非推导上下文”。简单来讲,这意味着编译器在对fwd的调用中推导表达式`{1,2,3}`的类型,因为fwd的参数没有声明为`std::initializer_list`。对于fwd参数的推导类型被阻止,编译器只能拒绝该调用。
|
||||
在上面的`fwd({ 1, 2, 3 })`例子中,问题在于,将花括号初始化传递给未声明为`std::initializer_list`的函数模板形参,被判定为——就像标准说的——“非推导上下文”。简单来讲,这意味着编译器不准在对`fwd`的调用中推导表达式`{ 1, 2, 3 }`的类型,因为`fwd`的形参没有声明为`std::initializer_list`。对于`fwd`形参的推导类型被阻止,编译器只能拒绝该调用。
|
||||
|
||||
有趣的是,Item2 说明了使用**braced initializer**的auto的变量初始化的类型推导是成功的。这种变量被视为`std::initializer_list`对象,在转发函数应推导为`std::initializer_list`类型的情况,这提供了一种简单的解决方法----使用auto声明一个局部变量,然后将局部变量转发:
|
||||
有趣的是,[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md)说明了使用花括号初始化的`auto`的变量的类型推导是成功的。这种变量被视为`std::initializer_list`对象,在转发函数应推导出类型为`std::initializer_list`的情况,这提供了一种简单的解决方法——使用`auto`声明一个局部变量,然后将局部变量传进转发函数:
|
||||
|
||||
```cpp
|
||||
auto il = {1,2,3}; // il's type deduced to be std::initializer_list<int>
|
||||
fwd(il); // fine, perfect-forwards il to f
|
||||
auto il = { 1, 2, 3 }; //il的类型被推导为std::initializer_list<int>
|
||||
fwd(il); //可以,完美转发il给f
|
||||
```
|
||||
|
||||
### 0 或者 `NULL` 作为空指针
|
||||
### `0`或者`NULL`作为空指针
|
||||
|
||||
Item8说明当你试图传递 0 或者 `NULL` 作为空指针给模板时,类型推导会出错,推导为一个整数类型而不是指针类型。结果就是不管是0还是NULL都不能被完美转发为空指针。解决方法非常简单,使用 `nullptr` 就可以了,具体的细节可参考Item 8.
|
||||
[Item8](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item8.md)说明当你试图传递`0`或者`NULL`作为空指针给模板时,类型推导会出错,会把传来的实参推导为一个整型类型(典型情况为`int`)而不是指针类型。结果就是不管是`0`还是`NULL`都不能作为空指针被完美转发。解决方法非常简单,传一个`nullptr`而不是`0`或者`NULL`。具体的细节,参考[Item8](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item8.md)。
|
||||
|
||||
### 仅声明的整数静态 `const` 数据成员
|
||||
### 仅有声明的整型`static const`数据成员
|
||||
|
||||
通常,无需在类中定义整数静态const数据成员;声明就可以了。这是因为编译器会对此类成员进行常量传播 (const propagation), 而不需要为它们开辟内存. 例如考虑下面的代码:
|
||||
通常,无需在类中定义整型`static const`数据成员;声明就可以了。这是因为编译器会对此类成员实行**常量传播**(*const propagation*),因此消除了保留内存的需要。比如,考虑下面的代码:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
static const std::size_t MinVals = 28; // MinVal's declaration
|
||||
...
|
||||
static const std::size_t MinVals = 28; //MinVal的声明
|
||||
…
|
||||
};
|
||||
... // no defn. for MinVals
|
||||
… //没有MinVals定义
|
||||
|
||||
std::vector<int> widgetData;
|
||||
widgetData.reserve(Widget::MinVals); // use of MinVals
|
||||
widgetData.reserve(Widget::MinVals); //使用MinVals
|
||||
```
|
||||
|
||||
这里,我们使用`Widget::MinVals`(或者简单点MinVals)来确定`widgetData`的初始容量,即使`MinVals`缺少定义。编译器通过将值28放入所有位置来补充缺少的定义。没有为`MinVals`的值留存储空间是没有问题的。如果要使用`MinVals`的地址(例如,有人创建了`MinVals`的指针),则`MinVals`需要存储(因为指针总要有一个地址),尽管上面的代码仍然可以编译,但是链接时就会报错,直到为`MinVals`提供定义。
|
||||
这里,我们使用`Widget::MinVals`(或者简单点`MinVals`)来确定`widgetData`的初始容量,即使`MinVals`缺少定义。编译器通过将值28放入所有提到`MinVals`的位置来补充缺少的定义(就像它们被要求的那样)。没有为`MinVals`的值留存储空间是没有问题的。如果要使用`MinVals`的地址(例如,有人创建了指向`MinVals`的指针),则`MinVals`需要存储(这样指针才有可指向的东西),尽管上面的代码仍然可以编译,但是链接时就会报错,直到为`MinVals`提供定义。
|
||||
|
||||
按照这个思路,想象下f(转发参数给 `fwd` 的函数)这样声明:
|
||||
按照这个思路,想象下`f`(`fwd`要转发实参给它的那个函数)这样声明:
|
||||
|
||||
```cpp
|
||||
void f(std::size_t val);
|
||||
```
|
||||
|
||||
使用`MinVals`调用f是可以的,因为编译器直接将值28代替`MinVals`:
|
||||
使用`MinVals`调用`f`是可以的,因为编译器直接将值28代替`MinVals`:
|
||||
|
||||
```cpp
|
||||
f(Widget::MinVals); // fine, treated as "28"
|
||||
f(Widget::MinVals); //可以,视为“f(28)”
|
||||
```
|
||||
|
||||
同样的,如果尝试通过fwd来调用f
|
||||
不过如果我们尝试通过`fwd`调用`f`,事情不会进展那么顺利:
|
||||
|
||||
```cpp
|
||||
fwd(Widget::MinVals); // error! shouldn't link
|
||||
fwd(Widget::MinVals); //错误!不应该链接
|
||||
```
|
||||
|
||||
代码可以编译,但是不能链接。就像使用`MinVals`地址表现一样,确实,底层的问题是一样的。
|
||||
代码可以编译,但是不应该链接。如果这让你想到使用`MinVals`地址会发生的事,确实,底层的问题是一样的。
|
||||
|
||||
尽管代码中没有使用`MinVals`的地址,但是fwd的参数是通用引用,而引用,在编译器生成的代码中,通常被视作指针。在程序的二进制底层代码中指针和引用是一样的。在这个水平下,引用只是可以自动取消引用的指针。在这种情况下,通过引用传递`MinVals`实际上与通过指针传递`MinVals`是一样的,因此,必须有内存使得指针可以指向。通过引用传递整型static const数据成员,必须定义它们,这个要求可能会造成完美转发失败,即使等效不使用完美转发的代码成功。(译者注:这里意思应该是没有定义,完美转发就会失败)
|
||||
尽管代码中没有使用`MinVals`的地址,但是`fwd`的形参是通用引用,而引用,在编译器生成的代码中,通常被视作指针。在程序的二进制底层代码中(以及硬件中)指针和引用是一样的。在这个水平上,引用只是可以自动解引用的指针。在这种情况下,通过引用传递`MinVals`实际上与通过指针传递`MinVals`是一样的,因此,必须有内存使得指针可以指向。通过引用传递的整型`static const`数据成员,通常需要定义它们,这个要求可能会造成在不使用完美转发的代码成功的地方,使用等效的完美转发失败。(译者注:这里意思应该是没有定义,完美转发就会失败)
|
||||
|
||||
可能你也注意到了在上述讨论中我使用了一些模棱两可的词。代码“不应该”链接,引用“通常”被看做指针。传递整型static const数据成员“通常”要求定义。看起来就像有些事情我没有告诉你......
|
||||
可能你也注意到了在上述讨论中我使用了一些模棱两可的词。代码“不应该”链接。引用“通常”被看做指针。传递整型`static const`数据成员“通常”要求定义。看起来就像有些事情我没有告诉你......
|
||||
|
||||
确实,根据标准,通过引用传递`MinVals`要求有定义。但不是所有的实现都强制要求这一点。所以,取决于你的编译器和链接器,你可能发现你可以在未定义的情况使用完美转发,恭喜你,但是这不是那样做的理由。为了具有可移植性,只要给整型static const提供一个定义,比如这样:
|
||||
确实,根据标准,通过引用传递`MinVals`要求有定义。但不是所有的实现都强制要求这一点。所以,取决于你的编译器和链接器,你可能发现你可以在未定义的情况使用完美转发,恭喜你,但是这不是那样做的理由。为了具有可移植性,只要给整型`static const`提供一个定义,比如这样:
|
||||
|
||||
```cpp
|
||||
const std::size_t Widget::MinVals; // in Widget's .cpp file
|
||||
const std::size_t Widget::MinVals; //在Widget的.cpp文件
|
||||
```
|
||||
|
||||
注意定义中不要重复初始化(这个例子中就是赋值28)。不要忽略这个细节,否则,编译器就会报错,提醒你只初始化一次。
|
||||
注意定义中不要重复初始化(这个例子中就是赋值28)。但是不要忽略这个细节。如果你忘了,并且在两个地方都提供了初始化,编译器就会报错,提醒你只能初始化一次。
|
||||
|
||||
### 重载的函数名称和模板名称
|
||||
### 重载函数的名称和模板名称
|
||||
|
||||
假定我们的函数f(通过fwd完美转发参数给f)可以通过向其传递执行某些功能的函数来定义其行为。假设这个函数参数和返回值都是整数,f声明就像这样:
|
||||
假定我们的函数`f`(我们想通过`fwd`完美转发实参给的那个函数)可以通过向其传递执行某些功能的函数来自定义其行为。假设这个函数接受和返回值都是`int`,`f`声明就像这样:
|
||||
|
||||
```cpp
|
||||
void f(int (*pf)(int)); // pf = "process function"
|
||||
void f(int (*pf)(int)); //pf = “process function”
|
||||
```
|
||||
|
||||
值得注意的是,也可以使用更简单的非指针语法声明。这种声明就像这样,含义与上面是一样的:
|
||||
|
||||
```cpp
|
||||
void f(int pf(int)); // declares same f as above
|
||||
void f(int pf(int)); //与上面定义相同的f
|
||||
```
|
||||
|
||||
无论哪种写法,我们都有了一个重载函数,processVal:
|
||||
无论哪种写法都可,现在假设我们有了一个重载函数,`processVal`:
|
||||
|
||||
```cpp
|
||||
int processVal(int value);
|
||||
int processVal(int value, int priority);
|
||||
```
|
||||
|
||||
我们可以传递processVal给f
|
||||
我们可以传递`processVal`给`f`,
|
||||
|
||||
```cpp
|
||||
f(processVal); // fine
|
||||
f(processVal); //可以
|
||||
```
|
||||
|
||||
但是有一点要注意,f要求一个函数指针,但是`processVal`不是一个函数指针或者一个函数,它是两个同名的函数。但是,编译器可以知道它需要哪个:通过参数类型和数量来匹配。因此选择了一个int参数的`processVal`地址传递给`f`。
|
||||
但是我们会发现一些吃惊的事情。`f`要求一个函数指针作为实参,但是`processVal`不是一个函数指针或者一个函数,它是同名的两个不同函数。但是,编译器可以知道它需要哪个:匹配上`f`的形参类型的那个。因此选择了仅带有一个`int`的`processVal`地址传递给`f`。
|
||||
|
||||
工作的基本机制是让编译器帮选择f的声明选择一个需要的`processVal`。但是,`fwd`是一个函数模板,没有需要的类型信息,使得编译器不可能帮助自动匹配一个合适的函数:
|
||||
工作的基本机制是`f`的声明让编译器识别出哪个是需要的`processVal`。但是,`fwd`是一个函数模板,没有它可接受的类型的信息,使得编译器不可能决定出哪个函数应被传递:
|
||||
|
||||
```cpp
|
||||
fwd(processVal); // error! which processVal?
|
||||
fwd(processVal); //错误!那个processVal?
|
||||
```
|
||||
|
||||
`processVal`没有类型信息,就不能类型推导,完美转发失败。
|
||||
单用`processVal`是没有类型信息的,所以就不能类型推导,完美转发失败。
|
||||
|
||||
同样的问题会发生在如果我们试图使用函数模板代替重载的函数名。一个函数模板是未实例化的函数,表示一个函数族:
|
||||
如果我们试图使用函数模板而不是(或者也加上)重载函数的名字,同样的问题也会发生。一个函数模板不代表单独一个函数,它表示一个函数族:
|
||||
|
||||
```cpp
|
||||
template<typename T>
|
||||
T workOnVal(T param) { ... } // template for processing values
|
||||
fwd(workOnVal); // error! which workOnVal instantiation ?
|
||||
T workOnVal(T param) //处理值的模板
|
||||
{ … }
|
||||
|
||||
fwd(workOnVal); //错误!哪个workOnVal实例?
|
||||
```
|
||||
|
||||
获得像`fwd`的完美转发接受一个重载函数名或者模板函数名的方式是指定转发的类型。比如,你可以创造与f相同参数类型的函数指针,通过`processVal`或者`workOnVal`实例化这个函数指针(可以引导生成代码时正确选择函数实例),然后传递指针给f:
|
||||
要让像`fwd`的完美转发函数接受一个重载函数名或者模板名,方法是指定要转发的那个重载或者实例。比如,你可以创造与`f`相同形参类型的函数指针,通过`processVal`或者`workOnVal`实例化这个函数指针(这可以引导选择正确版本的`processVal`或者产生正确的`workOnVal`实例),然后传递指针给`fwd`:
|
||||
|
||||
```cpp
|
||||
using ProcessFuncType = int (*)(int); // make typedef; see Item 9
|
||||
PorcessFuncType processValPtr = processVal; // specify needed signature for processVal
|
||||
fwd(processValPtr); // fine
|
||||
fwd(static_cast<ProcessFuncType>(workOnVal)); // alse fine
|
||||
using ProcessFuncType = //写个类型定义;见条款9
|
||||
int (*)(int);
|
||||
|
||||
ProcessFuncType processValPtr = processVal; //指定所需的processVal签名
|
||||
|
||||
fwd(processValPtr); //可以
|
||||
fwd(static_cast<ProcessFuncType>(workOnVal)); //也可以
|
||||
```
|
||||
|
||||
当然,这要求你知道fwd转发的函数指针的类型。对于完美转发来说这一点并不合理,毕竟,完美转发被设计为转发任何内容,如果没有文档告诉你转发的类型,你如何知道?(译者注:这里应该想表达,这是解决重载函数名或者函数模板的解决方案,但是这是完美转发本身的问题。)
|
||||
当然,这要求你知道`fwd`转发的函数指针的类型。没有理由去假定完美转发函数会记录着这些东西。毕竟,完美转发被设计为接受任何内容,所以如果没有文档告诉你要传递什么,你又从何而知这些东西呢?
|
||||
|
||||
### 位域
|
||||
|
||||
完美转发最后一种失败的情况是函数参数使用位域这种类型。为了更直观的解释,IPv4的头部可以如下定义:
|
||||
完美转发最后一种失败的情况是函数实参使用位域这种类型。为了更直观的解释,IPv4的头部有如下模型:(这假定的是位域是按从最低有效位(*least significant bit*,lsb)到最高有效位(*most significant bit*,msb)布局的。C++不保证这一点,但是编译器经常提供一种机制,允许程序员控制位域布局。)
|
||||
|
||||
```cpp
|
||||
struct IPv4Header {
|
||||
std::uint32_t version:4,
|
||||
IHL:4,
|
||||
DSCP:6,
|
||||
ECN:2,
|
||||
totalLength:16;
|
||||
...
|
||||
std::uint32_t version:4,
|
||||
IHL:4,
|
||||
DSCP:6,
|
||||
ECN:2,
|
||||
totalLength:16;
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
如果声明我们的函数`f`(转发函数fwd的目标)为接收一个`std::size_t`的参数,则使用`IPv4Header`对象的`totalLength`字段进行调用没有问题:
|
||||
如果声明我们的函数`f`(转发函数`fwd`的目标)为接收一个`std::size_t`的形参,则使用`IPv4Header`对象的`totalLength`字段进行调用没有问题:
|
||||
|
||||
```cpp
|
||||
void f(std::size_t sz);
|
||||
void f(std::size_t sz); //要调用的函数
|
||||
|
||||
IPv4Header h;
|
||||
...
|
||||
f(h.totalLength);// fine
|
||||
…
|
||||
f(h.totalLength); //可以
|
||||
```
|
||||
|
||||
如果通过 `fwd` 转发 `h.totalLength` 给 `f` 呢,那就是一个不同的情况了:
|
||||
如果通过`fwd`转发`h.totalLength`给`f`呢,那就是一个不同的情况了:
|
||||
|
||||
```cpp
|
||||
fwd(h.totalLength); // error!
|
||||
fwd(h.totalLength); //错误!
|
||||
```
|
||||
|
||||
问题在于 `fwd` 的参数是引用,而 `h.totalLength` 是非常量位域。听起来并不是那么糟糕,但是C++标准非常清楚地谴责了这种组合:非常量引用不应该绑定到位域。禁止的理由很充分。位域可能包含了机器字节的任意部分(比如 32 位 `int` 的 3 - 5 位),但是无法直接定位。我之前提到了在硬件层面引用和指针时一样的,所以没有办法创建一个指向任意bit的指针(C++规定你可以指向的最小单位是`char`),所以就没有办法绑定引用到任意 bit 上。
|
||||
问题在于`fwd`的形参是引用,而`h.totalLength`是non-`const`位域。听起来并不是那么糟糕,但是C++标准非常清楚地谴责了这种组合:non-`const`引用不应该绑定到位域。禁止的理由很充分。位域可能包含了机器字的任意部分(比如32位`int`的3-5位),但是这些东西无法直接寻址。我之前提到了在硬件层面引用和指针是一样的,所以没有办法创建一个指向任意*bit*的指针(C++规定你可以指向的最小单位是`char`),同样没有办法绑定引用到任意*bit*上。
|
||||
|
||||
一旦意识到接收位域作为参数的函数都将接收位域的副本,就可以轻松解决位域不能完美转发的问题。毕竟,没有函数可以绑定引用到位域,也没有函数可以接受指向位域的指针(不存在这种指针)。这种位域类型的参数只能按值传递,或者有趣的事,常量引用也可以。在按值传递时,被调用的函数接受了一个位域的副本,而且事实表明,位域的常量引用也是将其“复制”到普通对象再传递。
|
||||
一旦意识到接收位域实参的函数都将接收位域的**副本**,就可以轻松解决位域不能完美转发的问题。毕竟,没有函数可以绑定引用到位域,也没有函数可以接受指向位域的指针,因为不存在这种指针。位域可以传给的形参种类只有按值传递的形参,有趣的是,还有reference-to-`const`。在传值形参的情况中,被调用的函数接受了一个位域的副本;在传reference-to-`const`形参的情况中,标准要求这个引用实际上绑定到存放位域值的副本对象,这个对象是某种整型(比如`int`)。reference-to-`const`不直接绑定到位域,而是绑定位域值拷贝到的一个普通对象。
|
||||
|
||||
传递位域给完美转发的关键就是利用接收参数函数接受的是一个副本的事实。你可以自己创建副本然后利用副本调用完美转发。在 `IPv4Header` 的例子中,可以如下写法:
|
||||
传递位域给完美转发的关键就是利用传给的函数接受的是一个副本的事实。你可以自己创建副本然后利用副本调用完美转发。在`IPv4Header`的例子中,可以如下写法:
|
||||
|
||||
```cpp
|
||||
// copy bitfield value; see Item6 for info on init. form
|
||||
//拷贝位域值;参看条款6了解关于初始化形式的信息
|
||||
auto length = static_cast<std::uint16_t>(h.totalLength);
|
||||
fwd(length); // forward the copy
|
||||
|
||||
fwd(length); //转发这个副本
|
||||
```
|
||||
|
||||
### 总结
|
||||
|
||||
在大多数情况下,完美转发工作的很好。你基本不用考虑其他问题。但是当其不工作时,当看起来合理的代码无法编译,或者更糟的是,无法按照预期运行时,了解完美转发的缺陷就很重要了。同样重要的是如何解决它们。在大多数情况下都很简单。
|
||||
在大多数情况下,完美转发工作的很好。你基本不用考虑其他问题。但是当其不工作时——当看起来合理的代码无法编译,或者更糟的是,虽能编译但无法按照预期运行时——了解完美转发的缺陷就很重要了。同样重要的是如何解决它们。在大多数情况下,都很简单。
|
||||
|
||||
### 需要记住的事
|
||||
**请记住:**
|
||||
|
||||
- 完美转发会失败当模板类型推导失败或者推导类型错误。
|
||||
- 导致完美转发失败的类型有 braced initializers,作为空指针的 0 或者 `NULL`,只声明 (而未定义) 的整型 static const 数据成员,模板和重载的函数名和位域。
|
||||
- 当模板类型推导失败或者推导出错误类型,完美转发会失败。
|
||||
- 导致完美转发失败的实参种类有花括号初始化,作为空指针的`0`或者`NULL`,仅有声明的整型`static const`数据成员,模板和重载函数的名字,位域。
|
||||
|
BIN
5.RRefMovSemPerfForw/media/item29_fig1.png
Normal file
BIN
5.RRefMovSemPerfForw/media/item29_fig1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
5.RRefMovSemPerfForw/media/item29_fig2.png
Normal file
BIN
5.RRefMovSemPerfForw/media/item29_fig2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -1,246 +1,258 @@
|
||||
Lambda表达式是C++编程中的游戏规则改变者。这有点令人惊讶,因为它没有给语言带来新的表达能力。Lambda可以做的所有事情都可以通过其他方式完成。但是lambda是创建函数对象相当便捷的一种方法,对于日常的C++开发影响是巨大的。没有lambda时,标准库中的`_if`算法(比如,`std::find_if, std::remove_if, std::count_if`等)通常需要繁琐的谓词,但是当有lambda可用时,这些算法使用起来就变得相当方便。比较函数(比如,`std::sort, std::nth_element, std::lower_bound`等)与算法函数也是相同的。在标准库外,lambda可以快速创建`std::unique_ptr`和`std::shared_ptr`的自定义`deleter`,并且使线程API中条件变量的条件设置变得同样简单(参见Item 39)。除了标准库,lambda有利于即时的回调函数,接口适配函数和特定上下文中的一次性函数。Lambda确实使C++成为更令人愉快的编程语言。
|
||||
# 第6章 *lambda*表达式
|
||||
|
||||
与Lambda相关的词汇可能会令人疑惑,这里做一下简单的回顾:
|
||||
**CHAPTER 6 Lambda Expressions**
|
||||
|
||||
- *lambda表达式就是一个表达式*。在代码的高亮部分就是lambda
|
||||
*lambda*表达式是C++编程中的游戏规则改变者。这有点令人惊讶,因为它没有给语言带来新的表达能力。*lambda*可以做的所有事情都可以通过其他方式完成。但是*lambda*是创建函数对象相当便捷的一种方法,对于日常的C++开发影响是巨大的。没有*lambda*时,STL中的“`_if`”算法(比如,`std::find_if`,`std::remove_if`,`std::count_if`等)通常需要繁琐的谓词,但是当有*lambda*可用时,这些算法使用起来就变得相当方便。用比较函数(比如,`std::sort`,`std::nth_element`,`std::lower_bound`等)来自定义算法也是同样方便的。在STL外,*lambda*可以快速创建`std::unique_ptr`和`std::shared_ptr`的自定义删除器(见[Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md)和[19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)),并且使线程API中条件变量的谓词指定变得同样简单(参见[Item39](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md))。除了标准库,*lambda*有利于即时的回调函数,接口适配函数和特定上下文中的一次性函数。*lambda*确实使C++成为更令人愉快的编程语言。
|
||||
|
||||
与*lambda*相关的词汇可能会令人疑惑,这里做一下简单的回顾:
|
||||
|
||||
- ***lambda*表达式**(*lambda expression*)就是一个表达式。下面是部分源代码。在
|
||||
|
||||
```cpp
|
||||
std::find_if(container.begin(), container.end(),
|
||||
[](int val){ return 0 < val && val < 10; }); // 本行高亮
|
||||
[](int val){ return 0 < val && val < 10; }); //译者注:本行高亮
|
||||
```
|
||||
|
||||
- *闭包*是lambda创建的运行时对象。依赖捕获模式,闭包持有捕获数据的副本或者引用。在上面的`std::find_if`调用中,闭包是运行时传递给``std::find_if`第三个参数。
|
||||
中,代码的高亮部分就是*lambda*。
|
||||
|
||||
- *闭包类(closure class)*是从中实例化闭包的类。每个lambda都会使编译器生成唯一的闭包类。Lambda中的语句成为其闭包类的成员函数中的可执行指令。
|
||||
- **闭包**(*enclosure*)是*lambda*创建的运行时对象。依赖捕获模式,闭包持有被捕获数据的副本或者引用。在上面的`std::find_if`调用中,闭包是作为第三个实参在运行时传递给`std::find_if`的对象。
|
||||
|
||||
Lambda通常被用来创建闭包,该闭包仅用作函数的参数。上面对`std::find_if`的调用就是这种情况。然而,闭包通常可以拷贝,所以可能有多个闭包对应于一个lambda。比如下面的代码:
|
||||
- **闭包类**(*closure class*)是从中实例化闭包的类。每个*lambda*都会使编译器生成唯一的闭包类。*lambda*中的语句成为其闭包类的成员函数中的可执行指令。
|
||||
|
||||
*lambda*通常被用来创建闭包,该闭包仅用作函数的实参。上面对`std::find_if`的调用就是这种情况。然而,闭包通常可以拷贝,所以可能有多个闭包对应于一个*lambda*。比如下面的代码:
|
||||
|
||||
```cpp
|
||||
{
|
||||
int x; // x is local variable
|
||||
...
|
||||
auto c1 = [x](int y) { return x * y > 55; }; // c1 is copy of the closure produced by the lambda
|
||||
|
||||
auto c2 = c1; // c2 is copy of c1
|
||||
auto c3 = c2; // c3 is copy of c2
|
||||
...
|
||||
int x; //x是局部对象
|
||||
…
|
||||
|
||||
auto c1 = //c1是lambda产生的闭包的副本
|
||||
[x](int y) { return x * y > 55; };
|
||||
|
||||
auto c2 = c1; //c2是c1的拷贝
|
||||
|
||||
auto c3 = c2; //c3是c2的拷贝
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
c1, c2,c3都是lambda产生的闭包的副本。
|
||||
`c1`,`c2`,`c3`都是*lambda*产生的闭包的副本。
|
||||
|
||||
非正式的讲,模糊lambda,闭包和闭包类之间的界限是可以接受的。但是,在随后的Item中,区分编译期(lambdas 和 closure classes)还是运行时(closures)以及它们之间的相互关系是重要的。
|
||||
非正式的讲,模糊*lambda*,闭包和闭包类之间的界限是可以接受的。但是,在随后的Item中,区分什么存在于编译期(*lambdas* 和闭包类),什么存在于运行时(闭包)以及它们之间的相互关系是重要的。
|
||||
|
||||
# 避免使用默认捕获模式
|
||||
## 条款三十一:避免使用默认捕获模式
|
||||
|
||||
C++11中有两种默认的捕获模式:按引用捕获和按值捕获。但按引用捕获可能会带来悬空引用的问题,而按值引用可能会诱骗你让你以为能解决悬空引用的问题(实际上并没有),还会让你以为你的闭包是独立的(事实上也不是独立的)。
|
||||
**Item 31: Avoid default capture modes**
|
||||
|
||||
这就是本条目的一个总结。如果你是一个工程师,渴望了解更多内容,就让我们从按引用捕获的危害谈起把。
|
||||
C++11中有两种默认的捕获模式:按引用捕获和按值捕获。但默认按引用捕获模式可能会带来悬空引用的问题,而默认按值捕获模式可能会诱骗你让你以为能解决悬空引用的问题(实际上并没有),还会让你以为你的闭包是独立的(事实上也不是独立的)。
|
||||
|
||||
按引用捕获会导致闭包中包含了对局部变量或者某个形参(位于定义lambda的作用域)的引用,如果该lambda创建的闭包生命周期超过了局部变量或者参数的生命周期,那么闭包中的引用将会变成悬空引用。举个例子,假如我们有一个元素是过滤函数的容器,该函数接受一个int作为参数,并返回一个布尔值,该布尔值的结果表示传入的值是否满足过滤条件。
|
||||
这就是本条款的一个总结。如果你偏向技术,渴望了解更多内容,就让我们从按引用捕获的危害谈起吧。
|
||||
|
||||
按引用捕获会导致闭包中包含了对某个局部变量或者形参的引用,变量或形参只在定义*lambda*的作用域中可用。如果该*lambda*创建的闭包生命周期超过了局部变量或者形参的生命周期,那么闭包中的引用将会变成悬空引用。举个例子,假如我们有元素是过滤函数(filtering function)的一个容器,该函数接受一个`int`,并返回一个`bool`,该`bool`的结果表示传入的值是否满足过滤条件:
|
||||
|
||||
```c++
|
||||
using FilterContainer = // see Item 9 for
|
||||
std::vector<std::function<bool(int)>>; // "using", Item 2
|
||||
// for std::function
|
||||
FilterContainer filters; // filtering funcs
|
||||
using FilterContainer = //“using”参见条款9,
|
||||
std::vector<std::function<bool(int)>>; //std::function参见条款2
|
||||
|
||||
FilterContainer filters; //过滤函数
|
||||
```
|
||||
|
||||
我们可以添加一个过滤器,用来过滤掉5的倍数。
|
||||
我们可以添加一个过滤器,用来过滤掉5的倍数:
|
||||
|
||||
```c++
|
||||
filters.emplace_back( // see Item 42 for
|
||||
[](int value) { return value % 5 == 0; } // info on
|
||||
filters.emplace_back( //emplace_back的信息见条款42
|
||||
[](int value) { return value % 5 == 0; }
|
||||
);
|
||||
```
|
||||
|
||||
然而我们可能需要的是能够在运行期获得被除数,而不是将5硬编码到lambda中。因此添加的过滤器逻辑将会是如下这样:
|
||||
然而我们可能需要的是能够在运行期计算除数(divisor),即不能将5硬编码到*lambda*中。因此添加的过滤器逻辑将会是如下这样:
|
||||
|
||||
```c++
|
||||
void addDivisorFilter()
|
||||
{
|
||||
auto calc1 = computeSomeValue1();
|
||||
auto calc2 = computeSomeValue2();
|
||||
|
||||
auto divisor = computeDivisor(calc1, calc2);
|
||||
filters.emplace_back( // danger!
|
||||
[&](int value) { return value % divisor == 0; } // ref to
|
||||
); // divisor
|
||||
} // will
|
||||
// dangle!
|
||||
|
||||
filters.emplace_back( //危险!对divisor的引用
|
||||
[&](int value) { return value % divisor == 0; } //将会悬空!
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这个代码实现是一个定时炸弹。lambda对局部变量divisor进行了引用,但该变量的生命周期会在addDivisorFilter返回时结束,刚好就是在语句filters.emplace_back返回之后,因此该函数的本质就是容器添加完,该函数就死亡了。使用这个filter会导致未定义行为,这是由它被创建那一刻起就决定了的。
|
||||
这个代码实现是一个定时炸弹。*lambda*对局部变量`divisor`进行了引用,但该变量的生命周期会在`addDivisorFilter`返回时结束,刚好就是在语句`filters.emplace_back`返回之后。因此添加到`filters`的函数添加完,该函数就死亡了。使用这个过滤器(译者注:就是那个添加进`filters`的函数)会导致未定义行为,这是由它被创建那一刻起就决定了的。
|
||||
|
||||
现在,同样的问题也会出现在divisor的显式按引用捕获。
|
||||
现在,同样的问题也会出现在`divisor`的显式按引用捕获。
|
||||
|
||||
```c++
|
||||
filters.emplace_back(
|
||||
[&divisor](int value) // danger! ref to
|
||||
{ return value % divisor == 0; } // divisor will
|
||||
[&divisor](int value) //危险!对divisor的引用将会悬空!
|
||||
{ return value % divisor == 0; }
|
||||
);
|
||||
```
|
||||
|
||||
但通过显式的捕获,能更容易看到lambda的可行性依赖于变量divisor的生命周期。另外,写成这种形式能够提醒我们要注意确保divisor的生命周期至少跟lambda闭包一样长。比起"[&]"传达的意思,显式捕获能让人更容易想起“确保没有悬空变量”。
|
||||
但通过显式的捕获,能更容易看到*lambda*的可行性依赖于变量`divisor`的生命周期。另外,写下“divisor”这个名字能够提醒我们要注意确保`divisor`的生命周期至少跟*lambda*闭包一样长。比起“`[&]`”传达的意思,显式捕获能让人更容易想起“确保没有悬空变量”。
|
||||
|
||||
如果你知道一个闭包将会被马上使用(例如被传入到一个stl算法中)并且不会被拷贝,那么在lambda环境中使用引用捕获将不会有风险。在这种情况下,你可能会争论说,没有悬空引用的危险,就不需要避免使用默认的引用捕获模式。例如,我们的过滤lambda只会用做C++11中std::all_of的一个参数,返回满足条件的所有元素:
|
||||
如果你知道一个闭包将会被马上使用(例如被传入到一个STL算法中)并且不会被拷贝,那么在它的*lambda*被创建的环境中,将不会有持有的引用比局部变量和形参活得长的风险。在这种情况下,你可能会争论说,没有悬空引用的危险,就不需要避免使用默认的引用捕获模式。例如,我们的过滤*lambda*只会用做C++11中`std::all_of`的一个实参,返回满足条件的所有元素:
|
||||
|
||||
```c++
|
||||
template<typename C>
|
||||
void workWithContainer(const C& container)
|
||||
{
|
||||
auto calc1 = computeSomeValue1(); // as above
|
||||
auto calc2 = computeSomeValue2(); // as above
|
||||
auto divisor = computeDivisor(calc1, calc2); // as above
|
||||
using ContElemT = typename C::value_type; // type of
|
||||
// elements in
|
||||
// container
|
||||
using std::begin; // for
|
||||
using std::end; // genericity;
|
||||
// see Item 13
|
||||
if (std::all_of( // if all values
|
||||
begin(container), end(container), // in container
|
||||
[&](const ContElemT& value) // are multiples
|
||||
{ return value % divisor == 0; }) // of divisor...
|
||||
) {
|
||||
… // they are...
|
||||
auto calc1 = computeSomeValue1(); //同上
|
||||
auto calc2 = computeSomeValue2(); //同上
|
||||
auto divisor = computeDivisor(calc1, calc2); //同上
|
||||
|
||||
using ContElemT = typename C::value_type; //容器内元素的类型
|
||||
using std::begin; //为了泛型,见条款13
|
||||
using std::end;
|
||||
|
||||
if (std::all_of( //如果容器内所有值都为
|
||||
begin(container), end(container), //除数的倍数
|
||||
[&](const ContElemT& value)
|
||||
{ return value % divisor == 0; })
|
||||
) {
|
||||
… //它们...
|
||||
} else {
|
||||
… // at least one
|
||||
} // isn't...
|
||||
… //至少有一个不是的话...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
的确如此,这是安全的做法,但这种安全是不确定的。如果发现lambda在其它上下文中很有用(例如作为一个函数被添加在filters容器中),然后拷贝粘贴到一个divisor变量已经死亡的,但闭包生命周期还没结束的上下文中,你又回到了悬空的使用上了。同时,在该捕获语句中,也没有特别提醒了你注意分析divisor的生命周期。
|
||||
的确如此,这是安全的做法,但这种安全是不确定的。如果发现*lambda*在其它上下文中很有用(例如作为一个函数被添加在`filters`容器中),然后拷贝粘贴到一个`divisor`变量已经死亡,但闭包生命周期还没结束的上下文中,你又回到了悬空的使用上了。同时,在该捕获语句中,也没有特别提醒了你注意分析`divisor`的生命周期。
|
||||
|
||||
从长期来看,使用显式的局部变量和参数引用捕获方式,是更加符合软件工程规范的做法。
|
||||
从长期来看,显式列出*lambda*依赖的局部变量和形参,是更加符合软件工程规范的做法。
|
||||
|
||||
额外提一下,C++14支持了在lambda中使用auto来声明变量,上面的代码在C++14中可以进一步简化,ContElemT的别名可以去掉,if条件可以修改为:
|
||||
额外提一下,C++14支持了在*lambda*中使用`auto`来声明变量,上面的代码在C++14中可以进一步简化,`ContElemT`的别名可以去掉,`if`条件可以修改为:
|
||||
|
||||
```c++
|
||||
if (std::all_of(begin(container), end(container),
|
||||
[&](const auto& value) // C++14
|
||||
[&](const auto& value) // C++14
|
||||
{ return value % divisor == 0; }))
|
||||
```
|
||||
|
||||
一个解决问题的方法是,divisor按值捕获进去,也就是说可以按照以下方式来添加lambda:
|
||||
一个解决问题的方法是,`divisor`默认按值捕获进去,也就是说可以按照以下方式来添加*lambda*到`filters`:
|
||||
|
||||
```c++
|
||||
filters.emplace_back( // now
|
||||
[=](int value) { return value % divisor == 0; } // divisor
|
||||
); // can't
|
||||
// dangle
|
||||
filters.emplace_back( //现在divisor不会悬空了
|
||||
[=](int value) { return value % divisor == 0; }
|
||||
);
|
||||
```
|
||||
|
||||
这足以满足本实例的要求,但在通常情况下,按值捕获并不能完全解决悬空引用的问题。这里的问题是如果你按值捕获的是一个指针,你将该指针拷贝到lambda对应的闭包里,但这样并不能避免lambda外删除指针的行为,从而导致你的指针变成悬空指针。
|
||||
这足以满足本实例的要求,但在通常情况下,按值捕获并不能完全解决悬空引用的问题。这里的问题是如果你按值捕获的是一个指针,你将该指针拷贝到*lambda*对应的闭包里,但这样并不能避免*lambda*外`delete`这个指针的行为,从而导致你的副本指针变成悬空指针。
|
||||
|
||||
也许你要抗议说:“这不可能发生。看过了第四章,我对智能指针的使用非常热衷。只有那些失败的C++98的程序员才会用裸指针和delete语句。”这也许是正确的,但却是不相关的,因为事实上你的确会使用裸指针,也的确存在被你删除的可能性。只不过在现代的C++编程风格中,不容易在源代码中显露出来而已。
|
||||
也许你要抗议说:“这不可能发生。看过了[第4章](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md),我对智能指针的使用非常热衷。只有那些失败的C++98的程序员才会用裸指针和`delete`语句。”这也许是正确的,但却是不相关的,因为事实上你的确会使用裸指针,也的确存在被你`delete`的可能性。只不过在现代的C++编程风格中,不容易在源代码中显露出来而已。
|
||||
|
||||
假设在一个Widget类,可以实现向过滤容器添加条目:
|
||||
假设在一个`Widget`类,可以实现向过滤器的容器添加条目:
|
||||
|
||||
```c++
|
||||
class Widget {
|
||||
public:
|
||||
… // ctors, etc.
|
||||
void addFilter() const; // add an entry to filters
|
||||
… //构造函数等
|
||||
void addFilter() const; //向filters添加条目
|
||||
private:
|
||||
int divisor; // used in Widget's filter
|
||||
int divisor; //在Widget的过滤器使用
|
||||
};
|
||||
```
|
||||
|
||||
这是Widget::addFilter的定义:
|
||||
这是`Widget::addFilter`的定义:
|
||||
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
filters.emplace_back(
|
||||
[=](int value) { return value % divisor == 0; }
|
||||
);
|
||||
filters.emplace_back(
|
||||
[=](int value) { return value % divisor == 0; }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这个做法看起来是安全的代码,lambda依赖于变量divisor,但默认的按值捕获被拷贝进了lambda对应的所有比保重,这真的正确吗?
|
||||
这个做法看起来是安全的代码。*lambda*依赖于`divisor`,但默认的按值捕获确保`divisor`被拷贝进了*lambda*对应的所有闭包中,对吗?
|
||||
|
||||
错误,完全错误。
|
||||
|
||||
闭包只会对lambda被创建时所在作用域里的非静态局部变量生效。在Widget::addFilter()的视线里,divisor并不是一个局部变量,而是Widget类的一个成员变量。它不能被捕获。如果默认捕获模式被删除,代码就不能编译了:
|
||||
捕获只能应用于*lambda*被创建时所在作用域里的non-`static`局部变量(包括形参)。在`Widget::addFilter`的视线里,`divisor`并不是一个局部变量,而是`Widget`类的一个成员变量。它不能被捕获。而如果默认捕获模式被删除,代码就不能编译了:
|
||||
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
filters.emplace_back( // error!
|
||||
[](int value) { return value % divisor == 0; } // divisor
|
||||
); // not
|
||||
} // available
|
||||
filters.emplace_back( //错误!
|
||||
[](int value) { return value % divisor == 0; } //divisor不可用
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
另外,如果尝试去显式地按引用或者按值捕获divisor变量,也一样会编译失败,因为divisor不是这里的一个局部变量或者参数。
|
||||
另外,如果尝试去显式地捕获`divisor`变量(或者按引用或者按值——这不重要),也一样会编译失败,因为`divisor`不是一个局部变量或者形参。
|
||||
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
filters.emplace_back(
|
||||
[divisor](int value) // error! no local
|
||||
{ return value % divisor == 0; } // divisor to capture
|
||||
[divisor](int value) //错误!没有名为divisor局部变量可捕获
|
||||
{ return value % divisor == 0; }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
因此这里的默认按值捕获并不是不会变量divisor,但它的确能够编译通过,这是怎么一回事呢?
|
||||
所以如果默认按值捕获不能捕获`divisor`,而不用默认按值捕获代码就不能编译,这是怎么一回事呢?
|
||||
|
||||
解释就是这里隐式捕获了this指针。每一个非静态成员函数都有一个this指针,每次你使用一个类内的成员时都会使用到这个指针。例如,编译器会在内部将divisor替换成this->divisor。这里Widget::addFilter()的版本就是按值捕获了this。
|
||||
解释就是这里隐式使用了一个原始指针:`this`。每一个non-`static`成员函数都有一个`this`指针,每次你使用一个类内的数据成员时都会使用到这个指针。例如,在任何`Widget`成员函数中,编译器会在内部将`divisor`替换成`this->divisor`。在默认按值捕获的`Widget::addFilter`版本中,
|
||||
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
filters.emplace_back(
|
||||
[=](int value) { return value % divisor == 0; }
|
||||
[=](int value) { return value % divisor == 0; }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
真正被捕获的是Widget的this指针。编译器会将上面的代码看成以下的写法:
|
||||
真正被捕获的是`Widget`的`this`指针,而不是`divisor`。编译器会将上面的代码看成以下的写法:
|
||||
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
auto currentObjectPtr = this;
|
||||
|
||||
filters.emplace_back(
|
||||
[currentObjectPtr](int value)
|
||||
{ return value % currentObject->divisor == 0; }
|
||||
[currentObjectPtr](int value)
|
||||
{ return value % currentObjectPtr->divisor == 0; }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
明白了这个就相当于明白了lambda闭包的生命周期与Widget对象的关系,闭包内含有Widget的this指针的拷贝。特别是考虑以下的代码,再参考一下第四章的内容,只使用智能指针:
|
||||
明白了这个就相当于明白了*lambda*闭包的生命周期与`Widget`对象的关系,闭包内含有`Widget`的`this`指针的拷贝。特别是考虑以下的代码,参考[第4章](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md)的内容,只使用智能指针:
|
||||
|
||||
```c++
|
||||
using FilterContainer = // as before
|
||||
std::vector<std::function<bool(int)>>;
|
||||
FilterContainer filters; // as before
|
||||
using FilterContainer = //跟之前一样
|
||||
std::vector<std::function<bool(int)>>;
|
||||
|
||||
FilterContainer filters; //跟之前一样
|
||||
|
||||
void doSomeWork()
|
||||
{
|
||||
auto pw = // create Widget; see
|
||||
std::make_unique<Widget>(); // Item 21 for
|
||||
// std::make_unique
|
||||
pw->addFilter(); // add filter that uses
|
||||
// Widget::divisor
|
||||
…
|
||||
} // destroy Widget; filters
|
||||
// now holds dangling pointer!
|
||||
auto pw = //创建Widget;std::make_unique
|
||||
std::make_unique<Widget>(); //见条款21
|
||||
|
||||
pw->addFilter(); //添加使用Widget::divisor的过滤器
|
||||
|
||||
…
|
||||
} //销毁Widget;filters现在持有悬空指针!
|
||||
```
|
||||
|
||||
当调用doSomeWork时,就会创建一个过滤器,其生命周期依赖于由std::make_unique管理的Widget对象。即一个含有Widget this指针的过滤器。这个过滤器被添加到filters中,但当doSomeWork结束时,Widget会由std::unique_ptr去结束其生命。从这时起,filter会含有一个悬空指针。
|
||||
当调用`doSomeWork`时,就会创建一个过滤器,其生命周期依赖于由`std::make_unique`产生的`Widget`对象,即一个含有指向`Widget`的指针——`Widget`的`this`指针——的过滤器。这个过滤器被添加到`filters`中,但当`doSomeWork`结束时,`Widget`会由管理它的`std::unique_ptr`来销毁(见[Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md))。从这时起,`filter`会含有一个存着悬空指针的条目。
|
||||
|
||||
这个特定的问题可以通过做一个局部拷贝去解决:
|
||||
这个特定的问题可以通过给你想捕获的数据成员做一个局部副本,然后捕获这个副本去解决:
|
||||
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
auto divisorCopy = divisor; // copy data member
|
||||
filters.emplace_back(
|
||||
[divisorCopy](int value) // capture the copy
|
||||
{ return value % divisorCopy == 0; } // use the copy
|
||||
);
|
||||
auto divisorCopy = divisor; //拷贝数据成员
|
||||
|
||||
filters.emplace_back(
|
||||
[divisorCopy](int value) //捕获副本
|
||||
{ return value % divisorCopy == 0; } //使用副本
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@ -249,50 +261,54 @@ void Widget::addFilter() const
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
auto divisorCopy = divisor; // copy data member
|
||||
filters.emplace_back(
|
||||
[=](int value) // capture the copy
|
||||
{ return value % divisorCopy == 0; } // use the copy
|
||||
);
|
||||
auto divisorCopy = divisor; //拷贝数据成员
|
||||
|
||||
filters.emplace_back(
|
||||
[=](int value) //捕获副本
|
||||
{ return value % divisorCopy == 0; } //使用副本
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
但为什么要冒险呢?当你一开始捕获divisor的时候,默认的捕获模式就会自动将this指针捕获进来了。
|
||||
但为什么要冒险呢?当一开始你认为你捕获的是`divisor`的时候,默认捕获模式就是造成可能意外地捕获`this`的元凶。
|
||||
|
||||
在C++14中,一个更好的捕获成员变量的方式时使用通用的lambda捕获:
|
||||
在C++14中,一个更好的捕获成员变量的方式时使用通用的*lambda*捕获:
|
||||
|
||||
```c++
|
||||
void Widget::addFilter() const
|
||||
{
|
||||
filters.emplace_back( // C++14:
|
||||
[divisor = divisor](int value) // copy divisor to closure
|
||||
{ return value % divisor == 0; } // use the copy
|
||||
);
|
||||
filters.emplace_back( //C++14:
|
||||
[divisor = divisor](int value) //拷贝divisor到闭包
|
||||
{ return value % divisor == 0; } //使用这个副本
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这种通用的lambda捕获并没有默认的捕获模式,因此在C++14中,避免使用默认捕获模式的建议仍然时成立的。
|
||||
这种通用的*lambda*捕获并没有默认的捕获模式,因此在C++14中,本条款的建议——避免使用默认捕获模式——仍然是成立的。
|
||||
|
||||
使用默认的按值捕获还有另外的一个缺点,它们预示了相关的闭包是独立的并且不受外部数据变化的影响。一般来说,这是不对的。lambda并不会独立于局部变量和参数,但也没有不受静态存储生命周期的影响。一个定义在全局空间或者指定命名空间的全局变量,或者是一个声明为static的类内或文件内的成员。这些对象也能在lambda里使用,但它们不能被捕获。但按值引用可能会因此误导你,让你以为捕获了这些变量。参考下面版本的addDivisorFilter()函数:
|
||||
使用默认的按值捕获还有另外的一个缺点,它们预示了相关的闭包是独立的并且不受外部数据变化的影响。一般来说,这是不对的。*lambda*可能会依赖局部变量和形参(它们可能被捕获),还有**静态存储生命周期**(static storage duration)的对象。这些对象定义在全局空间或者命名空间,或者在类、函数、文件中声明为`static`。这些对象也能在*lambda*里使用,但它们不能被捕获。但默认按值捕获可能会因此误导你,让你以为捕获了这些变量。参考下面版本的`addDivisorFilter`函数:
|
||||
|
||||
```c++
|
||||
void addDivisorFilter()
|
||||
{
|
||||
static auto calc1 = computeSomeValue1(); // now static
|
||||
static auto calc2 = computeSomeValue2(); // now static
|
||||
static auto divisor = // now static
|
||||
computeDivisor(calc1, calc2);
|
||||
filters.emplace_back(
|
||||
[=](int value) // captures nothing!
|
||||
{ return value % divisor == 0; } // refers to above static
|
||||
);
|
||||
++divisor; // modify divisor
|
||||
static auto calc1 = computeSomeValue1(); //现在是static
|
||||
static auto calc2 = computeSomeValue2(); //现在是static
|
||||
static auto divisor = //现在是static
|
||||
computeDivisor(calc1, calc2);
|
||||
|
||||
filters.emplace_back(
|
||||
[=](int value) //什么也没捕获到!
|
||||
{ return value % divisor == 0; } //引用上面的static
|
||||
);
|
||||
|
||||
++divisor; //调整divisor
|
||||
}
|
||||
```
|
||||
|
||||
随意地看了这份代码的读者可能看到"[=]",就会认为“好的,lambda拷贝了所有使用的对象,因此这是独立的”。但上面的例子就表现了不独立闭包的一种情况。它没有使用任何的非static局部变量和形参,所以它没有捕获任何东西。然而lambda的代码引用了静态变量divisor,任何lambda被添加到filters之后,divisor都会递增。通过这个函数,会把许多lambda都添加到filiters里,但每一个lambda的行为都是新的(分别对应新的divisor值)。这个lambda是通过引用捕获divisor,这和默认的按值捕获表示的含义有着直接的矛盾。如果你一开始就避免使用默认的按值捕获模式,你就能解除代码的风险。
|
||||
随意地看了这份代码的读者可能看到“`[=]`”,就会认为“好的,*lambda*拷贝了所有使用的对象,因此这是独立的”。但其实不独立。这个*lambda*没有使用任何的non-`static`局部变量,所以它没有捕获任何东西。然而*lambda*的代码引用了`static`变量`divisor`,在每次调用`addDivisorFilter`的结尾,`divisor`都会递增,通过这个函数添加到`filters`的所有*lambda*都展示新的行为(分别对应新的`divisor`值)。这个*lambda*是通过引用捕获`divisor`,这和默认的按值捕获表示的含义有着直接的矛盾。如果你一开始就避免使用默认的按值捕获模式,你就能解除代码的风险。
|
||||
|
||||
## 建议
|
||||
**请记住:**
|
||||
|
||||
+ 默认的按引用捕获可能会导致悬空引用。
|
||||
+ 默认的按值捕获对于悬空指针很敏感(尤其是`this`指针),并且它会误导人产生*lambda*是独立的想法。
|
||||
|
||||
* 默认的按引用捕获可能会导致悬空引用;
|
||||
* 默认的按值引用对于悬空指针很敏感(尤其是this指针),并且它会误导人产生lambda是独立的想法;
|
@ -1,146 +1,161 @@
|
||||
# 使用初始化捕获来移动对象到闭包中
|
||||
## 条款三十二:使用初始化捕获来移动对象到闭包中
|
||||
|
||||
在某些场景下,按值捕获和按引用捕获都不是你所想要的。如果你有一个只能被移动的对象(例如**std::unique_ptr或std::future**)要进入到闭包里,使用C++11是无法实现的。如果你要复制的对象复制开销非常高,但移动的成本却不高(例如标准库中的大多数容器),并且你希望的是宁愿移动该对象到闭包而不是复制它。然而C++11却无法实现这一目标。
|
||||
**Item 32: Use init capture to move objects into closures**
|
||||
|
||||
但如果你的编译器支持C++14,那又是另一回事了,它能支持将对象移动道闭包中。如果你的兼容支持C++14,那么请愉快地阅读下去。如果你仍然在使用仅支持C++11的编译器,也请愉快阅读,因为在C++11中有很多方法可以实现近似的移动捕获。
|
||||
在某些场景下,按值捕获和按引用捕获都不是你所想要的。如果你有一个只能被移动的对象(例如`std::unique_ptr`或`std::future`)要进入到闭包里,使用C++11是无法实现的。如果你要复制的对象复制开销非常高,但移动的成本却不高(例如标准库中的大多数容器),并且你希望的是宁愿移动该对象到闭包而不是复制它。然而C++11却无法实现这一目标。
|
||||
|
||||
缺少移动捕获被认为是C++11的一个缺点,直接的补救措施是将该特性添加到C++14中,但标准化委员会选择了另一种方法。他们引入了一种新的捕获机制,该机制非常灵活,移动捕获是它执行的技术之一。新功能被称作初始化捕获,它几乎可以完成C++11捕获形式的所有工作,甚至能完成更多功能。默认的捕获模式使得你无法使用初始化捕获表示,但第31项说明提醒了你无论如何都应该远离这些捕获模式。(在C++11捕获模式所能覆盖的场景里,初始化捕获的语法有点不大方便。因此在C++11的捕获模式能完成所需功能的情况下,使用它是完全合理的)。
|
||||
但那是C++11的时候。到了C++14就另一回事了,它能支持将对象移动到闭包中。如果你的编译器兼容支持C++14,那么请愉快地阅读下去。如果你仍然在使用仅支持C++11的编译器,也请愉快阅读,因为在C++11中有很多方法可以实现近似的移动捕获。
|
||||
|
||||
缺少移动捕获被认为是C++11的一个缺点,直接的补救措施是将该特性添加到C++14中,但标准化委员会选择了另一种方法。他们引入了一种新的捕获机制,该机制非常灵活,移动捕获是它可以执行的技术之一。新功能被称作**初始化捕获**(*init capture*),C++11捕获形式能做的所有事它几乎可以做,甚至能完成更多功能。你不能用初始化捕获表达的东西是默认捕获模式,但[Item31](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item31.md)说明提醒了你无论如何都应该远离默认捕获模式。(在C++11捕获模式所能覆盖的场景里,初始化捕获的语法有点不大方便。因此在C++11的捕获模式能完成所需功能的情况下,使用它是完全合理的)。
|
||||
|
||||
使用初始化捕获可以让你指定:
|
||||
|
||||
1. 从lambda生成的闭包类中的数据成员名称;
|
||||
2. 初始化该成员的表达式;
|
||||
1. 从lambda生成的闭包类中的**数据成员名称**;
|
||||
2. 初始化该成员的**表达式**;
|
||||
|
||||
这是使用初始化捕获将**std::unique_ptr**移动到闭包中的方法:
|
||||
这是使用初始化捕获将`std::unique_ptr`移动到闭包中的方法:
|
||||
|
||||
```c++
|
||||
class Widget { // some useful type
|
||||
class Widget { //一些有用的类型
|
||||
public:
|
||||
...
|
||||
bool isValidated() const;
|
||||
bool isProcessed() const;
|
||||
bool isArchived() const;
|
||||
private: ...
|
||||
…
|
||||
bool isValidated() const;
|
||||
bool isProcessed() const;
|
||||
bool isArchived() const;
|
||||
private:
|
||||
…
|
||||
};
|
||||
|
||||
auto pw = std::make_unique<Widget>(); // create Widget; see Item 21 for info on std::make_unique configure *pw
|
||||
auto pw = std::make_unique<Widget>(); //创建Widget;使用std::make_unique
|
||||
//的有关信息参见条款21
|
||||
|
||||
auto func = [pw = std::move(pw)] // init data mbr in closure w/ std::move(pw)
|
||||
{ return pw->isValidated()
|
||||
&& pw->isArchived(); };
|
||||
… //设置*pw
|
||||
|
||||
auto func = [pw = std::move(pw)] //使用std::move(pw)初始化闭包数据成员
|
||||
{ return pw->isValidated()
|
||||
&& pw->isArchived(); };
|
||||
```
|
||||
|
||||
上面的文本包含了初始化捕获的使用,"="的左侧是指定的闭包类中数据成员的名称,右侧则是初始化表达式。有趣的是,"="左侧的作用范围不同于右侧的作用范围。在上面的示例中,'='左侧的名称`pw`表示闭包类中的数据成员,而右侧的名称`pw`表示在lambda上方声明的对象,即由调用初始化的变量到调用`std::make_unique`。因此,`pw = std :: move(pw)`的意思是“在闭包中创建一个数据成员pw,并通过将`std::move`应用于局部变量pw的方法来初始化该数据成员。
|
||||
高亮的文本包含了初始化捕获的使用(译者注:高亮了“`pw = std::move(pw)`”),“`=`”的左侧是指定的闭包类中数据成员的名称,右侧则是初始化表达式。有趣的是,“`=`”左侧的作用域不同于右侧的作用域。左侧的作用域是闭包类,右侧的作用域和*lambda*定义所在的作用域相同。在上面的示例中,“`=`”左侧的名称`pw`表示闭包类中的数据成员,而右侧的名称`pw`表示在*lambda*上方声明的对象,即由调用`std::make_unique`去初始化的变量。因此,“`pw = std::move(pw)`”的意思是“在闭包中创建一个数据成员`pw`,并使用将`std::move`应用于局部变量`pw`的结果来初始化该数据成员”。
|
||||
|
||||
一般中,lambda主体中的代码在闭包类的作用范围内,因此pw的使用指的是闭包类的数据成员。
|
||||
一般来说,*lambda*主体中的代码在闭包类的作用域内,因此`pw`的使用指的是闭包类的数据成员。
|
||||
|
||||
在此示例中,注释`configure * pw`表示在由`std::make_unique`创建窗口小部件之后,再由lambda捕获到该窗口小部件的`std::unique_ptr`之前,该窗口小部件即pw对象以某种方式进行了修改。如果不需要这样的配置,即如果`std::make_unique`创建的`Widget`处于适合被lambda捕获的状态,则不需要局部变量`pw`,因为闭包类的数据成员可以通过直接初始化`std::make_unique`来实现:
|
||||
在此示例中,注释“设置`*pw`”表示在由`std::make_unique`创建`Widget`之后,*lambda*捕获到指向`Widget`的`std::unique_ptr`之前,该`Widget`以某种方式进行了修改。如果不需要这样的设置,即如果`std::make_unique`创建的`Widget`处于适合被*lambda*捕获的状态,则不需要局部变量`pw`,因为闭包类的数据成员可以通过`std::make_unique`直接初始化:
|
||||
|
||||
```c++
|
||||
auto func = [pw = std::make_unique<Widget>()] // init data mbr
|
||||
{ return pw->isValidated() // in closure w/
|
||||
&& pw->isArchived(); }; // result of call // to make_unique
|
||||
auto func = [pw = std::make_unique<Widget>()] //使用调用make_unique得到的结果
|
||||
{ return pw->isValidated() //初始化闭包数据成员
|
||||
&& pw->isArchived(); };
|
||||
```
|
||||
|
||||
这清楚地表明了,这个C ++ 14的捕获概念是从C ++11发展出来的的,在C ++11中,无法捕获表达式的结果。 因此,初始化捕获的另一个名称是广义lambda捕获。
|
||||
但是,如果您使用的一个或多个编译器不支持C ++ 14的初始捕获怎么办? 如何使用不支持移动捕获的语言完成移动捕获?
|
||||
这清楚地表明了,这个C++14的捕获概念是从C++11发展出来的的,在C++11中,无法捕获表达式的结果。 因此,初始化捕获的另一个名称是**通用*lambda*捕获**(*generalized lambda capture*)。
|
||||
|
||||
请记住,lambda表达式只是生成类和创建该类型对象的一种方式而已。如果对于lambda,你觉得无能为力。 那么我们刚刚看到的C++ 14的示例代码可以用C ++11重新编写,如下所示:
|
||||
但是,如果你使用的一个或多个编译器不支持C++14的初始捕获怎么办? 如何使用不支持移动捕获的语言完成移动捕获?
|
||||
|
||||
请记住,*lambda*表达式只是生成一个类和创建该类型对象的一种简单方式而已。没什么事是你用*lambda*可以做而不能自己手动实现的。 那么我们刚刚看到的C++14的示例代码可以用C++11重新编写,如下所示:
|
||||
|
||||
```c++
|
||||
class IsValAndArch {
|
||||
class IsValAndArch { //“is validated and archived”
|
||||
public:
|
||||
using DataType = std::unique_ptr<Widget>; // "is validated and archived"
|
||||
explicit IsValAndArch(DataType&& ptr) // Item 25 explains
|
||||
: pw(std::move(ptr)) {} // use of std::move
|
||||
bool operator()() const
|
||||
{ return pw->isValidated() && pw->isArchived(); }
|
||||
using DataType = std::unique_ptr<Widget>;
|
||||
|
||||
explicit IsValAndArch(DataType&& ptr) //条款25解释了std::move的使用
|
||||
: pw(std::move(ptr)) {}
|
||||
|
||||
bool operator()() const
|
||||
{ return pw->isValidated() && pw->isArchived(); }
|
||||
|
||||
private:
|
||||
DataType pw;
|
||||
DataType pw;
|
||||
};
|
||||
|
||||
auto func = IsValAndArch(std::make_unique<Widget>());
|
||||
```
|
||||
|
||||
这个代码量比lambda表达式要多,但这并不难改变这样一个事实,即如果你希望使用一个C++11的类来支持其数据成员的移动初始化,那么你唯一要做的就是在键盘上多花点时间。
|
||||
这个代码量比*lambda*表达式要多,但这并不难改变这样一个事实,即如果你希望使用一个C++11的类来支持其数据成员的移动初始化,那么你唯一要做的就是在键盘上多花点时间。
|
||||
|
||||
如果你坚持要使用lambda(并且考虑到它们的便利性,你可能会这样做),可以在C++11中这样使用:
|
||||
如果你坚持要使用*lambda*(并且考虑到它们的便利性,你可能会这样做),移动捕获可以在C++11中这样模拟:
|
||||
|
||||
1. 将要捕获的对象移动到由`std::bind`;
|
||||
2. 将被捕获的对象赋予一个引用给lambda;
|
||||
1. **将要捕获的对象移动到由`std::bind`产生的函数对象中;**
|
||||
2. **将“被捕获的”对象的引用赋予给*lambda*。**
|
||||
|
||||
如果你熟悉std::bind,那么代码其实非常简单。如果你不熟悉std::bind,那可能需要花费一些时间来习惯改代码,但这无疑是值得的。
|
||||
如果你熟悉`std::bind`,那么代码其实非常简单。如果你不熟悉`std::bind`,那可能需要花费一些时间来习惯它,但这无疑是值得的。
|
||||
|
||||
假设你要创建一个本地的`std::vector`,在其中放入一组适当的值,然后将其移动到闭包中。在C ++14中,这很容易实现:
|
||||
假设你要创建一个本地的`std::vector`,在其中放入一组适当的值,然后将其移动到闭包中。在C++14中,这很容易实现:
|
||||
|
||||
```c++
|
||||
std::vector<double> data; // object to be moved
|
||||
// into closure
|
||||
// populate data
|
||||
auto func = [data = std::move(data)] { /* uses of data */ }; // C++14 init capture
|
||||
std::vector<double> data; //要移动进闭包的对象
|
||||
|
||||
… //填充data
|
||||
|
||||
auto func = [data = std::move(data)] //C++14初始化捕获
|
||||
{ /*使用data*/ };
|
||||
```
|
||||
|
||||
我已经对该代码的关键部分进行了高亮:要移动的对象的类型(`std::vector\<double>`),该对象的名称(数据)以及用于初始化捕获的初始化表达式(`std::move(data)`)。C++11的等效代码如下,其中我强调了相同的关键事项:
|
||||
我已经对该代码的关键部分进行了高亮:要移动的对象的类型(`std::vector<double>`),该对象的名称(`data`)以及用于初始化捕获的初始化表达式(`std::move(data)`)。C++11的等效代码如下,其中我强调了相同的关键事项:
|
||||
|
||||
```c++
|
||||
std::vector<double> data; // as above
|
||||
std::vector<double> data; //同上
|
||||
|
||||
… //同上
|
||||
|
||||
auto func =
|
||||
std::bind( // C++11 emulation
|
||||
[](const std::vector<double>& data) { /* uses of data */ }, // of init capture
|
||||
std::move(data)
|
||||
);
|
||||
std::bind( //C++11模拟初始化捕获
|
||||
[](const std::vector<double>& data) //译者注:本行高亮
|
||||
{ /*使用data*/ },
|
||||
std::move(data) //译者注:本行高亮
|
||||
);
|
||||
```
|
||||
|
||||
如lambda表达式一样,`std::bind`生产了函数对象。我将它称呼为由std::bind所绑定对象返回的函数对象。`std::bind`的第一个参数是可调用对象,后续参数表示要传递给该对象的值。
|
||||
如*lambda*表达式一样,`std::bind`产生函数对象。我将由`std::bind`返回的函数对象称为**bind对象**(*bind objects*)。`std::bind`的第一个实参是可调用对象,后续实参表示要传递给该对象的值。
|
||||
|
||||
一个绑定的对象包含了传递给`std::bind`的所有参数副本。对于每个左值参数,绑定对象中的对应对象都是复制构造的。对于每个右值,它都是移动构造的。在此示例中,第二个参数是一个右值(`std::move`的结果,请参见第23项),因此将数据移动到绑定对象中。这种移动构造是模仿移动捕获的关键,因为将右值移动到绑定对象是我们解决无法将右值移动到C++11闭包中的方法。
|
||||
一个bind对象包含了传递给`std::bind`的所有实参的副本。对于每个左值实参,bind对象中的对应对象都是复制构造的。对于每个右值,它都是移动构造的。在此示例中,第二个实参是一个右值(`std::move`的结果,请参见[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)),因此将`data`移动构造到绑定对象中。这种移动构造是模仿移动捕获的关键,因为将右值移动到bind对象是我们解决无法将右值移动到C++11闭包中的方法。
|
||||
|
||||
当“调用”绑定对象(即调用其函数调用运算符)时,其存储的参数将传递到最初传递给`std::bind`的可调用对象。在此示例中,这意味着当调用func(绑定对象)时,func中所移动构造的数据副本将作为参数传递给传递给`std::bind`中的lambda。
|
||||
当“调用”bind对象(即调用其函数调用运算符)时,其存储的实参将传递到最初传递给`std::bind`的可调用对象。在此示例中,这意味着当调用`func`(bind对象)时,`func`中所移动构造的`data`副本将作为实参传递给`std::bind`中的*lambda*。
|
||||
|
||||
该lambda与我们在C++14中使用的lambda相同,只是添加了一个参数data来对应我们的伪移动捕获对象。此参数是对绑定对象中数据副本的左值引用。(这不是右值引用,因尽管用于初始化数据副本的表达式(`std::move(data)`)为右值,但数据副本本身为左值。)因此,lambda将对绑定在对象内部的移动构造数据副本进行操作。
|
||||
该*lambda*与我们在C++14中使用的*lambda*相同,只是添加了一个形参`data`来对应我们的伪移动捕获对象。此形参是对bind对象中`data`副本的左值引用。(这不是右值引用,因为尽管用于初始化`data`副本的表达式(`std::move(data)`)为右值,但`data`副本本身为左值。)因此,*lambda*将对绑定在对象内部的移动构造的`data`副本进行操作。
|
||||
|
||||
默认情况下,从lambda生成的闭包类中的`operator()`成员函数为`const`的。这具有在lambda主体内呈现闭包中的所有数据成员为`const`的效果。但是,绑定对象内部的移动构造数据副本不一定是`const`的,因此,为了防止在lambda内修改该数据副本,lambda的参数应声明为`const`引用。 如果将`lambda`声明为可变的,则不会在其闭包类中将`operator()`声明为const,并且在lambda的参数声明中省略`const`也是合适的:
|
||||
默认情况下,从*lambda*生成的闭包类中的`operator()`成员函数为`const`的。这具有在*lambda*主体内把闭包中的所有数据成员渲染为`const`的效果。但是,bind对象内部的移动构造的`data`副本不是`const`的,因此,为了防止在*lambda*内修改该`data`副本,*lambda*的形参应声明为reference-to-`const`。 如果将*lambda*声明为`mutable`,则闭包类中的`operator()`将不会声明为`const`,并且在*lambda*的形参声明中省略`const`也是合适的:
|
||||
|
||||
```c++
|
||||
auto func =
|
||||
std::bind( // C++11 emulation
|
||||
[](std::vector<double>& data) mutable // of init capture
|
||||
{ /* uses of data */ }, // for mutable lambda std::move(data)
|
||||
);
|
||||
std::bind( //C++11对mutable lambda
|
||||
[](std::vector<double>& data) mutable //初始化捕获的模拟
|
||||
{ /*使用data*/ },
|
||||
std::move(data)
|
||||
);
|
||||
```
|
||||
|
||||
因为该绑定对象存储着传递给`std::bind`的所有参数副本,所以在我们的示例中,绑定对象包含由lambda生成的闭包副本,这是它的第一个参数。 因此闭包的生命周期与绑定对象的生命周期相同。 这很重要,因为这意味着只要存在闭包,包含伪移动捕获对象的绑定对象也将存在。
|
||||
因为bind对象存储着传递给`std::bind`的所有实参的副本,所以在我们的示例中,bind对象包含由*lambda*生成的闭包副本,这是它的第一个实参。 因此闭包的生命周期与bind对象的生命周期相同。 这很重要,因为这意味着只要存在闭包,包含伪移动捕获对象的bind对象也将存在。
|
||||
|
||||
如果这是您第一次接触`std::bind`,则可能需要先阅读您最喜欢的C ++11参考资料,然后再进行讨论所有详细信息。 即使是这样,这些基本要点也应该清楚:
|
||||
如果这是你第一次接触`std::bind`,则可能需要先阅读你最喜欢的C++11参考资料,然后再讨论所有详细信息。 即使是这样,这些基本要点也应该清楚:
|
||||
|
||||
* 无法将移动构造一个对象到C ++11闭包,但是可以将对象移动构造为C++11的绑定对象。
|
||||
* 在C++11中模拟移动捕获包括将对象移动构造为绑定对象,然后通过引用将对象移动构造传递给lambda。
|
||||
* 由于绑定对象的生命周期与闭包对象的生命周期相同,因此可以将绑定对象中的对象视为闭包中的对象。
|
||||
* 无法移动构造一个对象到C++11闭包,但是可以将对象移动构造进C++11的bind对象。
|
||||
* 在C++11中模拟移动捕获包括将对象移动构造进bind对象,然后通过传引用将移动构造的对象传递给*lambda*。
|
||||
* 由于bind对象的生命周期与闭包对象的生命周期相同,因此可以将bind对象中的对象视为闭包中的对象。
|
||||
|
||||
作为使用`std::bind`模仿移动捕获的第二个示例,这是我们之前看到的在闭包中创建`std::unique_ptr`的C++14代码:
|
||||
|
||||
```c++
|
||||
auto func = [pw = std::make_unique<Widget>()] // as before,
|
||||
{ return pw->isValidated() // create pw
|
||||
&& pw->isArchived(); }; // in closure
|
||||
auto func = [pw = std::make_unique<Widget>()] //同之前一样
|
||||
{ return pw->isValidated() //在闭包中创建pw
|
||||
&& pw->isArchived(); };
|
||||
```
|
||||
|
||||
这是C++11的模拟实现:
|
||||
|
||||
```c++
|
||||
auto func = std::bind(
|
||||
[](const std::unique_ptr<Widget>& pw)
|
||||
{ return pw->isValidated()
|
||||
&& pw->isArchived(); },
|
||||
std::make_unique<Widget>()
|
||||
);
|
||||
[](const std::unique_ptr<Widget>& pw)
|
||||
{ return pw->isValidated()
|
||||
&& pw->isArchived(); },
|
||||
std::make_unique<Widget>()
|
||||
);
|
||||
```
|
||||
|
||||
具备讽刺意味的是,这里我展示了如何使用`std::bind`解决C++11 lambda中的限制,但在条款34中,我却主张在`std::bind`上使用lambda。
|
||||
具备讽刺意味的是,这里我展示了如何使用`std::bind`解决C++11 *lambda*中的限制,因为在[Item34](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item34.md)中,我主张使用*lambda*而不是`std::bind`。但是,该条款解释的是在C++11中有些情况下`std::bind`可能有用,这就是其中一种。 (在C++14中,初始化捕获和`auto`形参等特性使得这些情况不再存在。)
|
||||
|
||||
但是,该条目解释的是在C++11中有些情况下`std::bind`可能有用,这就是其中一种。 (在C++14中,初始化捕获和自动参数等功能使得这些情况不再存在。)
|
||||
**请记住:**
|
||||
|
||||
**要谨记的是**:
|
||||
|
||||
* 使用C ++14的初始化捕获将对象移动到闭包中。
|
||||
* 在C ++11中,通过手写类或`std::bind`的方式来模拟初始化捕获。
|
||||
* 使用C++14的初始化捕获将对象移动到闭包中。
|
||||
* 在C++11中,通过手写类或`std::bind`的方式来模拟初始化捕获。
|
||||
|
@ -29,7 +29,7 @@ auto f = [](auto&& x)
|
||||
{ return func(normalize(std::forward<???>(x))); };
|
||||
```
|
||||
|
||||
在理论和实际之间存在一个问题:你传递给`std::forward`的参数是什么类型,即确定我在上面写的`???`该是什么。
|
||||
在理论和实际之间存在一个问题:你应该传递给`std::forward`的什么类型,即确定我在上面写的`???`该是什么。
|
||||
|
||||
一般来说,当你在使用完美转发时,你是在一个接受类型参数为`T`的模版函数里,所以你可以写`std::forward<T>`。但在泛型*lambda*中,没有可用的类型参数`T`。在*lambda*生成的闭包里,模版化的`operator()`函数中的确有一个`T`,但在*lambda*里却无法直接使用它,所以也没什么用。
|
||||
|
||||
@ -56,7 +56,7 @@ Widget&& forward(Widget& param) //当T是Widget时的std::forward实
|
||||
}
|
||||
```
|
||||
|
||||
思考一下如果用户代码想要完美转发一个`Widget`类型的右值,但没有遵守规则将`T`指定为非引用类型,而是将`T`指定为右值引用,这会发生什么。也就是,思考将`T`换成`Widget`会如何。在`std::forward`实例化、应用了`std::remove_reference_t`后,引用折叠之前,`std::forward`看起来像这样:
|
||||
思考一下如果用户代码想要完美转发一个`Widget`类型的右值,但没有遵守规则将`T`指定为非引用类型,而是将`T`指定为右值引用,这会发生什么。也就是,思考将`T`换成`Widget&&`会如何。在`std::forward`实例化、应用了`std::remove_reference_t`后,引用折叠之前,`std::forward`看起来像这样:
|
||||
|
||||
```c++
|
||||
Widget&& && forward(Widget& param) //当T是Widget&&时的std::forward实例
|
||||
@ -87,7 +87,7 @@ auto f =
|
||||
};
|
||||
```
|
||||
|
||||
再加上6个点,就可以让我们的*lambda*完美转发接受多个参数了,因为C++14中的*lambda*也可以是可变参数的:
|
||||
再加上6个点,就可以让我们的*lambda*完美转发接受多个形参了,因为C++14中的*lambda*也可以是可变形参的:
|
||||
|
||||
```c++
|
||||
auto f =
|
||||
|
@ -1,202 +1,218 @@
|
||||
# 考虑lambda表达式而非std::bind
|
||||
## 条款三十四:考虑*lambda*而非`std::bind`
|
||||
|
||||
C++11中的`std::bind`是C++98的`std::bind1st`和`std::bind2nd`的后续,但在2005年已经成为了标准库的一部分。那时标准化委员采用了TR1的文档,其中包含了bind的规范。(在TR1中,`bind`位于不同的命名空间,因此它是`std::tr1::bind`,而不是`std::bind`,接口细节也有所不同)。这段历史意味着一些程序员有十年或更长时间的使用`std::bind`经验。如果您是其中之一,可能会不愿意放弃一个对您有用的工具。这是可以理解的,但是在这种情况下,改变是更好的,因为在C ++11中,`lambda`几乎是比`std :: bind`更好的选择。 从C++14开始,`lambda`的作用不仅强大,而且是完全值得使用的。
|
||||
**Item 34: Prefer lambdas to `std::bind`**
|
||||
|
||||
这个条目假设您熟悉`std::bind`。 如果不是这样,您将需要获得基本的了解,然后再继续。 无论如何,这样的理解都是值得的,因为您永远不知道何时会在必须阅读或维护的代码库中遇到`std::bind`的使用。
|
||||
C++11中的`std::bind`是C++98的`std::bind1st`和`std::bind2nd`的后续,但在2005年已经非正式成为了标准库的一部分。那时标准化委员采用了TR1的文档,其中包含了`bind`的规范。(在TR1中,`bind`位于不同的命名空间,因此它是`std::tr1::bind`,而不是`std::bind`,接口细节也有所不同)。这段历史意味着一些程序员有十年及以上的`std::bind`使用经验。如果你是其中之一,可能会不愿意放弃一个对你有用的工具。这是可以理解的,但是在这种情况下,改变是更好的,因为在C++11中,*lambda*几乎总是比`std::bind`更好的选择。 从C++14开始,*lambda*的作用不仅强大,而且是完全值得使用的。
|
||||
|
||||
与第32项中一样,我们将从`std::bind`返回的函数对象称为绑定对象。
|
||||
这个条款假设你熟悉`std::bind`。 如果不是这样,你将需要获得基本的了解,然后再继续。 无论如何,这样的理解都是值得的,因为你永远不知道何时会在阅读或维护的代码库中遇到`std::bind`。
|
||||
|
||||
优先lambda而不是`std::bind`的最重要原因是lambda更易读。 例如,假设我们有一个设置闹钟的函数:
|
||||
与[Item32](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item32.md)中一样,我们将从`std::bind`返回的函数对象称为**bind对象**(*bind objects*)。
|
||||
|
||||
优先*lambda*而不是`std::bind`的最重要原因是*lambda*更易读。 例如,假设我们有一个设置警报器的函数:
|
||||
|
||||
```c++
|
||||
// typedef for a point in time (see Item 9 for syntax)
|
||||
//一个时间点的类型定义(语法见条款9)
|
||||
using Time = std::chrono::steady_clock::time_point;
|
||||
|
||||
// see Item 10 for "enum class"
|
||||
//“enum class”见条款10
|
||||
enum class Sound { Beep, Siren, Whistle };
|
||||
|
||||
// typedef for a length of time
|
||||
//时间段的类型定义
|
||||
using Duration = std::chrono::steady_clock::duration;
|
||||
// at time t, make sound s for duration d void setAlarm(Time t, Sound s, Duration d);
|
||||
|
||||
//在时间t,使用s声音响铃时长d
|
||||
void setAlarm(Time t, Sound s, Duration d);
|
||||
```
|
||||
|
||||
进一步假设,在程序的某个时刻,我们已经确定需要设置一个小时后响30秒的闹钟。 但是,具体声音仍未确定。我们可以编写一个lambda来修改`setAlarm`的界面,以便仅需要指定声音:
|
||||
进一步假设,在程序的某个时刻,我们已经确定需要设置一个小时后响30秒的警报器。 但是,具体声音仍未确定。我们可以编写一个*lambda*来修改`setAlarm`的界面,以便仅需要指定声音:
|
||||
|
||||
```c++
|
||||
// setSoundL ("L" for "lambda") is a function object allowing a // sound to be specified for a 30-sec alarm to go off an hour // after it's set
|
||||
//setSoundL(“L”指代“lambda”)是个函数对象,允许指定一小时后响30秒的警报器的声音
|
||||
auto setSoundL =
|
||||
[](Sound s)
|
||||
{
|
||||
// make std::chrono components available w/o qualification
|
||||
using namespace std::chrono;
|
||||
setAlarm(steady_clock::now() + hours(1), // alarm to go off
|
||||
s, // in an hour for
|
||||
seconds(30)); // 30 seconds
|
||||
};
|
||||
[](Sound s)
|
||||
{
|
||||
//使std::chrono部件在不指定限定的情况下可用
|
||||
using namespace std::chrono;
|
||||
|
||||
setAlarm(steady_clock::now() + hours(1), //一小时后响30秒的闹钟
|
||||
s, //译注:setAlarm三行高亮
|
||||
seconds(30));
|
||||
};
|
||||
```
|
||||
|
||||
我们在lambda中突出了对`setAlarm`的调用。这看来起是一个很正常的函数调用,即使是几乎没有lambda经验的读者也可以看到:传递给lambda的参数被传递给了`setAlarm`。
|
||||
我们在*lambda*中高亮了对`setAlarm`的调用。这看来起是一个很正常的函数调用,即使是几乎没有*lambda*经验的读者也可以看到:传递给*lambda*的形参`s`又作为实参被传递给了`setAlarm`。
|
||||
|
||||
通过使用基于C++11对用户自定义常量的支持而建立的标准后缀,如秒(s),毫秒(ms)和小时(h)等,我们可以简化C++14中的代码。这些后缀在`std::literals`命名空间中实现,因此上述代码可以按照以下方式重写:
|
||||
|
||||
```c++
|
||||
auto setSoundL =
|
||||
[](Sound s)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
using namespace std::literals; // for C++14 suffixes
|
||||
setAlarm(steady_clock::now() + 1h, // C++14, but
|
||||
s, // same meaning
|
||||
30s); // as above
|
||||
};
|
||||
```
|
||||
|
||||
下面是我们第一次编写对应的`std::bind`调用。这里存在一个我们后续会修复的错误,但正确的代码会更加复杂,即使是此简化版本也会带来一些重要问题:
|
||||
|
||||
```c++
|
||||
using namespace std::chrono; // as above
|
||||
using namespace std::literals;
|
||||
using namespace std::placeholders; // needed for use of "_1"
|
||||
auto setSoundB = std::bind(setAlarm, // "B" for "bind"
|
||||
steady_clock::now() + 1h, // incorrect! see below
|
||||
_1,
|
||||
30s);
|
||||
```
|
||||
|
||||
我想像在lambda中一样突出显示对`setAlarm`的调用,但是没有这么做。这段代码的读者只需知道,调用`setSoundB`会使用在对`std :: bind`的调用中所指定的时间和持续时间来调用`setAlarm`。对于初学者来说,占位符**“ _1”**本质上是一个魔术,但即使是普通读者也必须从思维上将占位符中的数字映射到其在`std::bind`参数列表中的位置,以便明白调用`setSoundB`时的第一个参数会被传递进`setAlarm`,作为调用时的第二个参数。在对`std::bind`的调用中未标识此参数的类型,因此读者必须查阅`setAlarm`声明以确定将哪种参数传递给`setSoundB`。
|
||||
|
||||
但正如我所说,代码并不完全正确。在lambda中,表达式`steady_clock::now() + 1h`显然是是`setAlarm`的参数。调用`setAlarm`时将对其进行计算。这是合理的:我们希望在调用`setAlarm`后一小时发出警报。但是,在`std::bind`调用中,将`steady_clock::now() + 1h`作为参数传递给了`std::bind,而不是`setAlarm`。这意味着将在调用`std::bind`时对表达式进行求值,并且该表达式产生的时间将存储在结果绑定对象中。结果,闹钟将被设置为在调用`std::bind`后一小时发出声音,而不是在调用`setAlarm`一小时后发出。
|
||||
|
||||
要解决此问题,需要告诉`std::bind`推迟对表达式的求值,直到调用`setAlarm`为止,而这样做的方法是将对`std::bind`的第二个调用嵌套在第一个调用中:
|
||||
|
||||
```c++
|
||||
auto setSoundB =
|
||||
std::bind(setAlarm,
|
||||
std::bind(std::plus<>(), steady_clock::now(), 1h), _1,
|
||||
30s);
|
||||
```
|
||||
|
||||
如果您熟悉C++98的`std::plus`模板,您可能会惊讶地发现在此代码中,尖括号之间未指定任何类型,即该代码包含`std::plus<>`,而不是`std::plus<type>`。 在C ++14中,通常可以省略标准运算符模板的模板类型参数,因此无需在此处提供。 C++11没有提供此类功能,因此等效于lambda的C ++11 `std::bind`使用为:
|
||||
|
||||
```c++
|
||||
using namespace std::chrono; // as above
|
||||
using namespace std::placeholders;
|
||||
auto setSoundB =
|
||||
std::bind(setAlarm,
|
||||
std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)),
|
||||
seconds(30));
|
||||
```
|
||||
|
||||
如果此时Lambda看起来不够吸引,那么应该检查一下视力了。
|
||||
|
||||
当setAlarm重载时,会出现一个新问题。 假设有一个重载函数,其中第四个参数指定了音量:
|
||||
|
||||
```c++
|
||||
enum class Volume { Normal, Loud, LoudPlusPlus };
|
||||
void setAlarm(Time t, Sound s, Duration d, Volume v);
|
||||
```
|
||||
|
||||
lambda能继续像以前一样使用,因为根据重载规则选择了`setAlarm`的三参数版本:
|
||||
我们通过使用标准后缀如秒(`s`),毫秒(`ms`)和小时(`h`)等简化在C++14中的代码,其中标准后缀基于C++11对用户自定义常量的支持。这些后缀在`std::literals`命名空间中实现,因此上述代码可以按照以下方式重写:
|
||||
|
||||
```c++
|
||||
auto setSoundL =
|
||||
[](Sound s)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
setAlarm(steady_clock::now() + 1h, s,
|
||||
30s);
|
||||
};
|
||||
using namespace std::chrono;
|
||||
using namespace std::literals; //对于C++14后缀
|
||||
|
||||
setAlarm(steady_clock::now() + 1h, //C++14写法,但是含义同上
|
||||
s,
|
||||
30s);
|
||||
};
|
||||
```
|
||||
|
||||
下面是我们第一次编写对应的`std::bind`调用。这里存在一个我们后续会修复的错误,但正确的代码会更加复杂,即使是此简化版本也会凸显一些重要问题:
|
||||
|
||||
```c++
|
||||
using namespace std::chrono; //同上
|
||||
using namespace std::literals;
|
||||
using namespace std::placeholders; //“_1”使用需要
|
||||
|
||||
auto setSoundB = //“B”代表“bind”
|
||||
std::bind(setAlarm,
|
||||
steady_clock::now() + 1h, //不正确!见下
|
||||
_1,
|
||||
30s);
|
||||
```
|
||||
|
||||
我想像在之前的*lambda*中一样高亮对`setAlarm`的调用,但是没这么个调用让我高亮。这段代码的读者只需知道,调用`setSoundB`会使用在对`std::bind`的调用中所指定的时间和持续时间来调用`setAlarm`。对于门外汉来说,占位符“`_1`”完全是一个魔法,但即使是知情的读者也必须从思维上将占位符中的数字映射到其在`std::bind`形参列表中的位置,以便明白调用`setSoundB`时的第一个实参会被传递进`setAlarm`,作为调用`setAlarm`的第二个实参。在对`std::bind`的调用中未标识此实参的类型,因此读者必须查阅`setAlarm`声明以确定将哪种实参传递给`setSoundB`。
|
||||
|
||||
但正如我所说,代码并不完全正确。在*lambda*中,表达式`steady_clock::now() + 1h`显然是`setAlarm`的实参。调用`setAlarm`时将对其进行计算。可以理解:我们希望在调用`setAlarm`后一小时响铃。但是,在`std::bind`调用中,将`steady_clock::now() + 1h`作为实参传递给了`std::bind`,而不是`setAlarm`。这意味着将在调用`std::bind`时对表达式进行求值,并且该表达式产生的时间将存储在产生的bind对象中。结果,警报器将被设置为在**调用`std::bind`后一小时**发出声音,而不是在调用`setAlarm`一小时后发出。
|
||||
|
||||
要解决此问题,需要告诉`std::bind`推迟对表达式的求值,直到调用`setAlarm`为止,而这样做的方法是将对`std::bind`的第二个调用嵌套在第一个调用中:
|
||||
|
||||
```c++
|
||||
auto setSoundB =
|
||||
std::bind(setAlarm,
|
||||
std::bind(std::plus<>(), steady_clock::now(), 1h),
|
||||
_1,
|
||||
30s);
|
||||
```
|
||||
|
||||
如果你熟悉C++98的`std::plus`模板,你可能会惊讶地发现在此代码中,尖括号之间未指定任何类型,即该代码包含“`std::plus<>`”,而不是“`std::plus<type>`”。 在C++14中,通常可以省略标准运算符模板的模板类型实参,因此无需在此处提供。 C++11没有提供此类功能,因此等效于*lambda*的C++11 `std::bind`为:
|
||||
|
||||
```c++
|
||||
using namespace std::chrono; //同上
|
||||
using namespace std::placeholders;
|
||||
auto setSoundB =
|
||||
std::bind(setAlarm,
|
||||
std::bind(std::plus<steady_clock::time_point>(),
|
||||
steady_clock::now(),
|
||||
hours(1)),
|
||||
_1,
|
||||
seconds(30));
|
||||
```
|
||||
|
||||
如果此时*lambda*看起来还没有吸引力,那么应该检查一下视力了。
|
||||
|
||||
当`setAlarm`重载时,会出现一个新问题。 假设有一个重载函数,其中第四个形参了音量:
|
||||
|
||||
```c++
|
||||
enum class Volume { Normal, Loud, LoudPlusPlus };
|
||||
|
||||
void setAlarm(Time t, Sound s, Duration d, Volume v);
|
||||
```
|
||||
|
||||
*lambda*能继续像以前一样使用,因为根据重载规则选择了`setAlarm`的三实参版本:
|
||||
|
||||
```c++
|
||||
auto setSoundL = //和之前一样
|
||||
[](Sound s)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
setAlarm(steady_clock::now() + 1h, //可以,调用三实参版本的setAlarm
|
||||
s,
|
||||
30s);
|
||||
};
|
||||
```
|
||||
|
||||
然而,`std::bind`的调用将会编译失败:
|
||||
|
||||
```c++
|
||||
auto setSoundB = // error! which
|
||||
std::bind(setAlarm, // setAlarm?
|
||||
std::bind(std::plus<>(),
|
||||
steady_clock::now(),
|
||||
1h),
|
||||
_1,
|
||||
30s);
|
||||
auto setSoundB = //错误!哪个setAlarm?
|
||||
std::bind(setAlarm,
|
||||
std::bind(std::plus<>(),
|
||||
steady_clock::now(),
|
||||
1h),
|
||||
_1,
|
||||
30s);
|
||||
```
|
||||
|
||||
这里的问题是,编译器无法确定应将两个setAlarm函数中的哪一个传递给`std::bind`。 它们仅有的是一个函数名称,而这个函数名称是不确定的。
|
||||
要获得对`std::bind`的调用能进行编译,必须将`setAlarm`强制转换为适当的函数指针类型:
|
||||
这里的问题是,编译器无法确定应将两个`setAlarm`函数中的哪一个传递给`std::bind`。 它们仅有的是一个函数名称,而这个单一个函数名称是有歧义的。
|
||||
|
||||
要使对`std::bind`的调用能编译,必须将`setAlarm`强制转换为适当的函数指针类型:
|
||||
|
||||
```c++
|
||||
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
|
||||
auto setSoundB = // now
|
||||
std::bind(static_cast<SetAlarm3ParamType>(setAlarm), // okay
|
||||
std::bind(std::plus<>(),
|
||||
steady_clock::now(),
|
||||
1h),
|
||||
_1,
|
||||
30s);
|
||||
|
||||
auto setSoundB = //现在可以了
|
||||
std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
|
||||
std::bind(std::plus<>(),
|
||||
steady_clock::now(),
|
||||
1h),
|
||||
_1,
|
||||
30s);
|
||||
```
|
||||
|
||||
但这在`lambda`和`std::bind`的使用上带来了另一个区别。 在`setSoundL`的函数调用操作符(即lambda的闭包类对应的函数调用操作符)内部,对`setAlarm`的调用是正常的函数调用,编译器可以按常规方式进行内联:
|
||||
但这在*lambda*和`std::bind`的使用上带来了另一个区别。 在`setSoundL`的函数调用操作符(即*lambda*的闭包类对应的函数调用操作符)内部,对`setAlarm`的调用是正常的函数调用,编译器可以按常规方式进行内联:
|
||||
|
||||
```c++
|
||||
setSoundL(Sound::Siren); // body of setAlarm may
|
||||
// well be inlined here
|
||||
setSoundL(Sound::Siren); //setAlarm函数体在这可以很好地内联
|
||||
```
|
||||
|
||||
但是,对`std::bind`的调用是将函数指针传递给`setAlarm`,这意味着在`setSoundB`的函数调用操作符(即绑定对象的函数调用操作符)内部,对`setAlarm`的调用是通过一个函数指针。 编译器不太可能通过函数指针内联函数,这意味着与通过`setSoundL`进行调用相比,通过`setSoundB`对`setAlarm的`调用,其函数不大可能被内联:
|
||||
|
||||
```c++
|
||||
setSoundB(Sound::Siren); // body of setAlarm is less
|
||||
// likely to be inlined here
|
||||
setSoundB(Sound::Siren); //setAlarm函数体在这不太可能内联
|
||||
```
|
||||
|
||||
因此,使用`lambda`可能会比使用`std::bind`能生成更快的代码。
|
||||
`setAlarm`示例仅涉及一个简单的函数调用。如果您想做更复杂的事情,使用lambda会更有利。 例如,考虑以下C++14的lambda使用,它返回其参数是否在最小值(`lowVal`)和最大值(`highVal`)之间的结果,其中`lowVal`和`highVal` 是局部变量:
|
||||
因此,使用*lambda*可能会比使用`std::bind`能生成更快的代码。
|
||||
|
||||
`setAlarm`示例仅涉及一个简单的函数调用。如果你想做更复杂的事情,使用*lambda*会更有利。 例如,考虑以下C++14的*lambda*使用,它返回其实参是否在最小值(`lowVal`)和最大值(`highVal`)之间的结果,其中`lowVal`和`highVal`是局部变量:
|
||||
|
||||
```c++
|
||||
auto betweenL =
|
||||
[lowVal, highVal]
|
||||
(const auto& val) // C++14
|
||||
{ return lowVal <= val && val <= highVal; };
|
||||
[lowVal, highVal]
|
||||
(const auto& val) //C++14
|
||||
{ return lowVal <= val && val <= highVal; };
|
||||
```
|
||||
|
||||
使用`std::bind`可以表达相同的内容,但是该构造是一个通过晦涩难懂的代码来保证工作安全性的示例:
|
||||
|
||||
```c++
|
||||
using namespace std::placeholders; // as above
|
||||
using namespace std::placeholders; //同上
|
||||
auto betweenB =
|
||||
std::bind(std::logical_and<>(), // C++14
|
||||
std::bind(std::less_equal<>(), lowVal, _1),
|
||||
std::bind(std::less_equal<>(), _1, highVal));
|
||||
std::bind(std::logical_and<>(), //C++14
|
||||
std::bind(std::less_equal<>(), lowVal, _1),
|
||||
std::bind(std::less_equal<>(), _1, highVal));
|
||||
```
|
||||
|
||||
在C++11中,我们必须指定要比较的类型,然后`std::bind`调用将如下所示:
|
||||
|
||||
```c++
|
||||
auto betweenB = // C++11 version
|
||||
std::bind(std::logical_and<bool>(),
|
||||
std::bind(std::less_equal<int>(), lowVal, _1),
|
||||
std::bind(std::less_equal<int>(), _1, highVal));
|
||||
auto betweenB =
|
||||
std::bind(std::logical_and<bool>(), //C++11版本
|
||||
std::bind(std::less_equal<int>(), lowVal, _1),
|
||||
std::bind(std::less_equal<int>(), _1, highVal));
|
||||
```
|
||||
|
||||
当然,在C++11中,lambda也不能采用`auto`参数,因此它也必须指定一个类型:
|
||||
当然,在C++11中,*lambda*也不能采用`auto`形参,因此它也必须指定一个类型:
|
||||
|
||||
```c++
|
||||
auto betweenL = // C++11 version
|
||||
[lowVal, highVal]
|
||||
(int val)
|
||||
{ return lowVal <= val && val <= highVal; };
|
||||
auto betweenL = //C++11版本
|
||||
[lowVal, highVal]
|
||||
(int val)
|
||||
{ return lowVal <= val && val <= highVal; };
|
||||
```
|
||||
|
||||
无论哪种方式,我希望我们都能同意,lambda版本不仅更短,而且更易于理解和维护。
|
||||
之前我就说过,对于那些没有`std::bind`使用经验的人,其占位符(例如\_1,\_2等)本质上都是magic。 但是,不仅仅占位符的行为是不透明的。 假设我们有一个函数可以创建Widget的压缩副本,
|
||||
无论哪种方式,我希望我们都能同意,*lambda*版本不仅更短,而且更易于理解和维护。
|
||||
|
||||
之前我就说过,对于那些没有`std::bind`使用经验的人,其占位符(例如`_1`,`_2`等)都是魔法。 但是这不仅仅在于占位符的行为是不透明的。 假设我们有一个函数可以创建`Widget`的压缩副本,
|
||||
|
||||
```c++
|
||||
enum class CompLevel { Low, Normal, High }; // compression
|
||||
// level
|
||||
Widget compress(const Widget& w, // make compressed
|
||||
CompLevel lev); // copy of w
|
||||
enum class CompLevel { Low, Normal, High }; //压缩等级
|
||||
|
||||
Widget compress(const Widget& w, //制作w的压缩副本
|
||||
CompLevel lev);
|
||||
```
|
||||
|
||||
并且我们想创建一个函数对象,该函数对象允许我们指定应将特定`w`的压缩级别。这种使用`std::bind`的话将创建一个这样的对象:
|
||||
并且我们想创建一个函数对象,该函数对象允许我们指定`Widget w`的压缩级别。这种使用`std::bind`的话将创建一个这样的对象:
|
||||
|
||||
```c++
|
||||
Widget w;
|
||||
@ -204,41 +220,41 @@ using namespace std::placeholders;
|
||||
auto compressRateB = std::bind(compress, w, _1);
|
||||
```
|
||||
|
||||
现在,当我们将`w`传递给`std::bind`时,必须将其存储起来,以便以后进行压缩。它存储在对象compressRateB中,但是这是如何存储的呢(是通过值还是引用)。之所以会有所不同,是因为如果在对`std::bind`的调用与对`compressRateB`的调用之间修改了`w`,则按引用捕获的`w`将反映其更改,而按值捕获则不会。
|
||||
现在,当我们将`w`传递给`std::bind`时,必须将其存储起来,以便以后进行压缩。它存储在对象`compressRateB`中,但是它是如何被存储的呢——是通过值还是引用?之所以会有所不同,是因为如果在对`std::bind`的调用与对`compressRateB`的调用之间修改了`w`,则按引用捕获的`w`将反映这个更改,而按值捕获则不会。
|
||||
|
||||
答案是它是按值捕获的,但唯一知道的方法是记住`std::bind`的工作方式;在对`std::bind`的调用中没有任何迹象。与lambda方法相反,其中`w`是通过值还是通过引用捕获是显式的:
|
||||
答案是它是按值捕获的(`std::bind`总是拷贝它的实参,但是调用者可以使用引用来存储实参,这要通过应用`std::ref`到实参上实现。`auto compressRateB = std::bind(compress, std::ref(w), _1);`的结果就是`compressRateB`行为像是持有`w`的引用而非副本。),但唯一知道的方法是记住`std::bind`的工作方式;在对`std::bind`的调用中没有任何迹象。然而在*lambda*方法中,其中`w`是通过值还是通过引用捕获是显式的:
|
||||
|
||||
```c++
|
||||
auto compressRateL = // w is captured by
|
||||
[w](CompLevel lev) // value; lev is
|
||||
{ return compress(w, lev); }; // passed by value
|
||||
auto compressRateL = //w是按值捕获,lev是按值传递
|
||||
[w](CompLevel lev)
|
||||
{ return compress(w, lev); };
|
||||
```
|
||||
|
||||
同样明确的是如何将参数传递给lambda。 在这里,很明显参数`lev`是通过值传递的。 因此:
|
||||
同样明确的是形参是如何传递给*lambda*的。 在这里,很明显形参`lev`是通过值传递的。 因此:
|
||||
|
||||
```c++
|
||||
compressRateL(CompLevel::High); // arg is passed
|
||||
// by value
|
||||
compressRateL(CompLevel::High); //实参按值传递
|
||||
```
|
||||
|
||||
但是在对由`std::bind`生成的对象调用中,参数如何传递?
|
||||
但是在对由`std::bind`生成的对象调用中,实参如何传递?
|
||||
|
||||
```c++
|
||||
compressRateB(CompLevel::High); // how is arg
|
||||
// passed?
|
||||
compressRateB(CompLevel::High); //实参如何传递?
|
||||
```
|
||||
|
||||
同样,唯一的方法是记住`std::bind`的工作方式。(答案是传递给绑定对象的所有参数都是通过引用传递的,因为此类对象的函数调用运算符使用完美转发。)
|
||||
与lambda相比,使用`std::bind`进行编码的代码可读性较低,表达能力较低,并且效率可能较低。 在C++14中,没有`std::bind`的合理用例。 但是,在C ++11中,可以在两个受约束的情况下证明使用`std::bind`是合理的:
|
||||
同样,唯一的方法是记住`std::bind`的工作方式。(答案是传递给bind对象的所有实参都是通过引用传递的,因为此类对象的函数调用运算符使用完美转发。)
|
||||
|
||||
* 移动捕获。 C++11的lambda不提供移动捕获,但是可以通过结合lambda和`std::bind`来模拟。 有关详细信息,请参阅条款32,该条款还解释了在C ++ 14中,lambda对初始化捕获的支持将少了模拟的需求。
|
||||
* 多态函数对象。 因为绑定对象上的函数调用运算符使用完全转发,所以它可以接受任何类型的参数(以条款30中描述的完全转发的限制为例子)。当您要使用模板化函数调用运算符来绑定对象时,此功能很有用。 例如这个类,
|
||||
与*lambda*相比,使用`std::bind`进行编码的代码可读性较低,表达能力较低,并且效率可能较低。 在C++14中,没有`std::bind`的合理用例。 但是,在C++11中,可以在两个受约束的情况下证明使用`std::bind`是合理的:
|
||||
|
||||
+ **移动捕获**。C++11的*lambda*不提供移动捕获,但是可以通过结合*lambda*和`std::bind`来模拟。 有关详细信息,请参阅[Item32](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.LambdaExpressions/item32.md),该条款还解释了在C++14中,*lambda*对初始化捕获的支持消除了这个模拟的需求。
|
||||
+ **多态函数对象**。因为bind对象上的函数调用运算符使用完美转发,所以它可以接受任何类型的实参(以[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)中描述的完美转发的限制为界限)。当你要绑定带有模板化函数调用运算符的对象时,此功能很有用。 例如这个类,
|
||||
|
||||
```c++
|
||||
class PolyWidget {
|
||||
public:
|
||||
public:
|
||||
template<typename T>
|
||||
void operator()(const T& param); ...
|
||||
void operator()(const T& param);
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
@ -252,26 +268,23 @@ auto boundPW = std::bind(pw, _1);
|
||||
`boundPW`可以接受任意类型的对象了:
|
||||
|
||||
```c++
|
||||
boundPW(1930); // pass int to
|
||||
// PolyWidget::operator()
|
||||
boundPW(nullptr); // pass nullptr to
|
||||
// PolyWidget::operator()
|
||||
boundPW("Rosebud"); // pass string literal to
|
||||
// PolyWidget::operator()
|
||||
boundPW(1930); //传int给PolyWidget::operator()
|
||||
boundPW(nullptr); //传nullptr给PolyWidget::operator()
|
||||
boundPW("Rosebud"); //传字面值给PolyWidget::operator()
|
||||
```
|
||||
|
||||
这一点无法使用C++11的lambda做到。 但是,在C++14中,可以通过带有`auto`参数的lambda轻松实现:
|
||||
这一点无法使用C++11的*lambda*做到。 但是,在C++14中,可以通过带有`auto`形参的*lambda*轻松实现:
|
||||
|
||||
```c++
|
||||
auto boundPW = [pw](const auto& param) // C++14
|
||||
{ pw(param); };
|
||||
auto boundPW = [pw](const auto& param) //C++14
|
||||
{ pw(param); };
|
||||
```
|
||||
|
||||
当然,这些是特殊情况,并且是暂时的特殊情况,因为支持C++14 lambda的编译器越来越普遍了。
|
||||
当`bind`在2005年被非正式地添加到C ++中时,与1998年的前身相比有了很大的改进。 在C ++11中增加了lambda支持,这使得`std::bind`几乎已经过时了,从C ++ 14开始,更是没有很好的用例了。
|
||||
当然,这些是特殊情况,并且是暂时的特殊情况,因为支持C++14 *lambda*的编译器越来越普遍了。
|
||||
|
||||
**要谨记的是**:
|
||||
当`bind`在2005年被非正式地添加到C++中时,与1998年的前身相比有了很大的改进。 在C++11中增加了*lambda*支持,这使得`std::bind`几乎已经过时了,从C++14开始,更是没有很好的用例了。
|
||||
|
||||
* 与使用`std::bind`相比,Lambda更易读,更具表达力并且可能更高效。
|
||||
* 只有在C++11中,`std::bind`可能对实现移动捕获或使用模板化函数调用运算符来绑定对象时会很有用。
|
||||
**请记住:**
|
||||
|
||||
+ 与使用`std::bind`相比,*lambda*更易读,更具表达力并且可能更高效。
|
||||
+ 只有在C++11中,`std::bind`可能对实现移动捕获或绑定带有模板化函数调用运算符的对象时会很有用。
|
||||
|
@ -1,72 +1,74 @@
|
||||
C++11的伟大标志之一是将并发整合到语言和库中。熟悉其他线程API(比如pthreads或者Windows threads)的开发者有时可能会对C++提供的斯巴达式(译者注:应该是简陋和严谨的意思)功能集感到惊讶,这是因为C++对于并发的大量支持是在编译器的约束层面。由此产生的语言保证意味着在C++的历史中,开发者首次通过标准库可以写出跨平台的多线程程序。这位构建表达库奠定了坚实的基础,并发标准库(tasks, futures, threads, mutexes, condition variables, atomic objects等)仅仅是成为并发软件开发者丰富工具集的基础。
|
||||
# 第7章 并发API
|
||||
|
||||
在接下来的Item中,记住标准库有两个futures的模板:`std::future` 和 `std::shared_future`。在许多情况下,区别不重要,所以我们经常简单的混于一谈为*futures*。
|
||||
**CHAPTER 7 The Concurrency API**
|
||||
|
||||
# 优先基于任务编程而不是基于线程
|
||||
C++11的伟大成功之一是将并发整合到语言和库中。熟悉其他线程API(比如pthreads或者Windows threads)的开发者有时可能会对C++提供的斯巴达式(译者注:应该是简陋和严谨的意思)功能集感到惊讶,这是因为C++对于并发的大量支持是在对编译器作者约束的层面。由此产生的语言保证意味着在C++的历史中,开发者首次通过标准库可以写出跨平台的多线程程序。这为构建表达库奠定了坚实的基础,标准库并发组件(任务*tasks*,期望*futures*,线程*threads*,互斥*mutexes*,条件变量*condition variables*,原子对象*atomic objects*等)仅仅是成为并发软件开发者丰富工具集的基础。
|
||||
|
||||
在接下来的条款中,记住标准库有两个*future*的模板:`std::future`和`std::shared_future`。在许多情况下,区别不重要,所以我们经常简单的混于一谈为*futures*。
|
||||
|
||||
## 条款三十五:优先考虑基于任务的编程而非基于线程的编程
|
||||
|
||||
**Item 35: Prefer task-based programming to thread-based**
|
||||
|
||||
如果开发者想要异步执行`doAsyncWork`函数,通常有两种方式。其一是通过创建`std::thread`执行`doAsyncWork`,这是应用了**基于线程**(*thread-based*)的方式:
|
||||
|
||||
如果开发者想要异步执行 `doAsyncWork` 函数,通常有两种方式。其一是通过创建 `std::thread` 执行 `doAsyncWork`, 比如
|
||||
```cpp
|
||||
int doAsyncWork();
|
||||
std::thread t(doAsyncWork);
|
||||
```
|
||||
其二是将 `doAsyncWork` 传递给 `std::async`, 一种基于任务的策略:
|
||||
其二是将`doAsyncWork`传递给`std::async`,一种**基于任务**(*task-based*)的策略:
|
||||
```cpp
|
||||
auto fut = std::async(doAsyncWork); // "fut" for "future"
|
||||
auto fut = std::async(doAsyncWork); //“fut”表示“future”
|
||||
```
|
||||
这种方式中,函数对象作为一个任务传递给 `std::async`。
|
||||
这种方式中,传递给`std::async`的函数对象被称为一个**任务**(*task*)。
|
||||
|
||||
基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设唤醒 `doAsyncWork` 的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法可以简单地获取`std::async`返回的`future`提供的`get`函数获取这个返回值。如果`doAsycnWork`发生了异常,`get`函数就显得更为重要,因为`get`函数可以提供抛出异常的访问,而基于线程的方法,如果`doAsyncWork`抛出了异常,线程会直接终止(通过调用`std::terminate`)。
|
||||
基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设调用`doAsyncWork`的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法就简单了,因为`std::async`返回的*future*提供了`get`函数(从而可以获取返回值)。如果`doAsycnWork`发生了异常,`get`函数就显得更为重要,因为`get`函数可以提供抛出异常的访问,而基于线程的方法,如果`doAsyncWork`抛出了异常,程序会直接终止(通过调用`std::terminate`)。
|
||||
|
||||
基于线程与基于任务最根本的区别在于抽象层次的高低。基于任务的方式使得开发者从线程管理的细节中解放出来,对此在C++并发软件中总结了'thread'的三种含义:
|
||||
基于线程与基于任务最根本的区别在于,基于任务的抽象层次更高。基于任务的方式使得开发者从线程管理的细节中解放出来,对此在C++并发软件中总结了“*thread*”的三种含义:
|
||||
|
||||
- 硬件线程(Hardware threads)是真实执行计算的线程。现代计算机体系结构为每个CPU核心提供一个或者多个硬件线程。
|
||||
- 软件线程(Software threads)(也被称为系统线程)是操作系统管理的在硬件线程上执行的线程。通常可以存在比硬件线程更多数量的软件线程,因为当软件线程被比如 I/O、同步锁或者条件变量阻塞的时候,操作系统可以调度其他未阻塞的软件线程执行提供吞吐量。
|
||||
- `std::thread` 是C++执行过程的对象,并作为软件线程的句柄 (handle)。`std::thread` 存在多种状态,1. `null`表示空句柄,因为处于默认构造状态(即没有函数来执行),因此不对应任何软件线程。 2. moved from (moved-to的`std::thread` 就对应软件进程开始执行) 3. `joined`(连接唤醒与被唤醒的两个线程) 4. `detached`(将两个连接的线程分离)
|
||||
- **硬件线程**(hardware threads)是真实执行计算的线程。现代计算机体系结构为每个CPU核心提供一个或者多个硬件线程。
|
||||
- **软件线程**(software threads)(也被称为系统线程(OS threads、system threads))是操作系统(假设有一个操作系统。有些嵌入式系统没有。)管理的在硬件线程上执行的线程。通常可以存在比硬件线程更多数量的软件线程,因为当软件线程被阻塞的时候(比如 I/O、同步锁或者条件变量),操作系统可以调度其他未阻塞的软件线程执行提供吞吐量。
|
||||
- **`std::thread`** 是C++执行过程的对象,并作为软件线程的句柄(*handle*)。有些`std::thread`对象代表“空”句柄,即没有对应软件线程,因为它们处在默认构造状态(即没有函数要执行);有些被移动走(移动到的`std::thread`就作为这个软件线程的句柄);有些被`join`(它们要运行的函数已经运行完);有些被`detach`(它们和对应的软件线程之间的连接关系被打断)。
|
||||
|
||||
软件线程是有限的资源。如果开发者试图创建大于系统支持的硬件线程数量,会抛出`std::system_error`异常。即使你编写了不抛出异常的代码,这仍然会发生,比如下面的代码,即使 `doAsyncWork`是 `noexcept`
|
||||
软件线程是有限的资源。如果开发者试图创建大于系统支持的线程数量,会抛出`std::system_error`异常。即使你编写了不抛出异常的代码,这仍然会发生,比如下面的代码,即使 `doAsyncWork`是 `noexcept`,
|
||||
```cpp
|
||||
int doAsyncWork() noexcept; // see Item 14 for noexcept
|
||||
int doAsyncWork() noexcept; //noexcept见条款14
|
||||
```
|
||||
这段代码仍然会抛出异常。
|
||||
这段代码仍然会抛出异常:
|
||||
```cpp
|
||||
std::thread t(doAsyncWork); // throw if no more
|
||||
// threads are available
|
||||
std::thread t(doAsyncWork); //如果没有更多线程可用,则抛出异常
|
||||
```
|
||||
|
||||
设计良好的软件必须有效地处理这种可能性(软件线程资源耗尽),一种有效的方法是在当前线程执行`doAsyncWork`,但是这可能会导致负载不均,而且如果当前线程是GUI线程,可能会导致响应时间过长的问题;另一种方法是等待当前运行的线程结束之后再创建新的线程,但是仍然有可能当前运行的线程在等待`doAsyncWork`的结果(例如操作得到的变量或者条件变量的通知)。
|
||||
设计良好的软件必须能有效地处理这种可能性,但是怎样做?一种方法是在当前线程执行`doAsyncWork`,但是这可能会导致负载不均,而且如果当前线程是GUI线程,可能会导致响应时间过长的问题。另一种方法是等待某些当前运行的软件线程结束之后再创建新的`std::thread`,但是仍然有可能当前运行的线程在等待`doAsyncWork`的动作(例如产生一个结果或者报告一个条件变量)。
|
||||
|
||||
即使没有超出软件线程的限额,仍然可能会遇到资源超额的麻烦。如果当前准备运行的软件线程大于硬件线程的数量,系统的线程调度程序会将硬件核心的时间切片,当一个软件线程的时间片执行结束,会让给另一个软件线程,即发生上下文切换。软件线程的上下文切换会增加系统的软件线程管理开销,并且如果发生了硬件核心漂移,这个开销会更高,具体来说,如果发生了硬件核心漂移,(1)CPU cache中关于上次执行线程的数据很少,需要重新加载指令;(2)新线程的cache数据会覆盖老线程的数据,如果将来会再次覆盖老线程的数据,显然频繁覆盖增加很多切换开销。
|
||||
即使没有超出软件线程的限额,仍然可能会遇到**资源超额**(*oversubscription*)的麻烦。这是一种当前准备运行的(即未阻塞的)软件线程大于硬件线程的数量的情况。情况发生时,线程调度器(操作系统的典型部分)会将软件线程时间切片,分配到硬件上。当一个软件线程的时间片执行结束,会让给另一个软件线程,此时发生上下文切换。软件线程的上下文切换会增加系统的软件线程管理开销,当软件线程安排到与上次时间片运行时不同的硬件线程上,这个开销会更高。这种情况下,(1)CPU缓存对这个软件线程很冷淡(即几乎没有什么数据,也没有有用的操作指南);(2)“新”软件线程的缓存数据会“污染”“旧”线程的数据,旧线程之前运行在这个核心上,而且还有可能再次在这里运行。
|
||||
|
||||
避免资源超额是困难的,因为软件线程之于硬件线程的最佳比例取决于软件线程的执行频率,(比如一个程序从IO密集型变成计算密集型,执行频率是会改变的),而且比例还依赖上下文切换的开销以及软件线程对于CPU cache的使用效率。此外,硬件线程的数量和CPU cache的速度取决于机器的体系结构,即使经过调校,软件比例在某一种机器平台取得较好效果,换一个其他类型的机器这个调校并不能提供较好效果的保证。
|
||||
避免资源超额很困难,因为软件线程之于硬件线程的最佳比例取决于软件线程的执行频率,那是动态改变的,比如一个程序从IO密集型变成计算密集型,执行频率是会改变的。而且比例还依赖上下文切换的开销以及软件线程对于CPU缓存的使用效率。此外,硬件线程的数量和CPU缓存的细节(比如缓存多大,相应速度多少)取决于机器的体系结构,即使经过调校,在某一种机器平台避免了资源超额(而仍然保持硬件的繁忙状态),换一个其他类型的机器这个调校并不能提供较好效果的保证。
|
||||
|
||||
而使用`std::async`可以将调校最优比例这件事隐藏于标准库中,在应用层面不需过多考虑
|
||||
如果你把这些问题推给另一个人做,你就会变得很轻松,而使用`std::async`就做了这件事:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(doAsyncWork); // onus of thread mgmt is
|
||||
// on implement of
|
||||
// the Standard Library
|
||||
auto fut = std::async(doAsyncWork); //线程管理责任交给了标准库的开发者
|
||||
```
|
||||
|
||||
这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额的异常,为何这么说? 调用 `std::async` 并不保证开启一个新的线程,只是提供了执行函数的保证,具体是否创建新的线程来运行此函数,取决于具体实现,比如可以通过调度程序来将`AsyncWork`运行在等待此函数结果的线程上,调度程序的合理性决定了系统是否会抛出资源超额的异常,但是这是库开发者需要考虑的事情了。
|
||||
这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额异常的可能性,因为这个调用可能不会开启一个新的线程。你会想:“怎么可能?如果我要求比系统可以提供的更多的软件线程,创建`std::thread`和调用`std::async`为什么会有区别?”确实有区别,因为以这种形式调用(即使用默认启动策略——见[Item36](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item36.md))时,`std::async`不保证会创建新的软件线程。然而,他们允许通过调度器来将特定函数(本例中为`doAsyncWork`)运行在等待此函数结果的线程上(即在对`fut`调用`get`或者`wait`的线程上),合理的调度器在系统资源超额或者线程耗尽时就会利用这个自由度。
|
||||
|
||||
如果考虑自己实现在等待结果的线程上运行输出结果的函数,之前提到了可能引出负载不均衡的问题,`std::async`运行时的调度程序显然比开发者更清楚调度策略的制定,因为运行时调度程序管理的是所有执行过程,而不仅仅个别开发者运行的代码。
|
||||
如果考虑自己实现“在等待结果的线程上运行输出结果的函数”,之前提到了可能引出负载不均衡的问题,这问题不那么容易解决,因为应该是`std::async`和运行时的调度程序来解决这个问题而不是你。遇到负载不均衡问题时,对机器内发生的事情,运行时调度程序比你有更全面的了解,因为它管理的是所有执行过程,而不仅仅个别开发者运行的代码。
|
||||
|
||||
如果在GUI程序中使用`std::async`会引起响应变慢的问题,还可以通过`std::launch::async`向`std::async`传递调度策略来保证运行函数在不同的线程上执行。
|
||||
有了`std::async`,GUI线程中响应变慢仍然是个问题,因为调度器并不知道你的哪个线程有高响应要求。这种情况下,你会想通过向`std::async`传递`std::launch::async`启动策略来保证想运行函数在不同的线程上执行(见[Item36](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item36.md))。
|
||||
|
||||
最前沿的线程调度算法使用线程池来避免资源超额的问题,并且通过窃取算法来提升了跨硬件核心的负载均衡。C++标准实际上并不要求使用线程池或者`work-stealing`算法,而且这些技术的实现难度可能比你想象中更有挑战。不过,库开发者在标准库实现中采用了这些前沿的技术,这使得采用基于任务的方式编程的开发者在这些技术发展中持续获得回报,相反如果开发者直接使用`std::thread`编程,处理资源耗竭,负责均衡问题的责任就压在了应用开发者身上,更不说如何使得开发方案跨平台使用。
|
||||
最前沿的线程调度器使用系统级线程池(*thread pool*)来避免资源超额的问题,并且通过工作窃取算法(*work-stealing algorithm*)来提升了跨硬件核心的负载均衡。C++标准实际上并不要求使用线程池或者工作窃取,实际上C++11并发规范的某些技术层面使得实现这些技术的难度可能比想象中更有挑战。不过,库开发者在标准库实现中采用了这些技术,也有理由期待这个领域会有更多进展。如果你当前的并发编程采用基于任务的方式,在这些技术发展中你会持续获得回报。相反如果你直接使用`std::thread`编程,处理线程耗尽、资源超额、负责均衡问题的责任就压在了你身上,更不用说你对这些问题的解决方法与同机器上其他程序采用的解决方案配合得好不好了。
|
||||
|
||||
对比基于线程的开发方式,基于任务的设计为开发者避免了线程管理的痛苦,并且自然提供了一种获取异步执行的结果的方式。当然,仍然存在一些场景直接使用`std::thread`会更有优势:
|
||||
对比基于线程的编程方式,基于任务的设计为开发者避免了手动线程管理的痛苦,并且自然提供了一种获取异步执行程序的结果(即返回值或者异常)的方式。当然,仍然存在一些场景直接使用`std::thread`会更有优势:
|
||||
|
||||
- **需要访问非常基础的线程API**。C++并发API通常是通过操作系统提供的系统级API(pthreads 或者 windows threads)来实现的,系统级API通常会提供更加灵活的操作方式,举个例子,C++并发API没有线程优先级和affinities的概念。为了提供对底层系统级线程API的访问,`std::thread`对象提供了`native_handle`的成员函数,而在高层抽象的比如`std::futures`没有这种能力。
|
||||
- **需要优化应用的线程使用**。举个例子,只在特定系统平台运行的软件,可以调教地比使用C++并行API更好的程序性能。
|
||||
- **需要实现C++并发API之外的线程技术**。举例来说,自行实现线程池技术。
|
||||
- **你需要访问非常基础的线程API**。C++并发API通常是通过操作系统提供的系统级API(pthreads或者Windows threads)来实现的,系统级API通常会提供更加灵活的操作方式(举个例子,C++没有线程优先级和亲和性的概念)。为了提供对底层系统级线程API的访问,`std::thread`对象提供了`native_handle`的成员函数,而`std::future`(即`std::async`返回的东西)没有这种能力。
|
||||
- **你需要且能够优化应用的线程使用**。举个例子,你要开发一款已知执行概况的服务器软件,部署在有固定硬件特性的机器上,作为唯一的关键进程。
|
||||
- **你需要实现C++并发API之外的线程技术**,比如,C++实现中未支持的平台的线程池。
|
||||
|
||||
这些都是在应用开发中并不常见的例子,大多数情况,开发者应该优先采用基于任务的编程方式。
|
||||
|
||||
**请记住:**
|
||||
|
||||
|
||||
## 记住
|
||||
|
||||
- `std::thread`API不能直接访问异步执行的结果,如果执行函数有异常抛出,代码会终止执行
|
||||
- 基于线程的编程方式关于解决资源超限,负载均衡的方案移植性不佳
|
||||
- 基于任务的编程方式`std::async`会默认解决上面两条问题
|
||||
- `std::thread` API不能直接访问异步执行的结果,如果执行函数有异常抛出,代码会终止执行。
|
||||
- 基于线程的编程方式需要手动的线程耗尽、资源超额、负责均衡、平台适配性管理。
|
||||
- 通过带有默认启动策略的`std::async`进行基于任务的编程方式会解决大部分问题。
|
||||
|
@ -1,120 +1,135 @@
|
||||
## Item 36:Specify std::launch::async if asynchronicity is essential
|
||||
## 条款三十六:如果有异步的必要请指定`std::launch::async`
|
||||
|
||||
## Item36: 确保在异步为必须时,才指定`std::launch::async`
|
||||
**Item 36: Specify `std::launch::async` if asynchronicity is essential.**
|
||||
|
||||
当你调用`std::async`执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你想要`std::async`执行的操作。你确实通过`std::async`launch policy(译者注:这里没有翻译)要求执行函数,有两种标准policy,都通过`std::launch`域的枚举类型表示(参见Item10关于枚举的更多细节)。假定一个函数**f**传给`std::async`来执行:
|
||||
当你调用`std::async`执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你要求`std::async`执行的操作。你事实上要求这个函数按照`std::async`启动策略来执行。有两种标准策略,每种都通过`std::launch`这个限域`enum`的一个枚举名表示(关于枚举的更多细节参见[Item10](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item10.md))。假定一个函数`f`传给`std::async`来执行:
|
||||
|
||||
- **`std::launch::async`的launch policy**意味着f必须异步执行,即在不同的线程
|
||||
- **`std::launch::deferred`的launch policy**意味着f仅仅在当调用`get或者wait`要求`std::async`的返回值时才执行。这表示f推迟到被求值才延迟执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当`get或wait`被调用,f会同步执行,即调用方停止直到f运行结束。如果`get和wait`都没有被调用,f将不会被执行
|
||||
- **`std::launch::async`启动策略**意味着`f`必须异步执行,即在不同的线程。
|
||||
- **`std::launch::deferred`启动策略**意味着`f`仅当在`std::async`返回的*future*上调用`get`或者`wait`时才执行。这表示`f`**推迟**到存在这样的调用时才执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当`get`或`wait`被调用,`f`会同步执行,即调用方被阻塞,直到`f`运行结束。如果`get`和`wait`都没有被调用,`f`将不会被执行。(这是个简化说法。关键点不是要在其上调用`get`或`wait`的那个*future*,而是*future*引用的那个共享状态。([Item38](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md)讨论了*future*与共享状态的关系。)因为`std::future`支持移动,也可以用来构造`std::shared_future`,并且因为`std::shared_future`可以被拷贝,对共享状态——对`f`传到的那个`std::async`进行调用产生的——进行引用的*future*对象,有可能与`std::async`返回的那个*future*对象不同。这非常绕口,所以经常回避这个事实,简称为在`std::async`返回的*future*上调用`get`或`wait`。)
|
||||
|
||||
有趣的是,`std::async`的默认launch policy是以上两种都不是。相反,是求或在一起的。下面的两种调用含义相同
|
||||
可能让人惊奇的是,`std::async`的默认启动策略——你不显式指定一个策略时它使用的那个——不是上面中任意一个。相反,是求或在一起的。下面的两种调用含义相同:
|
||||
|
||||
```cpp
|
||||
auto fut1 = std::async(f); // run f using default launch policy
|
||||
auto fut2 = std::async(std::launch::async | std::launch::deferred, f); // run f either async or defered
|
||||
auto fut1 = std::async(f); //使用默认启动策略运行f
|
||||
auto fut2 = std::async(std::launch::async | //使用async或者deferred运行f
|
||||
std::launch::deferred,
|
||||
f);
|
||||
```
|
||||
|
||||
因此默认策略允许f异步或者同步执行。如同Item 35中指出,这种灵活性允许`std::async`和标准库的线程管理组件(负责线程的创建或销毁)避免超载。这就是使用`std::async`并发编程如此方便的原因。
|
||||
因此默认策略允许`f`异步或者同步执行。如同[Item35](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md)中指出,这种灵活性允许`std::async`和标准库的线程管理组件承担线程创建和销毁的责任,避免资源超额,以及平衡负载。这就是使用`std::async`并发编程如此方便的原因。
|
||||
|
||||
但是,使用默认启动策略的`std::async`也有一些有趣的影响。给定一个线程t执行此语句:
|
||||
但是,使用默认启动策略的`std::async`也有一些有趣的影响。给定一个线程`t`执行此语句:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(f); // run f using default launch policy
|
||||
auto fut = std::async(f); //使用默认启动策略运行f
|
||||
```
|
||||
|
||||
- 无法预测f是否会与t同时运行,因为f可能被安排延迟运行
|
||||
- 无法预测f是否会在调用`get或wait`的线程上执行。如果那个线程是t,含义就是无法预测f是否也在线程t上执行
|
||||
- 无法预测f是否执行,因为不能确保`get或者wait`会被调用
|
||||
- **无法预测`f`是否会与`t`并发运行**,因为`f`可能被安排延迟运行。
|
||||
- **无法预测`f`是否会在与某线程相异的另一线程上执行,这个某线程在`fut`上调用`get`或`wait`**。如果对`fut`调用函数的线程是`t`,含义就是无法预测`f`是否在异于`t`的另一线程上执行。
|
||||
- **无法预测`f`是否执行**,因为不能确保在程序每条路径上,都会不会在`fut`上调用`get`或者`wait`。
|
||||
|
||||
默认启动策略的调度灵活性导致使用线程本地变量比较麻烦,因为这意味着如果f读写了线程本地存储(thread-local storage, TLS),不可能预测到哪个线程的本地变量被访问:
|
||||
默认启动策略的调度灵活性导致使用`thread_local`变量比较麻烦,因为这意味着如果`f`读写了**线程本地存储**(*thread-local storage*,TLS),不可能预测到哪个线程的变量被访问:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(f); // TLS for f possibly for independent thread, but possibly for thread invoking get or wait on fut
|
||||
auto fut = std::async(f); //f的TLS可能是为单独的线程建的,
|
||||
//也可能是为在fut上调用get或者wait的线程建的
|
||||
```
|
||||
|
||||
还会影响到基于超时机制的wait循环,因为在task的`wait_for`或者`wait_until`调用中(参见Item 35)会产生延迟求值(`std::launch::deferred`)。意味着,以下循环看似应该终止,但是实际上永远运行:
|
||||
这还会影响到基于`wait`的循环使用超时机制,因为在一个延时的任务(参见[Item35](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md))上调用`wait_for`或者`wait_until`会产生`std::launch::deferred`值。意味着,以下循环看似应该最终会终止,但可能实际上永远运行:
|
||||
|
||||
```cpp
|
||||
using namespace std::literals; // for C++14 duration suffixes; see Item 34
|
||||
void f()
|
||||
using namespace std::literals; //为了使用C++14中的时间段后缀;参见条款34
|
||||
|
||||
void f() //f休眠1秒,然后返回
|
||||
{
|
||||
std::this_thread::sleep_for(1s);
|
||||
std::this_thread::sleep_for(1s);
|
||||
}
|
||||
|
||||
auto fut = std::async(f);
|
||||
while (fut.wait_for(100ms) != std::future_status::ready)
|
||||
{ // loop until f has finished running... which may never happen!
|
||||
...
|
||||
auto fut = std::async(f); //异步运行f(理论上)
|
||||
|
||||
while (fut.wait_for(100ms) != //循环,直到f完成运行时停止...
|
||||
std::future_status::ready) //但是有可能永远不会发生!
|
||||
{
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
如果f与调用`std::async`的线程同时运行(即,如果为f选择的启动策略是`std::launch::async`),这里没有问题(假定f最终执行完毕),但是如果f是延迟执行,`fut.wait_for`将总是返回`std::future_status::deferred`。这表示循环会永远执行下去。
|
||||
如果`f`与调用`std::async`的线程并发运行(即,如果为`f`选择的启动策略是`std::launch::async`),这里没有问题(假定`f`最终会执行完毕),但是如果`f`是延迟执行,`fut.wait_for`将总是返回`std::future_status::deferred`。这永远不等于`std::future_status::ready`,循环会永远执行下去。
|
||||
|
||||
这种错误很容易在开发和单元测试中忽略,因为它可能在负载过高时才能显现出来。当机器负载过重时,任务推迟执行才最有可能发生。毕竟,如果硬件没有超载,没有理由不安排任务并发执行。
|
||||
这种错误很容易在开发和单元测试中忽略,因为它可能在负载过高时才能显现出来。那些是使机器资源超额或者线程耗尽的条件,此时任务推迟执行才最有可能发生。毕竟,如果硬件没有资源耗尽,没有理由不安排任务并发执行。
|
||||
|
||||
修复也是很简单的:只需要检查与`std::async`的future是否被延迟执行即可,那样就会避免进入无限循环。不幸的是,没有直接的方法来查看future是否被延迟执行。相反,你必须调用一个超时函数----比如`wait_for`这种函数。在这个逻辑中,你不想等待任何事,只想查看返回值是否`std::future_status::deferred`,如果是就使用0调用`wait_for`来终止循环。
|
||||
修复也是很简单的:只需要检查与`std::async`对应的`future`是否被延迟执行即可,那样就会避免进入无限循环。不幸的是,没有直接的方法来查看`future`是否被延迟执行。相反,你必须调用一个超时函数——比如`wait_for`这种函数。在这个情况中,你不想等待任何事,只想查看返回值是否是`std::future_status::deferred`,所以无须怀疑,使用0调用`wait_for`:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(f);
|
||||
if (fut.wait_for(0s) == std::future_status::deferred) { // if task is deferred
|
||||
... // use wait or get on fut to call f synchronously
|
||||
}
|
||||
else { // task isn't deferred
|
||||
while(fut.wait_for(100ms) != std::future_status::ready) { // infinite loop not possible(assuming f finished)
|
||||
... // task is neither deferred nor ready, so do concurrent word until it's ready
|
||||
}
|
||||
auto fut = std::async(f); //同上
|
||||
|
||||
if (fut.wait_for(0s) == //如果task是deferred(被延迟)状态
|
||||
std::future_status::deferred)
|
||||
{
|
||||
… //在fut上调用wait或get来异步调用f
|
||||
} else { //task没有deferred(被延迟)
|
||||
while (fut.wait_for(100ms) != //不可能无限循环(假设f完成)
|
||||
std::future_status::ready) {
|
||||
… //task没deferred(被延迟),也没ready(已准备)
|
||||
//做并行工作直到已准备
|
||||
}
|
||||
… //fut是ready(已准备)状态
|
||||
}
|
||||
```
|
||||
|
||||
这些各种考虑的结果就是,只要满足以下条件,`std::async`的默认启动策略就可以使用:
|
||||
|
||||
- task不需要和执行`get or wait`的线程并行执行
|
||||
- 不会读写线程的线程本地变量
|
||||
- 可以保证在`std::async`返回的将来会调用`get or wait`,或者该任务可能永远不会执行是可以接受的
|
||||
- 使用`wait_for or wait_until`编码时考虑deferred状态
|
||||
- 任务不需要和执行`get`或`wait`的线程并行执行。
|
||||
- 读写哪个线程的`thread_local`变量没什么问题。
|
||||
- 可以保证会在`std::async`返回的*future*上调用`get`或`wait`,或者该任务可能永远不会执行也可以接受。
|
||||
- 使用`wait_for`或`wait_until`编码时考虑到了延迟状态。
|
||||
|
||||
如果上述条件任何一个都满足不了,你可能想要保证`std::async`的任务真正的异步执行。进行此操作的方法是调用时,将`std::launch::async`作为第一个参数传递:
|
||||
如果上述条件任何一个都满足不了,你可能想要保证`std::async`会安排任务进行真正的异步执行。进行此操作的方法是调用时,将`std::launch::async`作为第一个实参传递:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(std::launch::async, f); // launch f asynchronously
|
||||
auto fut = std::async(std::launch::async, f); //异步启动f的执行
|
||||
```
|
||||
|
||||
事实上,具有类似`std::async`行为的函数,但是会自动使用`std::launch::async`作为启动策略的工具也是很容易编写的,C++11版本如下:
|
||||
事实上,对于一个类似`std::async`行为的函数,但是会自动使用`std::launch::async`作为启动策略的工具,拥有它会非常方便,而且编写起来很容易也使它看起来很棒。C++11版本如下:
|
||||
|
||||
```cpp
|
||||
template<typename F, typename... Ts>
|
||||
inline
|
||||
std::future<typename std::result_of<F(Ts...)>::type>
|
||||
reallyAsync(F&& f, Ts&&... params)
|
||||
reallyAsync(F&& f, Ts&&... params) //返回异步调用f(params...)得来的future
|
||||
{
|
||||
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
|
||||
return std::async(std::launch::async,
|
||||
std::forward<F>(f),
|
||||
std::forward<Ts>(params)...);
|
||||
}
|
||||
```
|
||||
|
||||
这个函数接受一个可调用对象和0或多个参数params然后完美转发(参见Item25)给`std::async`,使用`std::launch::async`作为启动参数。就像`std::async`一样,返回`std::future`类型。确定结果的类型很容易,因为类型特征`std::result_of`可以提供(参见Item 9 关于类型特征的详细表述)。
|
||||
这个函数接受一个可调用对象`f`和0或多个形参`params`,然后完美转发(参见[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md))给`std::async`,使用`std::launch::async`作为启动策略。就像`std::async`一样,返回`std::future`作为用`params`调用`f`得到的结果。确定结果的类型很容易,因为*type trait* `std::result_of`可以提供给你。(参见[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md)关于*type trait*的详细表述。)
|
||||
|
||||
`reallyAsync`就像`std::async`一样使用:
|
||||
|
||||
```cpp
|
||||
auto fut = reallyAsync(f);
|
||||
auto fut = reallyAsync(f); //异步运行f,如果std::async抛出异常它也会抛出
|
||||
```
|
||||
|
||||
在C++14中,返回类型的推导能力可以简化函数的定义:
|
||||
在C++14中,`reallyAsync`返回类型的推导能力可以简化函数的声明:
|
||||
|
||||
```cpp
|
||||
template<typename f, typename... Ts>
|
||||
inline
|
||||
auto
|
||||
template<typename F, typename... Ts>
|
||||
inline
|
||||
auto // C++14
|
||||
reallyAsync(F&& f, Ts&&... params)
|
||||
{
|
||||
return std::async(std::launch::async, std::forward<T>(f), std::forward<Ts>(params)...);
|
||||
return std::async(std::launch::async,
|
||||
std::forward<F>(f),
|
||||
std::forward<Ts>(params)...);
|
||||
}
|
||||
```
|
||||
|
||||
这个版本清楚表明,`reallyAsync`除了使用`std::launch::async`启动策略之外什么也没有做。
|
||||
|
||||
### 需要记住的事
|
||||
**请记住:**
|
||||
|
||||
- `std::async`的默认启动策略是异步或者同步的
|
||||
- 灵活性导致访问**thread_locals**的不确定性,隐含了task可能不会被执行的意思,会影响程序基于`wait`的超时逻辑
|
||||
- 只有确实异步时才指定`std::launch::async`
|
||||
- `std::async`的默认启动策略是异步和同步执行兼有的。
|
||||
- 这个灵活性导致访问`thread_local`s的不确定性,隐含了任务可能不会被执行的意思,会影响调用基于超时的`wait`的程序逻辑。
|
||||
- 如果异步执行任务非常关键,则指定`std::launch::async`。
|
||||
|
@ -1,176 +1,192 @@
|
||||
## Item 37:Make `std::threads` unjoinable on all paths
|
||||
## 条款三十七:使`std::thread`在所有路径最后都不可结合
|
||||
|
||||
每个`std::thread`对象处于两个状态之一:*joinable or unjoinable*。*joinable*状态的`std::thread`对应于正在运行或者可能正在运行的异步执行线程。比如,一个blocked或者等待调度的`std::thread`是*joinable*,已运行结束的`std::thread`也可以认为是*joinable*
|
||||
**Item 37: Make `std::thread`s unjoinable on all paths**
|
||||
|
||||
*unjoinable*的`std::thread`对象比如:
|
||||
每个`std::thread`对象处于两个状态之一:**可结合的**(*joinable*)或者**不可结合的**(*unjoinable*)。可结合状态的`std::thread`对应于正在运行或者可能要运行的异步执行线程。比如,对应于一个阻塞的(*blocked*)或者等待调度的线程的`std::thread`是可结合的,对应于运行结束的线程的`std::thread`也可以认为是可结合的。
|
||||
|
||||
- **Default-constructed std::threads**。这种`std::thread`没有函数执行,因此无法绑定到具体的线程上
|
||||
- **已经被moved的`std::thread`对象**。move的结果就是将`std::thread`对应的线程所有权转移给另一个`std::thread`
|
||||
- **已经joined的`std::thread`**。在join之后,`std::thread`执行结束,不再对应于具体的线程
|
||||
- **已经detached的`std::thread`**。detach断开了`std::thread`与线程之间的连接
|
||||
不可结合的`std::thread`正如所期待:一个不是可结合状态的`std::thread`。不可结合的`std::thread`对象包括:
|
||||
|
||||
- **默认构造的`std::thread`s**。这种`std::thread`没有函数执行,因此没有对应到底层执行线程上。
|
||||
- **已经被移动走的`std::thread`对象**。移动的结果就是一个`std::thread`原来对应的执行线程现在对应于另一个`std::thread`。
|
||||
- **已经被`join`的`std::thread`** 。在`join`之后,`std::thread`不再对应于已经运行完了的执行线程。
|
||||
- **已经被`detach`的`std::thread`** 。`detach`断开了`std::thread`对象与执行线程之间的连接。
|
||||
|
||||
(译者注:`std::thread`可以视作状态保存的对象,保存的状态可能也包括可调用对象,有没有具体的线程承载就是有没有连接)
|
||||
|
||||
`std::thread`的可连接性如此重要的原因之一就是当连接状态的析构函数被调用,执行逻辑被终止。比如,假定有一个函数`doWork`,执行过滤函数`filter`,接收一个参数`maxVal`。`doWork`检查是否满足计算所需的条件,然后通过使用0到maxVal之间的所有值过滤计算。如果进行过滤非常耗时,并且确定doWork条件是否满足也很耗时,则将两件事并发计算是很合理的。
|
||||
`std::thread`的可结合性如此重要的原因之一就是当可结合的线程的析构函数被调用,程序执行会终止。比如,假定有一个函数`doWork`,使用一个过滤函数`filter`,一个最大值`maxVal`作为形参。`doWork`检查是否满足计算所需的条件,然后使用在0到`maxVal`之间的通过过滤器的所有值进行计算。如果进行过滤非常耗时,并且确定`doWork`条件是否满足也很耗时,则将两件事并发计算是很合理的。
|
||||
|
||||
我们希望为此采用基于任务的设计(参与Item 35),但是假设我们希望设置做过滤线程的优先级。Item 35阐释了需要线程的基本句柄,只能通过`std::thread`的API来完成;基于任务的API(比如futures)做不到。所以最终采用基于`std::thread`而不是基于任务
|
||||
我们希望为此采用基于任务的设计(参见[Item35](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md)),但是假设我们希望设置做过滤的线程的优先级。[Item35](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md)阐释了那需要线程的原生句柄,只能通过`std::thread`的API来完成;基于任务的API(比如*future*)做不到。所以最终采用基于线程而不是基于任务。
|
||||
|
||||
我们可能写出以下代码:
|
||||
|
||||
代码如下:
|
||||
|
||||
```cpp
|
||||
constexpr auto tenMillion = 10000000; // see Item 15 for constexpr
|
||||
bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion) // return whether computation was performed; see Item2 for std::function
|
||||
constexpr auto tenMillion = 10000000; //constexpr见条款15
|
||||
|
||||
bool doWork(std::function<bool(int)> filter, //返回计算是否执行;
|
||||
int maxVal = tenMillion) //std::function见条款2
|
||||
{
|
||||
std::vector<int> goodVals;
|
||||
std::thread t([&filter, maxVal, &goodVals]
|
||||
{
|
||||
for (auto i = 0; i <= maxVal; ++i)
|
||||
std::vector<int> goodVals; //满足filter的值
|
||||
|
||||
std::thread t([&filter, maxVal, &goodVals] //填充goodVals
|
||||
{
|
||||
if (filter(i)) goodVals.push_back(i);
|
||||
}
|
||||
});
|
||||
auto nh = t.native_handle(); // use t's native handle to set t's priority
|
||||
...
|
||||
if (conditionsAreStatisfied()) {
|
||||
t.join(); // let t finish
|
||||
performComputation(goodVals); // computation was performed
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // computation was not performed
|
||||
for (auto i = 0; i <= maxVal; ++i)
|
||||
{ if (filter(i)) goodVals.push_back(i); }
|
||||
});
|
||||
|
||||
auto nh = t.native_handle(); //使用t的原生句柄
|
||||
… //来设置t的优先级
|
||||
|
||||
if (conditionsAreSatisfied()) {
|
||||
t.join(); //等t完成
|
||||
performComputation(goodVals);
|
||||
return true; //执行了计算
|
||||
}
|
||||
return false; //未执行计算
|
||||
}
|
||||
```
|
||||
|
||||
在解释这份代码为什么有问题之前,看一下tenMillion的初始化可以在C++14中更加易读,通过单引号分隔数字:
|
||||
在解释这份代码为什么有问题之前,我先把`tenMillion`的初始化值弄得更可读一些,这利用了C++14的能力,使用单引号作为数字分隔符:
|
||||
|
||||
```cpp
|
||||
constexpr auto tenMillion = 10'000'000; // C++14
|
||||
constexpr auto tenMillion = 10'000'000; //C++14
|
||||
```
|
||||
|
||||
还要指出,在开始运行之后设置t的优先级就像把马放出去之后再关上马厩门一样(译者注:太晚了)。更好的设计是在t为挂起状态时设置优先级(这样可以在执行任何计算前调整优先级),但是我不想你为这份代码考虑这个而分心。如果你感兴趣代码中忽略的部分,可以转到Item 39,那个Item告诉你如何以挂起状态开始线程。
|
||||
还要指出,在开始运行之后设置`t`的优先级就像把马放出去之后再关上马厩门一样(译者注:太晚了)。更好的设计是在挂起状态时开始`t`(这样可以在执行任何计算前调整优先级),但是我不想你为考虑那些代码而分心。如果你对代码中忽略的部分感兴趣,可以转到[Item39](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md),那个Item告诉你如何以开始那些挂起状态的线程。
|
||||
|
||||
返回`doWork`。如果`conditionsAreSatisfied()`返回真,没什么问题,但是如果返回假或者抛出异常,`std::thread`类型的`t`在`doWork`结束时会调用`t`的析构器。这造成程序执行中止。
|
||||
返回`doWork`。如果`conditionsAreSatisfied()`返回`true`,没什么问题,但是如果返回`false`或者抛出异常,在`doWork`结束调用`t`的析构函数时,`std::thread`对象`t`会是可结合的。这造成程序执行中止。
|
||||
|
||||
你可能会想,为什么`std::thread`析构的行为是这样的,那是因为另外两种显而易见的方式更糟:
|
||||
|
||||
- **隐式join**。这种情况下,`std::thread`的析构函数将等待其底层的异步执行线程完成。这听起来是合理的,但是可能会导致表现异常,而且难以追踪。比如,如果`conditonAreStatisfied()`已经返回了假,`doWork`继续等待过滤器应用于所有值就很违反直觉。
|
||||
- **隐式`join`** 。这种情况下,`std::thread`的析构函数将等待其底层的异步执行线程完成。这听起来是合理的,但是可能会导致难以追踪的异常表现。比如,如果`conditonAreStatisfied()`已经返回了`false`,`doWork`继续等待过滤器应用于所有值就很违反直觉。
|
||||
|
||||
- **隐式detach**。这种情况下,`std::thread`析构函数会分离其底层的线程。线程继续运行。听起来比join的方式好,但是可能导致更严重的调试问题。比如,在`doWork`中,`goodVals`是通过引用捕获的局部变量。可能会被lambda修改。假定,lambda的执行时异步的,`conditionsAreStatisfied()`返回假。这时,`doWork`返回,同时局部变量`goodVals`被销毁。堆栈被弹出,并在`doWork`的调用点继续执行线程
|
||||
- **隐式`detach`** 。这种情况下,`std::thread`析构函数会分离`std::thread`与其底层的线程。底层线程继续运行。听起来比`join`的方式好,但是可能导致更严重的调试问题。比如,在`doWork`中,`goodVals`是通过引用捕获的局部变量。它也被*lambda*修改(通过调用`push_back`)。假定,*lambda*异步执行时,`conditionsAreSatisfied()`返回`false`。这时,`doWork`返回,同时局部变量(包括`goodVals`)被销毁。栈被弹出,并在`doWork`的调用点继续执行线程。
|
||||
|
||||
某个调用点之后的语句有时会进行其他函数调用,并且至少一个这样的调用可能会占用曾经被`doWork`使用的堆栈位置。我们称为`f`,当`f`运行时,`doWork`启动的lambda仍在继续运行。该lambda可以在堆栈内存中调用`push_back`,该内存曾是`goodVals`,位于`doWork`曾经的堆栈位置。这意味着对`f`来说,内存被修改了,想象一下调试的时候痛苦
|
||||
调用点之后的语句有时会进行其他函数调用,并且至少一个这样的调用可能会占用曾经被`doWork`使用的栈位置。我们调用那么一个函数`f`。当`f`运行时,`doWork`启动的*lambda*仍在继续异步运行。该*lambda*可能在栈内存上调用`push_back`,该内存曾属于`goodVals`,但是现在是`f`的栈内存的某个位置。这意味着对`f`来说,内存被自动修改了!想象一下调试的时候“乐趣”吧。
|
||||
|
||||
标准委员会认为,销毁连接中的线程如此可怕以至于实际上禁止了它(通过指定销毁连接中的线程导致程序终止)
|
||||
标准委员会认为,销毁可结合的线程如此可怕以至于实际上禁止了它(规定销毁可结合的线程导致程序终止)。
|
||||
|
||||
这使你有责任确保使用`std::thread`对象时,在所有的路径上最终都是unjoinable的。但是覆盖每条路径可能很复杂,可能包括`return, continue, break, goto or exception`,有太多可能的路径。
|
||||
这使你有责任确保使用`std::thread`对象时,在所有的路径上超出定义所在的作用域时都是不可结合的。但是覆盖每条路径可能很复杂,可能包括自然执行通过作用域,或者通过`return`,`continue`,`break`,`goto`或异常跳出作用域,有太多可能的路径。
|
||||
|
||||
每当你想每条路径的块之外执行某种操作,最通用的方式就是将该操作放入本地对象的析构函数中。这些对象称为RAII对象,通过RAII类来实例化。(RAII全称为 Resource Acquisition Is Initialization)。RAII类在标准库中很常见。比如STL容器,智能指针,`std::fstream`类等。但是标准库没有RAII的`std::thread`类,可能是因为标准委员会拒绝将`join和detach`作为默认选项,不知道应该怎么样完成RAII。
|
||||
每当你想在执行跳至块之外的每条路径执行某种操作,最通用的方式就是将该操作放入局部对象的析构函数中。这些对象称为**RAII对象**(*RAII objects*),从**RAII类**中实例化。(RAII全称为 “Resource Acquisition Is Initialization”(资源获得即初始化),尽管技术关键点在析构上而不是实例化上)。RAII类在标准库中很常见。比如STL容器(每个容器析构函数都销毁容器中的内容物并释放内存),标准智能指针([Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md)-[20](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md)解释了,`std::uniqu_ptr`的析构函数调用他指向的对象的删除器,`std::shared_ptr`和`std::weak_ptr`的析构函数递减引用计数),`std::fstream`对象(它们的析构函数关闭对应的文件)等。但是标准库没有`std::thread`的RAII类,可能是因为标准委员会拒绝将`join`和`detach`作为默认选项,不知道应该怎么样完成RAII。
|
||||
|
||||
幸运的是,完成自行实现的类并不难。比如,下面的类实现允许调用者指定析构函数`join或者detach`:
|
||||
幸运的是,完成自行实现的类并不难。比如,下面的类实现允许调用者指定`ThreadRAII`对象(一个`std::thread`的RAII对象)析构时,调用`join`或者`detach`:
|
||||
|
||||
```cpp
|
||||
class ThreadRAII {
|
||||
public:
|
||||
enum class DtorAction{ join, detach }; // see Item 10 for enum class info
|
||||
ThreadRAII(std::thread&& t, DtorAction a): action(a), t(std::move(t)) {} // in dtor, take action a on t
|
||||
~ThreadRAII()
|
||||
{
|
||||
if (t.joinable()) {
|
||||
if (action == DtorAction::join) {
|
||||
t.join();
|
||||
} else {
|
||||
t.detach();
|
||||
}
|
||||
enum class DtorAction { join, detach }; //enum class的信息见条款10
|
||||
|
||||
ThreadRAII(std::thread&& t, DtorAction a) //析构函数中对t实行a动作
|
||||
: action(a), t(std::move(t)) {}
|
||||
|
||||
~ThreadRAII()
|
||||
{ //可结合性测试见下
|
||||
if (t.joinable()) {
|
||||
if (action == DtorAction::join) {
|
||||
t.join();
|
||||
} else {
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread& get() { return t; } // see below
|
||||
|
||||
std::thread& get() { return t; } //见下
|
||||
|
||||
private:
|
||||
DtorAction action;
|
||||
std::thread t;
|
||||
DtorAction action;
|
||||
std::thread t;
|
||||
};
|
||||
```
|
||||
|
||||
我希望这段代码是不言自明的,但是下面几点说明可能会有所帮助:
|
||||
|
||||
- 构造器只接受`std::thread`右值,因为我们想要move `std::thread`对象给`ThreadRAII`(再次强调,`std::thread`不可以复制)
|
||||
- 构造器只接受`std::thread`右值,因为我们想要把传来的`std::thread`对象移动进`ThreadRAII`。(`std::thread`不可以复制。)
|
||||
|
||||
- 构造器的参数顺序设计的符合调用者直觉(首先传递`std::thread`,然后选择析构执行的动作),但是成员初始化列表设计的匹配成员声明的顺序。将`std::thread`成员放在声明最后。在这个类中,这个顺序没什么特别之处,调整为其他顺序也没有问题,但是通常,可能一个成员的初始化依赖于另一个,因为`std::thread`对象可能会在初始化结束后就立即执行了,所以在最后声明是一个好习惯。这样就能保证一旦构造结束,所有数据成员都初始化完毕可以安全的异步绑定线程执行
|
||||
- 构造器的形参顺序设计的符合调用者直觉(首先传递`std::thread`,然后选择析构执行的动作,这比反过来更合理),但是成员初始化列表设计的匹配成员声明的顺序。将`std::thread`对象放在声明最后。在这个类中,这个顺序没什么特别之处,但是通常,可能一个数据成员的初始化依赖于另一个,因为`std::thread`对象可能会在初始化结束后就立即执行函数了,所以在最后声明是一个好习惯。这样就能保证一旦构造结束,在前面的所有数据成员都初始化完毕,可以供`std::thread`数据成员绑定的异步运行的线程安全使用。
|
||||
|
||||
- `ThreadRAII`提供了`get`函数访问内部的`std::thread`对象。这类似于标准智能指针提供的`get`函数,可以提供访问原始指针的入口。提供`get`函数避免了`ThreadRAII`复制完整`std::thread`接口的需要,因为着`ThreadRAII`可以在需要`std::thread`上下文的环境中使用
|
||||
- `ThreadRAII`提供了`get`函数访问内部的`std::thread`对象。这类似于标准智能指针提供的`get`函数,可以提供访问原始指针的入口。提供`get`函数避免了`ThreadRAII`复制完整`std::thread`接口的需要,也意味着`ThreadRAII`可以在需要`std::thread`对象的上下文环境中使用。
|
||||
|
||||
- 在`ThreadRAII`析构函数调用`std::thread`对象t的成员函数之前,检查t是否joinable。这是必须的,因为在unjoinbale的`std::thread`上调用`join or detach`会导致未定义行为。客户端可能会构造一个`std::thread` t,然后通过t构造一个`ThreadRAII`,使用`get`获取t,然后移动t,或者调用`join or detach`,每一个操作都使得t变为unjoinable
|
||||
- 在`ThreadRAII`析构函数调用`std::thread`对象`t`的成员函数之前,检查`t`是否可结合。这是必须的,因为在不可结合的`std::thread`上调用`join`或`detach`会导致未定义行为。客户端可能会构造一个`std::thread`,然后用它构造一个`ThreadRAII`,使用`get`获取`t`,然后移动`t`,或者调用`join`或`detach`,每一个操作都使得`t`变为不可结合的。
|
||||
|
||||
如果你担心下面这段代码
|
||||
|
||||
```cpp
|
||||
if (t.joinable()) {
|
||||
if (action == DtorAction::join) {
|
||||
t.join();
|
||||
} else {
|
||||
t.detach();
|
||||
}
|
||||
if (action == DtorAction::join) {
|
||||
t.join();
|
||||
} else {
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
存在竞争,因为在`t.joinable()`和`t.join or t.detach`执行中间,可能有其他线程改变了t为unjoinable,你的态度很好,但是这个担心不必要。`std::thread`只有自己可以改变`joinable or unjoinable`的状态。在`ThreadRAII`的析构函数中被调用时,其他线程不可能做成员函数的调用。如果同时进行调用,那肯定是有竞争的,但是不在析构函数中,是在客户端代码中试图同时在一个对象上调用两个成员函数(析构函数和其他函数)。通常,仅当所有都为const成员函数时,在一个对象同时调用两个成员函数才是安全的。
|
||||
存在竞争,因为在`t.joinable()`的执行和调用`join`或`detach`的中间,可能有其他线程改变了`t`为不可结合,你的直觉值得表扬,但是这个担心不必要。只有调用成员函数才能使`std::thread`对象从可结合变为不可结合状态,比如`join`,`detach`或者移动操作。在`ThreadRAII`对象析构函数调用时,应当没有其他线程在那个对象上调用成员函数。如果同时进行调用,那肯定是有竞争的,但是不在析构函数中,是在客户端代码中试图同时在一个对象上调用两个成员函数(析构函数和其他函数)。通常,仅当所有都为`const`成员函数时,在一个对象同时调用多个成员函数才是安全的。
|
||||
|
||||
在`doWork`的例子上使用`ThreadRAII`的代码如下:
|
||||
|
||||
```cpp
|
||||
bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion)
|
||||
bool doWork(std::function<bool(int)> filter, //同之前一样
|
||||
int maxVal = tenMillion)
|
||||
{
|
||||
std::vector<int> goodVals;
|
||||
ThreadRAII t(std::thread([&filter, maxVal, &goodVals] {
|
||||
for (auto i = 0; i <= maxVal; ++i) {
|
||||
if (filter(i)) goodVals.push_back(i);
|
||||
}
|
||||
}),
|
||||
ThreadRAII::DtorAction::join
|
||||
);
|
||||
auto nh = t.get().native_handle();
|
||||
...
|
||||
if (conditonsAreStatisfied()) {
|
||||
t.get().join();
|
||||
performComputation(goodVals);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
std::vector<int> goodVals; //同之前一样
|
||||
|
||||
ThreadRAII t( //使用RAII对象
|
||||
std::thread([&filter, maxVal, &goodVals]
|
||||
{
|
||||
for (auto i = 0; i <= maxVal; ++i)
|
||||
{ if (filter(i)) goodVals.push_back(i); }
|
||||
}),
|
||||
ThreadRAII::DtorAction::join //RAII动作
|
||||
);
|
||||
|
||||
auto nh = t.get().native_handle();
|
||||
…
|
||||
|
||||
if (conditionsAreSatisfied()) {
|
||||
t.get().join();
|
||||
performComputation(goodVals);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
这份代码中,我们通过`ThreadRAII`的析构函数对异步执行的线程进行`join`,因为在先前分析中,`detach`可能导致非常难缠的bug。我们之前也分析了`join`可能会导致表现异常(坦率说,也可能调试困难),但是在未定义行为(`detach`导致),程序终止(`std::thread`默认导致),或者表现异常之间选择一个后果,可能表现异常是最好的那个。
|
||||
这种情况下,我们选择在`ThreadRAII`的析构函数对异步执行的线程进行`join`,因为在先前分析中,`detach`可能导致噩梦般的调试过程。我们之前也分析了`join`可能会导致表现异常(坦率说,也可能调试困难),但是在未定义行为(`detach`导致),程序终止(使用原生`std::thread`导致),或者表现异常之间选择一个后果,可能表现异常是最好的那个。
|
||||
|
||||
哎,Item 39表明了使用`ThreadRAII`来保证在`std::thread`的析构时执行`join`有时可能不仅导致程序表现异常,还可能导致程序挂起。“适当”的解决方案是此类程序应该和异步执行的lambda通信,告诉它不需要执行了,可以直接返回,但是C++11中不支持可中断线程。可以自行实现,但是这不是本书讨论的主题。(译者注:关于这一点,C++ Concurrency in Action 的section 9.2 中有详细讨论,也有中文版出版)
|
||||
哎,[Item39](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md)表明了使用`ThreadRAII`来保证在`std::thread`的析构时执行`join`有时不仅可能导致程序表现异常,还可能导致程序挂起。“适当”的解决方案是此类程序应该和异步执行的*lambda*通信,告诉它不需要执行了,可以直接返回,但是C++11中不支持**可中断线程**(*interruptible threads*)。可以自行实现,但是这不是本书讨论的主题。(关于这一点,Anthony Williams的《C++ Concurrency in Action》(Manning Publications,2012)的section 9.2中有详细讨论。)(译者注:此书中文版已出版,名为《C++并发编程实战》,且本文翻译时(2020)已有第二版出版。)
|
||||
|
||||
Item 17说明因为`ThreadRAII`声明了一个析构函数,因此不会有编译器生成移动操作,但是没有理由`ThreadRAII`对象不能移动。所以需要我们显式声明来告诉编译器自动生成:
|
||||
[Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)说明因为`ThreadRAII`声明了一个析构函数,因此不会有编译器生成移动操作,但是没有理由`ThreadRAII`对象不能移动。如果要求编译器生成这些函数,函数的功能也正确,所以显式声明来告诉编译器自动生成也是合适的:
|
||||
|
||||
```cpp
|
||||
class ThreadRAII {
|
||||
public:
|
||||
enum class DtorAction{ join, detach }; // see Item 10 for enum class info
|
||||
ThreadRAII(std::thread&& t, DtorAction a): action(a), t(std::move(t)) {} // in dtor, take action a on t
|
||||
~ThreadRAII()
|
||||
{
|
||||
if (t.joinable()) {
|
||||
if (action == DtorAction::join) {
|
||||
t.join();
|
||||
} else {
|
||||
t.detach();
|
||||
}
|
||||
enum class DtorAction { join, detach }; //跟之前一样
|
||||
|
||||
ThreadRAII(std::thread&& t, DtorAction a) //跟之前一样
|
||||
: action(a), t(std::move(t)) {}
|
||||
|
||||
~ThreadRAII()
|
||||
{
|
||||
… //跟之前一样
|
||||
}
|
||||
}
|
||||
|
||||
ThreadRAII(ThreadRAII&&) = default;
|
||||
ThreadRAII& operator=(ThreadRAII&&) = default;
|
||||
std::thread& get() { return t; } // see below
|
||||
private:
|
||||
DtorAction action;
|
||||
std::thread t;
|
||||
|
||||
ThreadRAII(ThreadRAII&&) = default; //支持移动
|
||||
ThreadRAII& operator=(ThreadRAII&&) = default;
|
||||
|
||||
std::thread& get() { return t; } //跟之前一样
|
||||
|
||||
private: // as before
|
||||
DtorAction action;
|
||||
std::thread t;
|
||||
};
|
||||
```
|
||||
|
||||
### 需要记住的事
|
||||
**请记住:**
|
||||
|
||||
- 在所有路径上保证`thread`最终是unjoinable
|
||||
- 析构时`join`会导致难以调试的表现异常问题
|
||||
- 析构时`detach`会导致难以调试的未定义行为
|
||||
- 声明类数据成员时,最后声明`std::thread`类型成员
|
||||
- 在所有路径上保证`thread`最终是不可结合的。
|
||||
- 析构时`join`会导致难以调试的表现异常问题。
|
||||
- 析构时`detach`会导致难以调试的未定义行为。
|
||||
- 声明类数据成员时,最后声明`std::thread`对象。
|
||||
|
@ -104,7 +104,7 @@ cv.notify_one(); //通知反应任务(第2部分)
|
||||
|
||||
方案很简单。检测任务有一个`std::promise`对象(即通信信道的写入端),反应任务有对应的*future*。当检测任务看到事件已经发生,设置`std::promise`对象(即写入到通信信道)。同时,反应任务。`wait`会锁住反应任务直到`std::promise`被设置。
|
||||
|
||||
现在,`std::promise`和*futures*(即`std::future`和`std::shared_future`)都是需要类型形参的模板。形参表明通过通信信道被传递的信息的类型。在这里,没有数据被传递,只需要让反应任务知道它的*future*已经被设置了。我们在`std::promise`和*future*模板中需要的东西是表明通信信道中没有数据被传递的一个类型。这个类型就是`void`。检测任务使用`std::promise<void>`,反应任务使用`std::future<void>`或者`std::shared_future<void>`。当感兴趣的事件发生时,检测任务设置`std::promise<void>`,反应任务在*future*上`wait`。尽管反应任务不从检测任务那里接收任何数据,通信信道也可以让反应任务知道,检测任务什么时候已经通过对`std::promise<void>`调用`set_value`“写入”了`void`数据。
|
||||
现在,`std::promise`和*futures*(即`std::future`和`std::shared_future`)都是需要类型参数的模板。形参表明通过通信信道被传递的信息的类型。在这里,没有数据被传递,只需要让反应任务知道它的*future*已经被设置了。我们在`std::promise`和*future*模板中需要的东西是表明通信信道中没有数据被传递的一个类型。这个类型就是`void`。检测任务使用`std::promise<void>`,反应任务使用`std::future<void>`或者`std::shared_future<void>`。当感兴趣的事件发生时,检测任务设置`std::promise<void>`,反应任务在*future*上`wait`。尽管反应任务不从检测任务那里接收任何数据,通信信道也可以让反应任务知道,检测任务什么时候已经通过对`std::promise<void>`调用`set_value`“写入”了`void`数据。
|
||||
|
||||
所以,有
|
||||
|
||||
@ -175,7 +175,7 @@ void detect()
|
||||
|
||||
这样看起来安全多了。问题在于第一个“…”区域中(注释了“`tr`中的线程在这里被挂起”的那句),如果异常发生,`p`上的`set_value`永远不会调用,这意味着*lambda*中的`wait`永远不会返回。那意味着在*lambda*中运行的线程不会结束,这是个问题,因为RAII对象`tr`在析构函数中被设置为在(`tr`中创建的)那个线程上实行`join`。换句话说,如果在第一个“…”区域中发生了异常,函数挂起,因为`tr`的析构函数永远无法完成。
|
||||
|
||||
有很多方案解决这个问题,但是我把这个经验留给读者。(一个开始研究这个问题的好地方是我的博客*[The View From Aristeia](http://scottmeyers.blogspot.com/)*中,2013年12月24日的文章“[ThreadRAII + Thread Suspension = Trouble?](http://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html)”)这里,我只想展示如何扩展原始代码(即不使用RAII类)使其挂起然后取消挂起不仅一个反应任务,而是多个任务。简单概括,关键就是在`react`的代码中使用`std::shared_future`代替`std::future`。一旦你知道`std::future`的`share`成员函数将共享状态所有权转移到`share`产生的`std::shared_future`中,代码自然就写出来了。唯一需要注意的是,每个反应线程都需要自己的`std::shared_future`副本,该副本引用共享状态,因此通过`share`获得的`shared_future`要被在反应线程中运行的*lambda*按值捕获:
|
||||
有很多方案解决这个问题,但是我把这个经验留给读者。(一个开始研究这个问题的好地方是我的博客[*The View From Aristeia*](http://scottmeyers.blogspot.com/)中,2013年12月24日的文章“[ThreadRAII + Thread Suspension = Trouble?](http://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html)”)这里,我只想展示如何扩展原始代码(即不使用RAII类)使其挂起然后取消挂起不仅一个反应任务,而是多个任务。简单概括,关键就是在`react`的代码中使用`std::shared_future`代替`std::future`。一旦你知道`std::future`的`share`成员函数将共享状态所有权转移到`share`产生的`std::shared_future`中,代码自然就写出来了。唯一需要注意的是,每个反应线程都需要自己的`std::shared_future`副本,该副本引用共享状态,因此通过`share`获得的`shared_future`要被在反应线程中运行的*lambda*按值捕获:
|
||||
|
||||
```cpp
|
||||
std::promise<void> p; //跟之前一样
|
||||
|
@ -1,74 +1,74 @@
|
||||
## Item 40:Use std::atomic for councurrency, volatile for special memory
|
||||
## 条款四十:对于并发使用`std::atomic`,对于特殊内存使用`volatile`
|
||||
|
||||
Item 40: 当需要并发时使用`std::atomic`,特定内存才使用`volatile`
|
||||
**Item 40: Use `std::atomic` for concurrency, `volatile` for special memory**
|
||||
|
||||
可伶的`volatile`。如此令人迷惑。本不应该出现在本章节,因为它没有关于并发的能力。但是在其他编程语言中(比如,Java和C#),`volatile`是有并发含义的,即使在C++中,有些编译器在实现时也将并发的某种含义加入到了`volatile`关键字中。因此在此值得讨论下关于`volatile`关键字的含义以消除异议。
|
||||
可怜的`volatile`。如此令人迷惑。本不应该出现在本章节,因为它跟并发编程没有关系。但是在其他编程语言中(比如,Java和C#),`volatile`是有并发含义的,即使在C++中,有些编译器在实现时也将并发的某种含义加入到了`volatile`关键字中(但仅仅是在用那些编译器时)。因此在此值得讨论下关于`volatile`关键字的含义以消除异议。
|
||||
|
||||
开发者有时会混淆`volatile`的特性是`std::atomic`(这确实本节的内容)的模板。这种模板的实例化(比如,`std::atomic<int> , std::atomic<bool>, std::atomic<Widget*>`等)给其他线程提供了原子操作的保证。一旦`std::atomic`对象被构建,在其上的操作使用特定的机器指令实现,这比锁的实现更高效。
|
||||
开发者有时会与`volatile`混淆的特性——本来应该属于本章的那个特性——是`std::atomic`模板。这种模板的实例化(比如,`std::atomic<int>`,`std::atomic<bool>`,`std::atomic<Widget*>`等)提供了一种在其他线程看来操作是原子性的的保证(译注:即某些操作是像原子一样的不可分割。)。一旦`std::atomic`对象被构建,在其上的操作表现得像操作是在互斥锁保护的关键区内,但是通常这些操作是使用特定的机器指令实现,这比锁的实现更高效。
|
||||
|
||||
分析如下使用`std::atmoic`的代码:
|
||||
|
||||
```cpp
|
||||
std::atomic<int> ai(0); // initialize ai to 0
|
||||
ai = 10; // atomically set ai to 10
|
||||
std::cout << ai; // atomically read ai's value
|
||||
++ai; //atomically increment ai to 11
|
||||
--ai; // atomically decrement ai to 10
|
||||
std::atomic<int> ai(0); //初始化ai为0
|
||||
ai = 10; //原子性地设置ai为10
|
||||
std::cout << ai; //原子性地读取ai的值
|
||||
++ai; //原子性地递增ai到11
|
||||
--ai; //原子性地递减ai到10
|
||||
```
|
||||
|
||||
在这些语句执行过程中,其他线程读取`ai`,只能读取到0,10,11三个值其中一个。在没有其他线程修改`ai`情况下,没有其他可能。
|
||||
在这些语句执行过程中,其他线程读取`ai`,只能读取到0,10,11三个值其中一个。没有其他可能(当然,假设只有这个线程会修改`ai`)。
|
||||
|
||||
这个例子中有两点值得注意。**首先**,在`std::cout << ai;`中,`std::atomic`只保证了对`ai`的读取时原子的。没有保证语句的整个执行是原子的,这意味着在读取`ai`与将其通过`≤≤`操作符写入到标准输出之间,另一个线程可能会修改`ai`的值。这对于这个语句没有影响,因为`<<`操作符是按值传递参数的(所以输出就是读取到的`ai`的值),但是重要的是要理解原子性的范围只保证了读取是原子的。
|
||||
这个例子中有两点值得注意。首先,在“`std::cout << ai;`”中,`ai`是一个`std::atomic`的事实只保证了对`ai`的读取是原子的。没有保证整个语句的执行是原子的。在读取`ai`的时刻与调用`operator<<`将值写入到标准输出之间,另一个线程可能会修改`ai`的值。这对于这个语句没有影响,因为`int`的`operator<<`是使用`int`型的传值形参来输出(所以输出的值就是读取到的`ai`的值),但是重要的是要理解原子性的范围只保证了读取`ai`是原子性的。
|
||||
|
||||
第二点值得注意的是最后两条语句---关于`ai`的加减。他们都是 read-modify-write(RMW)操作,各自原子执行。这是`std::atomic`类型的最优的特性之一:一旦`std::atomic`对象被构建,所有成员函数,包括RMW操作,对于其他线程来说保证原子执行。
|
||||
第二点值得注意的是最后两条语句——关于`ai`的递增递减。他们都是读-改-写(read-modify-write,RMW)操作,它们整体作为原子执行。这是`std::atomic`类型的最优的特性之一:一旦`std::atomic`对象被构建,所有成员函数,包括RMW操作,从其他线程来看都是原子性的。
|
||||
|
||||
相反,使用`volatile`在多线程中不保证任何事情:
|
||||
相反,使用`volatile`在多线程中实际上不保证任何事情:
|
||||
|
||||
```cpp
|
||||
volatile int vi(0); // initalize vi to 0
|
||||
vi = 10; // set vi to 10
|
||||
std::cout << vi; // read vi's value
|
||||
++vi; // increment vi to 11
|
||||
--vi; // decrement vi to 10
|
||||
volatile int vi(0); //初始化vi为0
|
||||
vi = 10; //设置vi为10
|
||||
std::cout << vi; //读vi的值
|
||||
++vi; //递增vi到11
|
||||
--vi; //递减vi到10
|
||||
```
|
||||
|
||||
代码的执行过程中,如果其他线程读取`vi`,可能读到任何值,比如-12,68,4090727。这份代码就是未定义的,因为这里的语句修改`vi`,同时其他线程读取,这就是有没有`std::atomic`或者互斥锁保护的对于内存的同时读写,这就是数据竞争的定义。
|
||||
代码的执行过程中,如果其他线程读取`vi`,可能读到任何值,比如-12,68,4090727——任何值!这份代码有未定义行为,因为这里的语句修改`vi`,所以如果同时其他线程读取`vi`,同时存在多个readers和writers读取没有`std::atomic`或者互斥锁保护的内存,这就是数据竞争的定义。
|
||||
|
||||
为了举一个关于在多线程程序中`std::atomic`和`volatile`表现不同的恰当例子,考虑这样一个加单的计数器,同时初始化为0:
|
||||
举一个关于在多线程程序中`std::atomic`和`volatile`表现不同的具体例子,考虑这样一个简单的计数器,通过多线程递增。我们把它们初始化为0:
|
||||
|
||||
```cpp
|
||||
std::atomic<int> ac(0);
|
||||
volatile int vc(0);
|
||||
std::atomic<int> ac(0); //“原子性的计数器”
|
||||
volatile int vc(0); //“volatile计数器”
|
||||
```
|
||||
|
||||
然后我们在两个同时运行的线程中对两个计数器计数:
|
||||
然后我们在两个同时运行的线程中对两个计数器递增:
|
||||
|
||||
```cpp
|
||||
/*--------- Thread1 ---------*/ /*---------- Thread2 -----------*/
|
||||
++ac; ++ac;
|
||||
++vc; ++vc;
|
||||
/*----- Thread 1 ----- */ /*------- Thread 2 ------- */
|
||||
++ac; ++ac;
|
||||
++vc; ++vc;
|
||||
```
|
||||
|
||||
当两个线程执行结束时,`ac`的值肯定是2,以为每个自增操作都是原子的。另一方面,`vc`的值,不一定是2,因为自增不是原子的。每个自增操作包括了读取`vc`的值,增加读取的值,然后将结果写回到`vc`。这三个操作对于`volatile`修饰的整形变量不能保证原子执行,所有可能是下面的执行顺序:
|
||||
当两个线程执行结束时,`ac`的值(即`std::atomic`的值)肯定是2,因为每个自增操作都是不可分割的(原子性的)。另一方面,`vc`的值,不一定是2,因为自增不是原子性的。每个自增操作包括了读取`vc`的值,增加读取的值,然后将结果写回到`vc`。这三个操作对于`volatile`对象不能保证原子执行,所有可能是下面的交叉执行顺序:
|
||||
|
||||
1. Thread1 读取`vc`的值,是0
|
||||
2. Thread2读取`vc`的值,还是0
|
||||
3. Thread1 将0加1,然后写回到`vc`
|
||||
4. Thread2将0加1,然后写回到vc
|
||||
1. Thread1读取`vc`的值,是0。
|
||||
2. Thread2读取`vc`的值,还是0。
|
||||
3. Thread1将读到的0加1,然后写回到`vc`。
|
||||
4. Thread2将读到的0加1,然后写回到`vc`。
|
||||
|
||||
`vc`的最后结果是1,即使看起来自增了两次。
|
||||
|
||||
不仅只有这一种执行顺序的可能,`vc`的最终结果是不可预测的,因为`vc`会发生数据竞争,标准规定数据竞争的造成的未定义行为表示编译器生成的代码可能是任何逻辑,当然,编译器不会利用这种行为来作恶。但是只有在没有数据竞争的程序中编译器的优化才有效,这些优化在存在数据竞争的程序中会造成异常和不可预测的行为。
|
||||
不仅只有这一种可能的结果,通常来说`vc`的最终结果是不可预测的,因为`vc`会发生数据竞争,对于数据竞争造成未定义行为,标准规定表示编译器生成的代码可能是任何逻辑。当然,编译器不会利用这种行为来作恶。但是它们通常做出一些没有数据竞争的程序中才有效的优化,这些优化在存在数据竞争的程序中会造成异常和不可预测的行为。
|
||||
|
||||
RMW操作不是仅有的`std::atomic`在并发中有效而`volatile`无效的例子。假定一个任务计算第二个任务需要的重要值。当第一个任务完成计算,必须传递给第二个任务。Item 39表明一种使用`std::atomic<bool>`的方法来使第一个任务通知第二个任务计算完成。代码如下:
|
||||
RMW操作不是仅有的`std::atomic`在并发中有效而`volatile`无效的例子。假定一个任务计算第二个任务需要的一个重要的值。当第一个任务完成计算,必须传递给第二个任务。[Item39]()表明一种使用`std::atomic<bool>`的方法来使第一个任务通知第二个任务计算完成。计算值的任务的代码如下:
|
||||
|
||||
```cpp
|
||||
std::atomic<bool> valVailable(false);
|
||||
auto imptValue = coputeImportantValue(); // compute value
|
||||
valAvailable = true; // tell other task it's vailable
|
||||
auto imptValue = computeImportantValue(); //计算值
|
||||
valAvailable = true; //告诉另一个任务,值可用了
|
||||
```
|
||||
|
||||
人类读这份代码,能看到在`valAvailable`赋值true之前对`imptValue`赋值是重要的顺序,但是所有编译器看到的是一对没有依赖关系的赋值操作。通常来说,编译器会被允许重排这对没有关联的操作。这意味着,给定如下顺序的赋值操作:
|
||||
人类读这份代码,能看到在`valAvailable`赋值之前对`imptValue`赋值很关键,但是所有编译器看到的是给相互独立的变量的一对赋值操作。通常来说,编译器会被允许重排这对没有关联的操作。这意味着,给定如下顺序的赋值操作(其中`a`,`b`,`x`,`y`都是互相独立的变量),
|
||||
|
||||
```cpp
|
||||
a = b;
|
||||
@ -82,30 +82,30 @@ x = y;
|
||||
a = b;
|
||||
```
|
||||
|
||||
即使编译器没有重排顺序,底层硬件也可能重排,因为有时这样代码执行更快。
|
||||
即使编译器没有重排顺序,底层硬件也可能重排(或者可能使它看起来运行在其他核心上),因为有时这样代码执行更快。
|
||||
|
||||
然而,`std::atomic`会限制这种重排序,并且这样的限制之一是,在源代码中,对`std::atomic`变量写之前不会有任何操作。这意味对我们的代码
|
||||
然而,`std::atomic`会限制这种重排序,并且这样的限制之一是,在源代码中,对`std::atomic`变量写之前不会有任何操作(或者操作发生在其他核心上)。(这只在`std::atomic`s使用**顺序一致性**(*sequential consistency*)时成立,对于使用在本书中展示的语法的`std::atomic`对象,这也是默认的和唯一的一致性模型。C++11也支持带有更灵活的代码重排规则的一致性模型。这样的**弱**(*weak*)(亦称**松散的**,*relaxed*)模型使构建一些软件在某些硬件构架上运行的更快成为可能,但是使用这样的模型产生的软件**更加**难改正、理解、维护。在使用松散原子性的代码中微小的错误很常见,即使专家也会出错,所以应当尽可能坚持顺序一致性。)这意味对我们的代码,
|
||||
|
||||
```cpp
|
||||
auto impatValue = computeImportantValue();
|
||||
valVailable = true;
|
||||
auto imptValue = computeImportantValue(); //计算值
|
||||
valAvailable = true; //告诉另一个任务,值可用了
|
||||
```
|
||||
|
||||
编译器不仅要保证赋值顺序,还要保证生成的硬件代码不会改变这个顺序。结果就是,将`valAvaliable`声明为`std::atomic`确保了必要的顺序---- 其他线程看到`imptValue`值保证`valVailable`设为true之后。
|
||||
编译器不仅要保证`imptValue`和`valAvailable`的赋值顺序,还要保证生成的硬件代码不会改变这个顺序。结果就是,将`valAvailable`声明为`std::atomic`确保了必要的顺序——其他线程看到的是`imptValue`值的改变不会晚于`valAvailable`。
|
||||
|
||||
声明为`volatile`不能保证上述顺序:
|
||||
将`valAvailable`声明为`volatile`不能保证上述顺序:
|
||||
|
||||
```cpp
|
||||
volatile bool valAvaliable(false);
|
||||
volatile bool valVailable(false);
|
||||
auto imptValue = computeImportantValue();
|
||||
valAvailable = true;
|
||||
valAvailable = true; //其他线程可能看到这个赋值操作早于imptValue的赋值操作
|
||||
```
|
||||
|
||||
这份代码编译器可能将赋值顺序对调,也可能在生成机器代码时,其他核心看到`valVailable`更改在`imptValue`之前。
|
||||
这份代码编译器可能将`imptValue`和`valAvailable`赋值顺序对调,如果它们没这么做,可能不能生成机器代码,来阻止底部硬件在其他核心上的代码看到`valAvailable`更改在`imptValue`之前。
|
||||
|
||||
--------
|
||||
这两个问题——不保证操作的原子性以及对代码重排顺序没有足够限制——解释了为什么`volatile`在多线程编程中没用,但是没有解释它应该用在哪。简而言之,它是用来告诉编译器,它们处理的内存有不正常的表现。
|
||||
|
||||
“正常”内存应该有这个特性,在写入值之后,这个值会一直保证直到被覆盖。假设有这样一个正常的int
|
||||
“正常”内存应该有这个特性,在写入值之后,这个值会一直保持直到被覆盖。假设有这样一个正常的`int`
|
||||
|
||||
```cpp
|
||||
int x;
|
||||
@ -114,141 +114,141 @@ int x;
|
||||
编译器看到下列的操作序列:
|
||||
|
||||
```cpp
|
||||
auto y = x; // read x
|
||||
y = x; // read x again
|
||||
auto y = x; //读x
|
||||
y = x; //再次读x
|
||||
```
|
||||
|
||||
编译器可通过忽略对y的一次赋值来优化代码,因为初始化和赋值是冗余的。
|
||||
编译器可通过忽略对`y`的一次赋值来优化代码,因为有了`y`初始化,赋值是冗余的。
|
||||
|
||||
正常内存还有一个特征,就是如果你写入内存没有读取,再次写入,第一次写就可以被忽略,因为肯定会被覆盖。给出下面的代码:
|
||||
正常内存还有一个特征,就是如果你写入内存没有读取,再次写入,第一次写就可以被忽略,因为值没被用过。给出下面的代码:
|
||||
|
||||
```cpp
|
||||
x = 10; // write x
|
||||
x = 20; // write x again
|
||||
x = 10; //写x
|
||||
x = 20; //再次写x
|
||||
```
|
||||
|
||||
编译器可以忽略第一次写入。这意味着如果写在一起:
|
||||
|
||||
```cpp
|
||||
auto y = x;
|
||||
y = x;
|
||||
x = 10;
|
||||
x = 20;
|
||||
auto y = x; //读x
|
||||
y = x; //再次读x
|
||||
x = 10; //写x
|
||||
x = 20; //再次写x
|
||||
```
|
||||
|
||||
编译器生成的代码是这样的:
|
||||
|
||||
```cpp
|
||||
auto y = x;
|
||||
x = 20;
|
||||
auto y = x; //读x
|
||||
x = 20; //写x
|
||||
```
|
||||
|
||||
可能你会想谁会写这种重复读写的代码(技术上称为redundant loads 和 dead stores),答案是开发者不会直接写,至少我们不希望开发者这样写。但是在编译器执行了模板实例化,内联和一系列重排序优化之后,结果会出现多余的操作和无效存储,所以编译器需要摆脱这样的情况并不少见。
|
||||
可能你会想谁会写这种重复读写的代码(技术上称为**冗余访问**(*redundant loads*)和**无用存储**(*dead stores*)),答案是开发者不会直接写——至少我们不希望开发者这样写。但是在编译器拿到看起来合理的代码,执行了模板实例化,内联和一系列重排序优化之后,结果会出现冗余访问和无用存储,所以编译器需要摆脱这样的情况并不少见。
|
||||
|
||||
这种有话讲仅仅在内存表现正常时有效。“特殊”的内存不行。最常见的“特殊”内存是用来memory-mapped I/O的内存。这种内存实际上是与外围设备(比如外部传感器或者显示器,打印机,网络端口)通信,而不是读写(比如RAM)。这种情况下,再次考虑多余的代码:
|
||||
这种优化仅仅在内存表现正常时有效。“特殊”的内存不行。最常见的“特殊”内存是用来做内存映射I/O的内存。这种内存实际上是与外围设备(比如外部传感器或者显示器,打印机,网络端口)通信,而不是读写通常的内存(比如RAM)。这种情况下,再次考虑这看起来冗余的代码:
|
||||
|
||||
```cpp
|
||||
auto y = x; // read x
|
||||
y = x; // read x again
|
||||
auto y = x; //读x
|
||||
y = x; //再次读x
|
||||
```
|
||||
|
||||
如果x的值是一个温度传感器上报的,第二次对于x的读取就不是多余的,因为温度可能在第一次和第二次读取之间变化。
|
||||
如果`x`的值是一个温度传感器上报的,第二次对于`x`的读取就不是多余的,因为温度可能在第一次和第二次读取之间变化。
|
||||
|
||||
类似的,写也是一样:
|
||||
看起来冗余的写操作也类似。比如在这段代码中:
|
||||
|
||||
```cpp
|
||||
x = 10;
|
||||
x = 20;
|
||||
x = 10; //写x
|
||||
x = 20; //再次写x
|
||||
```
|
||||
|
||||
如果x与无线电发射器的控制端口关联,则代码时控制无线电,10和20意味着不同的指令。优化会更改第一条无线电指令。
|
||||
如果`x`与无线电发射器的控制端口关联,则代码是给无线电发指令,10和20意味着不同的指令。优化掉第一条赋值会改变发送到无线电的指令流。
|
||||
|
||||
`volatile`是告诉编译器我们正在处理“特殊”内存。意味着告诉编译器“不要对这块内存执行任何优化”。所以如果x对应于特殊内存,应该声明为`volatile`:
|
||||
`volatile`是告诉编译器我们正在处理特殊内存。意味着告诉编译器“不要对这块内存执行任何优化”。所以如果`x`对应于特殊内存,应该声明为`volatile`:
|
||||
|
||||
```cpp
|
||||
volatile int x;
|
||||
```
|
||||
|
||||
带回我们原始代码:
|
||||
考虑对我们的原始代码序列有何影响:
|
||||
|
||||
```cpp
|
||||
auto y = x;
|
||||
y = x; // can't be optimized away
|
||||
auto y = x; //读x
|
||||
y = x; //再次读x(不会被优化掉)
|
||||
|
||||
x = 10; // can't be optimized away
|
||||
x = 20;
|
||||
x = 10; //写x(不会被优化掉)
|
||||
x = 20; //再次写x
|
||||
```
|
||||
|
||||
如果x是内存映射(或者已经映射到跨进程共享的内存位置等),这正是我们想要的。
|
||||
如果`x`是内存映射的(或者已经映射到跨进程共享的内存位置等),这正是我们想要的。
|
||||
|
||||
那么,在最后一段代码中,y是什么类型:int还是volatile int?
|
||||
突击测试!在最后一段代码中,`y`是什么类型:`int`还是`volatile int`?(`y`的类型使用`auto`类型推导,所以使用[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md)中的规则。规则上说非引用非指针类型的声明(就是`y`的情况),`const`和`volatile`限定符被拿掉。`y`的类型因此仅仅是`int`。这意味着对`y`的冗余读取和写入可以被消除。在例子中,编译器必须执行对`y`的初始化和赋值两个语句,因为`x`是`volatile`的,所以第二次对`x`的读取可能会产生一个与第一次不同的值。)
|
||||
|
||||
在处理特殊内存时,必须保留看似多余的读取或者无效存储的事实,顺便说明了为什么`std::atomic`不适合这种场景。`std::atomic`类型允许编译器消除此类冗余操作。代码的编写方式与使用`volatile`的方式完全不同,但是如果我们暂时忽略它,只关注编译器执行的操作,则可以说,
|
||||
在处理特殊内存时,必须保留看似冗余访问和无用存储的事实,顺便说明了为什么`std::atomic`不适合这种场景。编译器被允许消除对`std::atomic`的冗余操作。代码的编写方式与`volatile`那些不那么相同,但是如果我们暂时忽略它,只关注编译器执行的操作,则概念上可以说,编译器看到这个,
|
||||
|
||||
```cpp
|
||||
std::atomic<int> x;
|
||||
auto y = x; // conceptually read x (see below)
|
||||
y = x; // conceptually read x again(see below)
|
||||
auto y = x; //概念上会读x(见下)
|
||||
y = x; //概念上会再次读x(见下)
|
||||
|
||||
x = 10; // write x
|
||||
y = 20; // write x again
|
||||
x = 10; //写x
|
||||
x = 20; //再次写x
|
||||
```
|
||||
|
||||
原则上,编译器可能会优化为:
|
||||
会优化为:
|
||||
|
||||
```cpp
|
||||
auto y = x; // conceptually read x
|
||||
x = 20; // write x
|
||||
auto y = x; //概念上会读x(见下)
|
||||
x = 20; //写x
|
||||
```
|
||||
|
||||
对于特殊内存,显然这是不可接受的。
|
||||
|
||||
现在,就当他没有优化了,但是对于x是`std::atomic<int>`类型来说,下面的两条语句都编译不通过。
|
||||
现在,就像下面所发生的,当`x`是`std::atomic`时,这两条语句都无法编译通过:
|
||||
|
||||
```cpp
|
||||
auto y = x; // error
|
||||
y = x; // error
|
||||
auto y = x; //错误
|
||||
y = x; //错误
|
||||
```
|
||||
|
||||
这是因为`std::atomic`类型的拷贝操作是被删除的(参见[Item11](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item11.md))。因为有个很好的理由删除。想象一下如果`y`使用`x`来初始化会发生什么。因为`x`是`std::atomic`类型,`y`的类型被推导为`std::atomic`(参见[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md))。我之前说了`std::atomic`最好的特性之一就是所有成员函数都是原子性的,但是为了使从`x`拷贝初始化`y`的过程是原子性的,编译器不得不生成代码,把读取`x`和写入`y`放在一个单独的原子性操作中。硬件通常无法做到这一点,因此`std::atomic`不支持拷贝构造。出于同样的原因,拷贝赋值也被删除了,这也是为什么从`x`赋值给`y`也编译失败。(移动操作在`std::atomic`没有显式声明,因此根据[Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)中描述的规则来看,`std::atomic`不支持移动构造和移动赋值)。
|
||||
|
||||
|
||||
这是因为`std::atomic`类型的拷贝操作时被删除的(参见Item 11)。想象一下如果y使用x来初始化会发生什么。因为x是`std::atomic`类型,y的类型被推导为`std::atomic`(参见Item 2)。我之前说了`std::atomic`最好的特性之一就是所有成员函数都是原子的,但是为了执行从x到y的拷贝初始化是原子的,编译器不得不生成读取x和写入x为原子的代码。硬件通常无法做到这一点,因此`std::atomic`不支持拷贝构造。处于同样的原因,拷贝赋值也被delete了,这也是为什么从x赋值给y也编译失败。(移动操作在`std::atomic`没有显式声明,因此对于Item 17中描述的规则来看,`std::atomic`既不提移动构造器也不提供移动赋值能力)。
|
||||
|
||||
可以将x的值传递给y,但是需要使用`std::atomic`的`load和store`成员函数。`load`函数原子读取,`store`原子写入。要使用x初始化y,然后将x的值放入y,代码应该这样写:
|
||||
可以将`x`的值传递给`y`,但是需要使用`std::atomic`的`load`和`store`成员函数。`load`函数原子性地读取,`store`原子性地写入。要使用`x`初始化`y`,然后将`x`的值放入`y`,代码应该这样写:
|
||||
|
||||
```cpp
|
||||
std::atomic<int> y(x.load());
|
||||
y.store(x.load());
|
||||
std::atomic<int> y(x.load()); //读x
|
||||
y.store(x.load()); //再次读x
|
||||
```
|
||||
|
||||
这可以编译,但是可以清楚看到不是整条语句原子,而是读取写入分别原子化执行。
|
||||
这可以编译,读取`x`(通过`x.load()`)是与初始化或者存储到`y`相独立的函数,这个事实清楚地表明没理由期待上面的任何一个语句会在单独的原子性的操作中整体执行。
|
||||
|
||||
给出的代码,编译器可以通过存储x的值到寄存器代替读取两次来“优化”:
|
||||
给出上面的代码,编译器可以通过存储x的值到寄存器代替读取两次来“优化”:
|
||||
|
||||
```cpp
|
||||
register = x.load(); // read x into register
|
||||
std::atomic<int> y(register); // init y with register value
|
||||
y.store(register); // store register value into y
|
||||
register = x.load(); //把x读到寄存器
|
||||
std::atomic<int> y(register); //使用寄存器值初始化y
|
||||
y.store(register); //把寄存器值存储到y
|
||||
```
|
||||
|
||||
结果如你所见,仅读取x一次,这是对于特殊内存必须避免的优化(这种优化不允许对`volatile`类型值执行)。
|
||||
结果如你所见,仅读取`x`一次,这是对于特殊内存必须避免的优化。(这种优化是不允许对`volatile`类型变量执行的。)
|
||||
|
||||
事情越辩越明:
|
||||
因此情况很明显:
|
||||
|
||||
- `std::atomic`用在并发程序中
|
||||
- `volatile`用于特殊内存场景
|
||||
- `std::atomic`用在并发编程中,对访问特殊内存没用。
|
||||
- `volatile`用于访问特殊内存,对并发编程没用。
|
||||
|
||||
因为`std::atomic`和`volatile`用于不同的目的,所以可以结合起来使用:
|
||||
|
||||
```cpp
|
||||
volatile std::atomic<int> vai; // operations on vai are atomic and can't be optimized away
|
||||
volatile std::atomic<int> vai; //对vai的操作是原子性的,且不能被优化掉
|
||||
```
|
||||
|
||||
这可以用在比如`vai`变量关联了memory-mapped I/O内存并且用于并发程序的场景。
|
||||
如果`vai`变量关联了内存映射I/O的位置,被多个线程并发访问,这会很有用。
|
||||
|
||||
最后一点,一些开发者尤其喜欢使用`std::atomic`的`load`和`store`函数即使不必要时,因为这在代码中显式表明了这个变量不“正常”。强调这一事实并非没有道理。因为访问`std::atomic`确实会更慢一些,我们也看到了`std::atomic`会阻止编译器对代码执行顺序重排。调用`load`和`store`可以帮助识别潜在的可扩展性瓶颈。从正确性的角度来看,没有看到在一个变量上调用`store`来与其他线程进行通信(比如flag表示数据的可用性)可能意味着该变量在声明时没有使用`std::atomic`。这更多是习惯问题,但是,一定要知道`atomic`和`volatile`的巨大不同。
|
||||
最后一点,一些开发者在即使不必要时也尤其喜欢使用`std::atomic`的`load`和`store`函数,因为这在代码中显式表明了这个变量不“正常”。强调这一事实并非没有道理。因为访问`std::atomic`确实会比non-`std::atomic`更慢一些,我们也看到了`std::atomic`会阻止编译器对代码执行一些特定的,本应被允许的顺序重排。调用`load`和`store`可以帮助识别潜在的可扩展性瓶颈。从正确性的角度来看,**没有**看到在一个变量上调用`store`来与其他线程进行通信(比如用个flag表示数据的可用性)可能意味着该变量在声明时本应使用而没有使用`std::atomic`。
|
||||
|
||||
这更多是习惯问题,但是,一定要知道`atomic`和`volatile`的巨大不同。
|
||||
|
||||
### 必须记住的事
|
||||
|
||||
- `std::atomic`是用在不使用锁,来使变量被多个线程访问。是用来编写并发程序的
|
||||
- `volatile`是用在特殊内存的场景中,避免被编译器优化内存。
|
||||
- `std::atomic`用于在不使用互斥锁情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具。
|
||||
- `volatile`用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具。
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@ -1,156 +1,154 @@
|
||||
# CHAPTER8 Tweaks
|
||||
# 第8章 微调
|
||||
|
||||
-------
|
||||
**CHAPTER 8 Tweaks**
|
||||
|
||||
对于C++中的通用技术,总是存在适用场景。除了本章覆盖的两个例外,描述什么场景使用哪种通用技术通常来说很容易。这两个例外是传值(pass by value)和 emplacement。决定何时使用这两种技术受到多种因素的影响,本书提供的最佳建议是在使用它们的同时仔细考虑清楚,尽管它们都是高效的现代C++编程的重要角色。接下来的Items提供了是否使用它们来编写软件的所需信息。
|
||||
对于C++中的通用技术和特性,总是存在适用和不适用的场景。除了本章覆盖的两个例外,描述什么场景使用哪种通用技术通常来说很容易。这两个例外是传值(pass by value)和安置(emplacement)。决定何时使用这两种技术受到多种因素的影响,本书提供的最佳建议是在使用它们的同时仔细考虑清楚,尽管它们都是高效的现代C++编程的重要角色。接下来的条款提供了使用它们来编写软件是否合适的所需信息。
|
||||
|
||||
## 条款四十一:对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
|
||||
|
||||
**Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied**
|
||||
|
||||
## Item41.Consider pass by value for copyable parameters that are cheap to move and always copied 如果参数可拷贝并且移动操作开销很低,总是考虑直接按值传递
|
||||
|
||||
有些函数的参数是可复制的。比如说,`addName`成员函数可以拷贝自己的参数到一个私有容器。为了提高效率,应该拷贝左值,移动右值。
|
||||
有些函数的形参是可拷贝的。(在本条款中,“拷贝”一个形参通常意思是,将形参作为拷贝或移动操作的源对象。[简介](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/Introduction.md)中提到,C++没有术语来区分拷贝操作得到的副本和移动操作得到的副本。)比如说,`addName`成员函数可以拷贝自己的形参到一个私有容器。为了提高效率,应该拷贝左值,移动右值。
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
void addName(const std::string& newName) {
|
||||
names.push_back(newName);
|
||||
}
|
||||
void addName(std::string&& newName) {
|
||||
names.push_back(std::move(newName));
|
||||
}
|
||||
...
|
||||
void addName(const std::string& newName) //接受左值;拷贝它
|
||||
{ names.push_back(newName); }
|
||||
|
||||
void addName(std::string&& newName) //接受右值;移动它;std::move
|
||||
{ names.push_back(std::move(newName)); } //的使用见条款25
|
||||
…
|
||||
private:
|
||||
std::vector<std::string> names;
|
||||
std::vector<std::string> names;
|
||||
};
|
||||
```
|
||||
|
||||
这是可行的,但是需要编写两个同名异参函数,这有点让人难受:两个函数声明,两个函数实现,两个函数文档,两个函数的维护。唉。
|
||||
这是可行的,但是需要编写两个函数来做本质相同的事。这有点让人难受:两个函数声明,两个函数实现,两个函数写说明,两个函数的维护。唉。
|
||||
|
||||
此外,你可能会担心程序的目标代码的空间占用,当函数都内联(inlined)的时候,会避免同时两个函数同时存在导致的代码膨胀问题,但是一旦存在没有被内联(inlined),目标代码就是出现两个函数。
|
||||
此外,目标代码中会有两个函数——你可能会担心程序的空间占用。这种情况下,两个函数都可能内联,可能会避免同时两个函数同时存在导致的代码膨胀问题,但是一旦没有被内联,目标代码就会出现两个函数。
|
||||
|
||||
另一种方法是使`addName`函数成为具有通用引用的函数模板:(参考Item24)
|
||||
另一种方法是使`addName`函数成为具有通用引用的函数模板(参考[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)):
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
template<typename T>
|
||||
void addName(T&& newName) {
|
||||
names.push_back(std::forward<T>(newName));
|
||||
}
|
||||
...
|
||||
template<typename T> //接受左值和右值;
|
||||
void addName(T&& newName) { //拷贝左值,移动右值;
|
||||
names.push_back(std::forward<T>(newName)); //std::forward的使用见条款25
|
||||
}
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
这减少了源代码的维护工作,但是通用引用会导致其他复杂性。作为模板,`addName`的实现必须放置在头文件中。在编译器展开的时候,可能会不止为左值和右值实例化为多个函数,也可能为`std::string`和可转换为`std::string`的类型分别实例化为多个函数(参考Item25)。同时有些参数类型不能通过通用引用传递(参考Item30),而且如果传递了不合法的参数类型,编译器错误会令人生畏。(参考Item27)
|
||||
这减少了源代码的维护工作,但是通用引用会导致其他复杂性。作为模板,`addName`的实现必须放置在头文件中。在编译器展开的时候,可能会产生多个函数,因为不止为左值和右值实例化,也可能为`std::string`和可转换为`std::string`的类型分别实例化为多个函数(参考[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md))。同时有些实参类型不能通过通用引用传递(参考[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)),而且如果传递了不合法的实参类型,编译器错误会令人生畏(参考[Item27](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item27.md))。
|
||||
|
||||
是否存在一种编写`addName`的方法(左值拷贝,右值移动),而且源代码和目标代码中都只有一个函数,避免使用通用模板这种特性?答案是是的。你要做的就是放弃你学习C++编程的第一条规则,就是用户定义的对象避免传值。像是`addName`函数中的`newName`参数,按值传递可能是一种完全合理的策略。
|
||||
是否存在一种编写`addName`的方法,可以左值拷贝,右值移动,只用处理一个函数(源代码和目标代码中),且避免使用通用引用?答案是是的。你要做的就是放弃你学习C++编程的第一条规则。这条规则是避免在传递用户定义的对象时使用传值方式。像是`addName`函数中的`newName`形参,按值传递可能是一种完全合理的策略。
|
||||
|
||||
在我们讨论为什么对于`addName`中的`newName`参数按值传递非常合理之前,让我们来考虑如下实现:
|
||||
在我们讨论为什么对于`addName`中的`newName`按值传递非常合理之前,让我们来考虑该会怎样实现:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
void addName(std::string newName) {
|
||||
names.push_back(std::move(newName));
|
||||
}
|
||||
...
|
||||
void addName(std::string newName) { //接受左值或右值;移动它
|
||||
names.push_back(std::move(newName));
|
||||
}
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
该代码唯一可能令人困惑的部分就是`std::move`这里。`std::move`典型的应用场景是用在右值引用,但是在这里,我们了解到的信息:(1)`newName`是完全复制的传递进来的对象,换句话说,改变不会影响原值;(2)`newName`的最终用途就在这个函数里,不会再做他用,所以移动它不会影响其他代码。
|
||||
该代码唯一可能令人困惑的部分就是对形参`newName`使用`std::move`。`std::move`典型的应用场景是用在右值引用,但是在这里,我们了解到:(1)`newName`是,与调用者传进来的对象相完全独立的,一个对象,所以改变`newName`不会影响到调用者;(2)这就是`newName`的最后一次使用,所以移动它不会影响函数内此后的其他代码。
|
||||
|
||||
事实就是我们只编写了一个`addName`函数,避免了源代码和目标代码的重复。我们没有使用通用引用的特性,不会导致头文件膨胀,odd failure cases(这里不知道咋翻译),或者令人困惑的错误问题(编译)。但是这种设计的效率如何呢?按值传值会不会开销很大?
|
||||
只有一个`addName`函数的事实,解释了在源代码和目标代码中,我们怎样避免了代码重复。我们没有使用通用引用,不会导致头文件膨胀、奇怪的失败情况、或者令人困惑的编译错误消息。但是这种设计的效率如何呢?我们在按值传递哎,会不会开销很大?
|
||||
|
||||
在C++98中,可以肯定的是,无论调用者如何调用,参数`newName`都是拷贝传递。但是在C++11中,`addName`就是左值拷贝,右值移动,来看如下例子:
|
||||
在C++98中,可以肯定确实开销会大。无论调用者传入什么,形参`newName`都是由拷贝构造出来。但是在C++11中,只有在左值实参情况下,`addName`被拷贝构造出来;对于右值,它会被移动构造。来看如下例子:
|
||||
|
||||
```cpp
|
||||
Widget w;
|
||||
...
|
||||
…
|
||||
std::string name("Bart");
|
||||
w.addName(name); // call addName with lvalue
|
||||
...
|
||||
w.addName(name + "Jenne"); // call addName with rvalue
|
||||
w.addName(name); //使用左值调用addName
|
||||
…
|
||||
w.addName(name + "Jenne"); //使用右值调用addName(见下)
|
||||
```
|
||||
|
||||
第一处调用,`addName`的参数是左值,因此是拷贝构造参数,就像在C++98中一样。第二处调用,参数是一个临时值,是一个右值,因此`newName`的参数是移动构造的。
|
||||
第一处调用`addName`(当`name`被传递时),形参`newName`是使用左值被初始化。`newName`因此是被拷贝构造,就像在C++98中一样。第二处调用,`newName`使用`std::string`对象被初始化,这个`std::string`对象是调用`std::string`的`operator+`(即*append*操作)得到的结果。这个对象是一个右值,因此`newName`是被移动构造的。
|
||||
|
||||
就像我们想要的那样,左值拷贝,右值移动,优雅吧?
|
||||
|
||||
优雅,但是要牢记一些警示,回顾一下我们考虑过的三个版本的`addName`:
|
||||
优雅,但是要牢记一些警示,回顾一下我们考虑过的三个版本的`addName`:
|
||||
|
||||
```cpp
|
||||
class Widget { // Approach 1
|
||||
class Widget { //方法1:对左值和右值重载
|
||||
public:
|
||||
void addName(const std::string& newName) {
|
||||
names.push_back(newName);
|
||||
}
|
||||
void addName(std::string&& newName) {
|
||||
names.push_back(std::move(newName));
|
||||
}
|
||||
...
|
||||
void addName(const std::string& newName)
|
||||
{ names.push_back(newName); } // rvalues
|
||||
void addName(std::string&& newName)
|
||||
{ names.push_back(std::move(newName)); }
|
||||
…
|
||||
private:
|
||||
std::vector<std::string> names;
|
||||
std::vector<std::string> names;
|
||||
};
|
||||
|
||||
class Widget { // Approach 2
|
||||
class Widget { //方法2:使用通用引用
|
||||
public:
|
||||
template<typename T>
|
||||
void addName(T&& newName) {
|
||||
names.push_back(std::forward<T>(newName));
|
||||
}
|
||||
...
|
||||
template<typename T>
|
||||
void addName(T&& newName)
|
||||
{ names.push_back(std::forward<T>(newName)); }
|
||||
…
|
||||
};
|
||||
|
||||
class Widget { // Approach 3
|
||||
class Widget { //方法3:传值
|
||||
public:
|
||||
void addName(std::string newName) {
|
||||
names.push_back(std::move(newName));
|
||||
}
|
||||
...
|
||||
void addName(std::string newName)
|
||||
{ names.push_back(std::move(newName)); }
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
本书将前两个版本称为“按引用方法”,因为都是通过引用传递参数,仍然考虑这两种调用方式:
|
||||
我将前两个版本称为“按引用方法”,因为都是通过引用传递形参。
|
||||
|
||||
仍然考虑这两种调用方式:
|
||||
|
||||
```cpp
|
||||
Widget w;
|
||||
...
|
||||
…
|
||||
std::string name("Bart");
|
||||
w.addName(name); // call addName with lvalue
|
||||
...
|
||||
w.addName(name + "Jenne"); // call addName with rvalue
|
||||
w.addName(name); //传左值
|
||||
…
|
||||
w.addName(name + "Jenne"); //传右值
|
||||
```
|
||||
|
||||
现在分别考虑三种实现中,两种调用方式,拷贝和移动操作的开销。会忽略编译器对于移动和拷贝操作的优化。
|
||||
现在分别考虑三种实现中,给`Widget`添加一个名字的两种调用方式,拷贝和移动操作的开销。会忽略编译器对于移动和拷贝操作的优化,因为这些优化是与上下文和编译器有关的,实际上不会改变我们分析的本质。
|
||||
|
||||
- **Overloading(重载)**:无论传递左值还是传递右值,调用都会绑定到一种`newName`的引用实现方式上。拷贝和复制零开销。左值重载中,`newName`拷贝到`Widget::names`中,右值重载中,移动进去。开销总结:左值一次拷贝,右值一次移动。
|
||||
- **Using a universal reference(通用模板方式)**:同重载一样,调用也绑定到`addName`的引用实现上,没有开销。由于使用了`std::forward`,左值参数会复制到`Widget::names`,右值参数移动进去。开销总结同重载方式。
|
||||
Item25 解释了如果调用者传递的参数不是`std::string`类型,将会转发到`std::string`的构造函数(几乎是零开销的拷贝或者移动操作)。因此通用引用的方式同样有同样效率,所以者不影响本次分析,简单分析`std::string`参数类型即可。
|
||||
- **Passing by value(按值传递)**:无论传递左值还是右值,都必须构造`newName`参数。如果传递的是左值,需要拷贝的开销,如果传递的是右值,需要移动的开销。在函数的实现中,`newName`总是采用移动的方式到`Widget::names`。开销总结:左值参数,一次拷贝一次移动,右值参数两次移动。对比按引动传递的方法,对于左值或者右值,均多出一次移动操作。
|
||||
- **重载**:无论传递左值还是传递右值,调用都会绑定到一个叫`newName`的引用上。从拷贝和移动操作方面看,这个过程零开销。左值重载中,`newName`拷贝到`Widget::names`中,右值重载中,移动进去。开销总结:左值一次拷贝,右值一次移动。
|
||||
- **使用通用引用**:同重载一样,调用也绑定到`addName`这个引用上,没有开销。由于使用了`std::forward`,左值`std::string`实参会拷贝到`Widget::names`,右值`std::string`实参移动进去。对`std::string`实参来说,开销同重载方式一样:左值一次拷贝,右值一次移动。
|
||||
|
||||
Item25 解释了如果调用者传递的实参不是`std::string`类型,将会转发到`std::string`的构造函数,几乎也没有`std::string`拷贝或者移动操作。因此通用引用的方式有同样效率,所以这不影响本次分析,简单假定调用者总是传入`std::string`类型实参即可。
|
||||
- **按值传递**:无论传递左值还是右值,都必须构造`newName`形参。如果传递的是左值,需要拷贝的开销,如果传递的是右值,需要移动的开销。在函数的实现中,`newName`总是采用移动的方式到`Widget::names`。开销总结:左值实参,一次拷贝一次移动,右值实参两次移动。对比按引用传递的方法,对于左值或者右值,均多出一次移动操作。
|
||||
|
||||
再次回顾本Item的内容:
|
||||
|
||||
```
|
||||
总是考虑直接按值传递,如果参数可拷贝并且移动操作开销很低
|
||||
```
|
||||
> 对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
|
||||
|
||||
这样措辞是有原因的:
|
||||
|
||||
1. 应该仅*consider using pass by value*。是的,因为只需要编写一个函数,同时只会在目标代码中生成一个函数。避免了通用引用方式的种种问题。但是毕竟开销会更高,而且下面还会讨论,还会存在一些目前我们并未讨论到的开销。
|
||||
这样措辞是有原因的。实际上四个原因:
|
||||
|
||||
2. 仅考虑对于*copable parameters*按值传递。不符合此条件的的参数必须只有移动构造函数。回忆一下“重载”方案的问题,就是必须编写两个函数来分别处理左值和右值,如果参数没有拷贝构造函数,那么只需要编写右值参数的函数,重载方案就搞定了。
|
||||
考虑一下`std::unique_ptr<std::string>`的数据成员和其`set`函数。因为`std::unique_ptr`是仅可移动的类型,所以考虑使用“重载”方式编写即可:
|
||||
1. 应该仅**考虑**使用传值方式。确实,只需要编写一个函数。确实,只会在目标代码中生成一个函数。确实,避免了通用引用的种种问题。但是毕竟开销会比那些替代方式更高(译者注:指接受引用的两种实现方式),而且下面还会讨论,还会存在一些目前我们并未讨论到的开销。
|
||||
|
||||
2. 仅考虑对于**可拷贝形参**使用按值传递。不符合此条件的的形参必须有只可移动的类型(*move-only types*)(的数据成员),因为函数总是会做副本(译注:指的是传值时形参总是实参的一个副本),而如果它们不可拷贝,副本就必须通过移动构造函数创建。(这样的句子就说明有一个术语来区分拷贝操作制作的副本,和移动操作制作的副本,是非常好的。)回忆一下传值方案比“重载”方案的优势在于,仅有一个函数要写。但是对于只可移动类型,没必要为左值实参提供重载,因为拷贝左值需要拷贝构造函数,只可移动类型的拷贝构造函数是禁用的。那意味着只需要支持右值实参,“重载”方案只需要一个重载函数:接受右值引用的函数。
|
||||
|
||||
考虑一个类:有个`std::unique_ptr<std::string>`的数据成员,对它有个赋值器(*setter*)。`std::unique_ptr`是只可移动的类型,所以赋值器的“重载”方式只有一个函数:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
...
|
||||
void setPtr(std::unique_ptr<std::string>&& ptr) {
|
||||
p = std::move(ptr);
|
||||
}
|
||||
…
|
||||
void setPtr(std::unique_ptr<std::string>&& ptr)
|
||||
{ p = std::move(ptr); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<std::string> p;
|
||||
std::unique_ptr<std::string> p;
|
||||
};
|
||||
```
|
||||
|
||||
@ -158,60 +156,63 @@ w.addName(name + "Jenne"); // call addName with rvalue
|
||||
|
||||
```cpp
|
||||
Widget w;
|
||||
...
|
||||
…
|
||||
w.setPtr(std::make_unique<std::string>("Modern C++"));
|
||||
```
|
||||
|
||||
这样,传递给`setPtr`的参数就是右值,整体开销就是一次移动。如果使用传值方式编写:
|
||||
这样,从`std::make_unique`返回的右值`std::unique_ptr<std::string>`通过右值引用被传给`setPtr`,然后移动到数据成员`p`中。整体开销就是一次移动。
|
||||
|
||||
如果`setPtr`使用传值方式接受形参:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
…
|
||||
void setPtr(std::unique_ptr<std::string> ptr)
|
||||
{ p = std::move(ptr); }
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
同样的调用就会先移动构造`ptr`形参,然后`ptr`再移动赋值到数据成员`p`,整体开销就是两次移动——是“重载”方法开销的两倍。
|
||||
|
||||
3. 按值传递应该仅考虑那些**移动开销小**的形参。当移动的开销较低,额外的一次移动才能被开发者接受,但是当移动的开销很大,执行不必要的移动就类似执行一个不必要的拷贝,而避免不必要的拷贝的重要性就是最开始C++98规则中避免传值的原因!
|
||||
|
||||
4. 你应该只对**总是被拷贝**的形参考虑按值传递。为了看清楚为什么这很重要,假定在拷贝形参到`names`容器前,`addName`需要检查新的名字的长度是否过长或者过短,如果是,就忽略增加名字的操作:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
...
|
||||
void setPtr(std::unique_ptr<std::string> ptr) {
|
||||
p = std::move(ptr);
|
||||
}
|
||||
private:
|
||||
std::unique_ptr<std::string> p;
|
||||
};
|
||||
```
|
||||
|
||||
同样的调用就会先使用移动构造函数移动到参数`ptr`,然后再移动到`p`,整体开销就是两次移动。
|
||||
|
||||
3. 按值传递应该仅应用于哪些*cheap to move*的参数。当移动的开销较低,额外的一次移动才能被开发者接受,但是当移动的开销很大,执行不必要的移动类似不必要的复制时,这个规则就不适用了。
|
||||
|
||||
4. 你应该只对*always copied(肯定复制)*的参数考虑按值传递。为了看清楚为什么这很重要,假定在复制参数到`names`容器前,`addName`需要检查参数的长度是否过长或者过短,如果是,就忽略增加`name`的操作:
|
||||
|
||||
```cpp
|
||||
class Widget { // Approach 3
|
||||
public:
|
||||
void addName(std::string newName) {
|
||||
if ((newName.length() >= minLen) && (newName.length() <= maxLen)) {
|
||||
names.push_back(std::move(newName));
|
||||
void addName(std::string newName)
|
||||
{
|
||||
if ((newName.length() >= minLen) &&
|
||||
(newName.length() <= maxLen))
|
||||
{
|
||||
names.push_back(std::move(newName));
|
||||
}
|
||||
}
|
||||
}
|
||||
...
|
||||
…
|
||||
private:
|
||||
std::vector<std::string> names;
|
||||
std::vector<std::string> names;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
即使这个函数没有在`names`添加任何内容,也增加了构造和销毁`newName`的开销,而按引用传递会避免这笔开销。
|
||||
|
||||
即使你编写的函数是移动开销小的参数而且无条件复制,有时也可能不适合按值传递。这是因为函数复制参数存在两种方式:一种是通过构造函数(拷贝构造或者移动构造),还有一种是赋值(拷贝赋值或者移动赋值)。`addName`使用构造函数,它的参数传递给`vector::push_back`,在这个函数内部,`newName`是通过构造函数在`std::vector`创建一个新元素。对于使用构造函数拷贝参数的函数,上述分析已经可以给出最终结论:按值传递对于左值和右值均增加了一次移动操作的开销。
|
||||
即使你编写的函数对可拷贝类型执行无条件的复制,且这个类型移动开销小,有时也可能不适合按值传递。这是因为函数拷贝一个形参存在两种方式:一种是通过构造函数(拷贝构造或者移动构造),还有一种是赋值(拷贝赋值或者移动赋值)。`addName`使用构造函数,它的形参`newName`传递给`vector::push_back`,在这个函数内部,`newName`是通过拷贝构造在`std::vector`末尾创建一个新元素。对于使用构造函数拷贝形参的函数,之前的分析已经可以给出最终结论:按值传递对于左值和右值均增加了一次移动操作的开销。
|
||||
|
||||
当参数通过赋值操作进行拷贝时,分析起来更加复杂。比如,我们有一个表征密码的类,因为密码可能会被修改,我们提供了`setter`函数`changeTo`。用按值传递的策略,我们实现一个密码类如下:
|
||||
当形参通过赋值操作进行拷贝时,分析起来更加复杂。比如,我们有一个表征密码的类,因为密码可能会被修改,我们提供了赋值器函数`changeTo`。用按值传递的策略,我们实现一个`Password`类如下:
|
||||
|
||||
```cpp
|
||||
class Password {
|
||||
public:
|
||||
explicit Password(std::string pwd) : text(std::move(pwd)) {}
|
||||
void changeTo(std::string newPwd) {
|
||||
text = std::move(newPwd);
|
||||
}
|
||||
...
|
||||
explicit Password(std::string pwd) //传值
|
||||
: text(std::move(pwd)) {} //构造text
|
||||
void changeTo(std::string newPwd) //传值
|
||||
{ text = std::move(newPwd); } //赋值text
|
||||
…
|
||||
private:
|
||||
std::string text;
|
||||
std::string text; //密码的text
|
||||
};
|
||||
```
|
||||
|
||||
@ -222,71 +223,69 @@ std::string initPwd("Supercalifragilisticexpialidocious");
|
||||
Password p(initPwd);
|
||||
```
|
||||
|
||||
`p.text`被给定的密码构造,用按值传递的方式增加了一次移动操作的开销相对于重载或者通用引用,但是这无关紧要,一切看起来如此美好。
|
||||
毫无疑问:`p.text`被给定的密码构造,在构造函数用按值传递的方式增加了一次移动构造的开销,如果使用重载或者通用引用就会消除这个开销。一切都还好。
|
||||
|
||||
但是,该程序的用户可能对初始密码不太满意,因为这段密码`"Supercalifragilisticexpialidocious"`在许多字典中可以被发现。他或者她因此修改密码:
|
||||
但是,该程序的用户可能对这个密码不太满意,因为“Supercalifragilisticexpialidocious”可以在许多字典中找到。他或者她因此采取等价于如下代码的一些动作:
|
||||
|
||||
```cpp
|
||||
std::string newPassword = "Beware the Jabberwock";
|
||||
p.changeTo(newPassword);
|
||||
```
|
||||
|
||||
不用关心新密码是不是比就密码更好,那是用户关心的问题。我们对于`changeTo`函数的按值传递实现方案会导致开销大大增加。
|
||||
不用关心新密码是不是比旧密码更好,那是用户关心的问题。我们关心的是`changeTo`使用赋值来拷贝形参`newPwd`,可能导致函数的按值传递实现方案的开销大大增加。
|
||||
|
||||
传递给`changeTo`的参数是一个左值(`newPassword`),所以`newPwd`参数需要被构造,`std::string`的拷贝构造函数会被调用,这个函数会分配新的存储空间给新密码。`newPwd`会移动赋值到`text`,这会导致释放旧密码的内存。所以`changeTo`存在两次动态内存管理的操作:一次是为新密码创建内存,一次是销毁旧密码的内存。
|
||||
传递给`changeTo`的实参是一个左值(`newPassword`),所以`newPwd`形参被构造时,`std::string`的拷贝构造函数会被调用,这个函数会分配新的存储空间给新密码。`newPwd`会移动赋值到`text`,这会导致`text`本来持有的内存被释放。所以`changeTo`存在两次动态内存管理的操作:一次是为新密码创建内存,一次是销毁旧密码的内存。
|
||||
|
||||
但是在这个例子中,旧密码比新密码长度更长,所以本来不需要分配新内存,销毁就内存的操作。如果使用重载的方式,两次动态内存管理操作可以避免:
|
||||
但是在这个例子中,旧密码比新密码长度更长,所以不需要分配新内存,销毁旧内存的操作。如果使用重载的方式,有可能两次动态内存管理操作得以避免:
|
||||
|
||||
```cpp
|
||||
class Password {
|
||||
public:
|
||||
...
|
||||
void changeTo(std::string& newPwd) {
|
||||
text = newPwd;
|
||||
}
|
||||
...
|
||||
…
|
||||
void changeTo(std::string& newPwd) //对左值的重载
|
||||
{
|
||||
text = newPwd; //如果text.capacity() >= newPwd.size(),
|
||||
//可能重用text的内存
|
||||
}
|
||||
…
|
||||
private:
|
||||
std::string text;
|
||||
std::string text; //同上
|
||||
};
|
||||
```
|
||||
|
||||
这种情况下,按值传递的开销(包括了内存分配和内存销毁)可能会比`std::string`的`move`操作高出几个数量级。
|
||||
这种情况下,按值传递的开销包括了内存分配和内存销毁——可能会比`std::string`的移动操作高出几个数量级。
|
||||
|
||||
有趣的是,如果旧密码短于新密码,在赋值过程中就不可避免要重新分配内存,这种情况,按值传递跟按引用传递的效率是一样的。因此,参数的赋值操作开销取决于具体的参数的值,这种分析适用于动态分配内存的参数类型。
|
||||
有趣的是,如果旧密码短于新密码,在赋值过程中就不可避免要销毁、分配内存,这种情况,按值传递跟按引用传递的效率是一样的。因此,基于赋值的形参拷贝操作开销取决于具体的实参的值,这种分析适用于在动态分配内存中存值的形参类型。不是所有类型都满足,但是很多——包括`std::string`和`std::vector`——是这样。
|
||||
|
||||
这种潜在的开销增加仅在传递左值参数时才适用,因为执行内存分配和释放通常发生在复制操作中。
|
||||
这种潜在的开销增加仅在传递左值实参时才适用,因为执行内存分配和释放通常发生在真正的拷贝操作(即,不是移动)中。对右值实参,移动几乎就足够了。
|
||||
|
||||
结论是,使用按值传递的函数通过赋值复制一个参数的额外开销取决于传递的类型中左值和右值的比例,即这个值是否需要动态分配内存,以及赋值操作符的具体实现中对于内存的使用。对于`std::string`来说,取决于实现是否使用了小字符串优化(SSO 参考Item 29),如果是,值是否匹配SSO缓冲区。
|
||||
结论是,使用通过赋值拷贝一个形参进行按值传递的函数的额外开销,取决于传递的类型,左值和右值的比例,这个类型是否需要动态分配内存,以及,如果需要分配内存的话,赋值操作符的具体实现,还有赋值目标占的内存至少要跟赋值源占的内存一样大。对于`std::string`来说,开销还取决于实现是否使用了小字符串优化(SSO——参考[Item29](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item29.md)),如果是,那么要赋值的值是否匹配SSO缓冲区。
|
||||
|
||||
所以,正如我所说,当参数通过赋值进行拷贝时,分析按值传递的开销是复杂的。通常,最有效的经验就是“在证明没问题之前假设有问题”,就是除非已证明按值传递会为你需要的参数产生可接受开销的执行效率,否则使用重载或者通用引用的实现方式。
|
||||
所以,正如我所说,当形参通过赋值进行拷贝时,分析按值传递的开销是复杂的。通常,最有效的经验就是“在证明没问题之前假设有问题”,就是除非已证明按值传递会为你需要的形参类型产生可接受的执行效率,否则使用重载或者通用引用的实现方式。
|
||||
|
||||
到此为止,对于需要运行尽可能快的软件来说,按值传递可能不是一个好策略,因为毕竟多了一次移动操作。此外,有时并不能知道是不是还多了其他开销。在`Widget::addName`例子中,按值传递仅多了一次移动操作,但是如果加入值的一些校验,可能按值传递就多了创建和销毁类型的开销相对于重载和通用引用的实现方式。
|
||||
到此为止,对于需要运行尽可能快的软件来说,按值传递可能不是一个好策略,因为避免即使开销很小的移动操作也非常重要。此外,有时并不能清楚知道会发生多少次移动操作。在`Widget::addName`例子中,按值传递仅多了一次移动操作,但是如果`Widget::addName`调用了`Widget::validateName`,这个函数也是按值传递。(假定它有理由总是拷贝它的形参,比如把验证过的所有值存在一个数据结构中。)并假设`validateName`调用了第三个函数,也是按值传递……
|
||||
|
||||
可以看到导致的方向,在调用链中,每次调用多了一次移动的开销,那么当调用链较长,总体就会产生无法忍受的开销,通过引用传递,调用链不会增加任何开销。
|
||||
可以看到这将会通向何方。在调用链中,每个函数都使用传值,因为“只多了一次移动的开销”,但是整个调用链总体就会产生无法忍受的开销,通过引用传递,调用链不会增加这种开销。
|
||||
|
||||
跟性能无关,总是需要考虑的是,按值传递不像按引用传递那样,会收到切片问题的影响。这是C++98的问题,在此不在详述,但是如果要设计一个函数,来处理这样的参数:基类或者其派生类,如果不想声明为按值传递,因为你就是要分割派生类型
|
||||
跟性能无关,但总是需要考虑的是,按值传递不像按引用传递那样会收到切片问题的影响。这是C++98的事,在此不在详述,但是如果要设计一个函数,来处理这样的形参:基类**或者任何其派生类**,你肯定不想声明一个那个类型的传值形参,因为你会“切掉”传入的任意派生类对象的派生类特征:
|
||||
|
||||
```cpp
|
||||
class Widget{...};
|
||||
class SpecialWidget: public Widget{...};
|
||||
void processWidget(Widget w);
|
||||
...
|
||||
SecialWidget sw;
|
||||
...
|
||||
processWidget(sw);
|
||||
class Widget { … }; //基类
|
||||
class SpecialWidget: public Widget { … }; //派生类
|
||||
void processWidget(Widget w); //对任意类型的Widget的函数,包括派生类型
|
||||
… //遇到对象切片问题
|
||||
SpecialWidget sw;
|
||||
…
|
||||
processWidget(sw); //processWidget看到的是Widget,
|
||||
//不是SpecialWidget!
|
||||
```
|
||||
|
||||
如果不熟悉**slicing problem**,可以先通过搜索引擎了解一下。这样你就知道切片问题是另一个C++98中默认按值传递名声不好的原因。有充分的理由来说明为什么你学习C++编程的第一件事就是避免用户自定义类型进行按值传递。
|
||||
如果不熟悉对象切片问题,可以先通过搜索引擎了解一下。这样你就知道切片问题是C++98中默认按值传递名声不好的另一个原因(要在效率问题的原因之上)。有充分的理由来说明为什么你学习C++编程的第一件事就是避免用户自定义类型进行按值传递。
|
||||
|
||||
C++11没有从根本上改变C++98按值传递的基本盘,通常,按值传递仍然会带来你希望避免的性能下降,而且按值传递会导致切片问题。C++11中新的功能是区分了左值和右值,实现了可移动类型的移动语义,尽管重载和通用引用都有其缺陷。对于特殊的场景,复制参数,总是会被拷贝,而且移动开销小的函数,可以按值传递,这种场景通常也不会有切片问题,这时,按值传递就提供了一种简单的实现方式,同时实现了接近引用传递的开销的效率。
|
||||
C++11没有从根本上改变C++98对于按值传递的智慧。通常,按值传递仍然会带来你希望避免的性能下降,而且按值传递会导致切片问题。C++11中新的功能是区分了左值和右值实参。利用对于可拷贝类型的右值的移动语义,需要重载或者通用引用,尽管两者都有其缺陷。对于特殊的场景,可拷贝且移动开销小的类型,传递给总是会拷贝他们的一个函数,并且切片也不需要考虑,这时,按值传递就提供了一种简单的实现方式,效率接近传递引用的函数,但是避免了传引用方案的缺点。
|
||||
|
||||
## 需要记住的事
|
||||
|
||||
- 对于可复制,移动开销低,而且无条件复制的参数,按值传递效率基本与按引用传递效率一致,而且易于实现,生成更少的目标代码
|
||||
- 通过构造函数拷贝参数可能比通过赋值拷贝开销大的多
|
||||
- 按值传递会引起切片问题,所说不适合基类类型的参数
|
||||
|
||||
|
||||
|
||||
|
||||
**请记住:**
|
||||
|
||||
- 对于可拷贝,移动开销低,而且无条件被拷贝的形参,按值传递效率基本与按引用传递效率一致,而且易于实现,还生成更少的目标代码。
|
||||
- 通过构造拷贝形参可能比通过赋值拷贝形参开销大的多。
|
||||
- 按值传递会引起切片问题,所说不适合基类形参类型。
|
||||
|
@ -1,171 +1,183 @@
|
||||
## Item42: 考虑使用emplacement代替insertion
|
||||
## 条款四十二:考虑使用置入代替插入
|
||||
|
||||
如果你拥有一个容器,例如`std::string`,那么当你通过插入函数(例如`insert, push_front, push_back`,或者对于`std::forward_list`, `insert_after`)添加新元素时,你传入的元素类型应该是`std::string`。毕竟,这就是容器里的内容。
|
||||
**Item 42: Consider emplacement instead of insertion**
|
||||
|
||||
如果你拥有一个容器,例如放着`std::string`,那么当你通过插入(insertion)函数(例如`insert`,`push_front`,`push_back`,或者对于`std::forward_list`来说是`insert_after`)添加新元素时,你传入的元素类型应该是`std::string`。毕竟,这就是容器里的内容。
|
||||
|
||||
逻辑上看来如此,但是并非总是如此。考虑如下代码:
|
||||
|
||||
```cpp
|
||||
std::vector<std::string> vs; // container of std::string
|
||||
vs.push_back("xyzzy"); // add string literal
|
||||
std::vector<std::string> vs; //std::string的容器
|
||||
vs.push_back("xyzzy"); //添加字符串字面量
|
||||
```
|
||||
|
||||
这里,容量里内容是`std::string`,但是你试图通过`push_back`加入字符串字面量,即引号内的字符序列。字符转字面量并不是`std::string`,这意味着你传递给`push_back`的参数并不是容器里的内容类型。
|
||||
这里,容器里内容是`std::string`,但是你有的——你实际上试图通过`push_back`加入的——是字符串字面量,即引号内的字符序列。字符串字面量并不是`std::string`,这意味着你传递给`push_back`的实参并不是容器里的内容类型。
|
||||
|
||||
`std::vector`的`push_back`被按左值和右值分别重载:
|
||||
|
||||
```cpp
|
||||
template<class T, class Allocator = allocator<T>>
|
||||
template <class T, //来自C++11标准
|
||||
class Allocator = allocator<T>>
|
||||
class vector {
|
||||
public:
|
||||
...
|
||||
void push_back(const &T x); // insert lvalue
|
||||
void push_back(T&& x); // insert rvalue
|
||||
...
|
||||
…
|
||||
void push_back(const T& x); //插入左值
|
||||
void push_back(T&& x); //插入右值
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
在`vs.push_back("xyzzy")`这个调用中,编译器看到参数类型(const char[6])和`push_back`采用的参数类型(`std::string`的引用)之间不匹配。它们通过从字符串字面量创建一个`std::string`类型的临时变量来消除不匹配,然后传递临时变量给`push_back`。换句话说,编译器处理的这个调用应该像这样:
|
||||
在
|
||||
|
||||
```cpp
|
||||
vs.push_back(std::string("xyzzy")); // create temp std::string and pass it to push_back
|
||||
vs.push_back("xyzzy");
|
||||
```
|
||||
|
||||
代码编译并运行,皆大欢喜。除了对于性能执着的人意识到了这份代码不如预期的执行效率高。
|
||||
|
||||
为了创建`std::string`类型的临时变量,调用了`std::string`的构造器,但是这份代码并不仅调用了一次构造器,调用了两次,而且还调用了析构器。这发生在`push_back`运行时:
|
||||
|
||||
1. 一个`std::string`的临时对象从字面量"xyzzy"被创建。这个对象没有名字,我们可以称为*temp*,*temp*通过`std::string`构造器生成,因为是临时变量,所以*temp*是右值。
|
||||
2. *temp*被传递给`push_back`的右值x重载函数。在`std::vector`的内存中一个x的副本被创建。这次构造器是第二次调用,在`std::vector`内部重新创建一个对象。(将x副本复制到`std::vector`内部的构造器是移动构造器,因为x传入的是右值,有关将右值引用强制转换为右值的信息,请参见Item25)。
|
||||
3. 在`push_back`返回之后,*temp*被销毁,调用了一次`std::string`的析构器。
|
||||
|
||||
性能执着者(译者注:直译性能怪人)不禁注意到是否存在一种方法可以获取字符串字面量并将其直接传入到步骤2中的`std::string`内部构造,可以避免临时对象*temp*的创建与销毁。这样的效率最好,性能执着者也不会有什么意见了。
|
||||
|
||||
因为你是一个C++开发者,所以你会有高于平均水平的要求。如果你不是C++开发者,你可能也会同意这个观点(如果你根本不考虑性能,为什么你没在用python?)。所以让我来告诉你如何使得`push_back`达到最高的效率。就是不使用`push_back`,你需要的是`emplace_back`。
|
||||
|
||||
`emplace_back`就是像我们想要的那样做的:直接把传递的参数(无论是不是`std::string`)直接传递到`std::vector`内部的构造器。没有临时变量会生成:
|
||||
这个调用中,编译器看到实参类型(`const char[6]`)和`push_back`采用的形参类型(`std::string`的引用)之间不匹配。它们通过从字符串字面量创建一个`std::string`类型的临时对象来消除不匹配,然后传递临时变量给`push_back`。换句话说,编译器处理的这个调用应该像这样:
|
||||
|
||||
```cpp
|
||||
vs.emplace_back("xyzzy"); // construct std::string inside vs directly from "xyzzy"
|
||||
vs.push_back(std::string("xyzzy")); //创建临时std::string,把它传给push_back
|
||||
```
|
||||
|
||||
`emplace_back`使用完美转发,因此只要你没有遇到完美转发的限制(参见Item30),就可以传递任何参数以及组合到`emplace_back`。比如,如果你在vs传递一个字符和一个数量给`std::string`构造器创建`std::string`,代码如下:
|
||||
代码可以编译并运行,皆大欢喜。除了对于性能执着的人意识到了这份代码不如预期的执行效率高。
|
||||
|
||||
为了在`std::string`容器中创建新元素,调用了`std::string`的构造函数,但是这份代码并不仅调用了一次构造函数,而是调用了两次,而且还调用了`std::string`析构函数。下面是在`push_back`运行时发生了什么:
|
||||
|
||||
1. 一个`std::string`的临时对象从字面量“`xyzzy`”被创建。这个对象没有名字,我们可以称为`temp`。`temp`的构造是第一次`std::string`构造。因为是临时变量,所以`temp`是右值。
|
||||
2. `temp`被传递给`push_back`的右值重载函数,绑定到右值引用形参`x`。在`std::vector`的内存中一个`x`的副本被创建。这次构造——也是第二次构造——在`std::vector`内部真正创建一个对象。(将`x`副本拷贝到`std::vector`内部的构造函数是移动构造函数,因为`x`在它被拷贝前被转换为一个右值,成为右值引用。有关将右值引用形参强制转换为右值的信息,请参见[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md))。
|
||||
3. 在`push_back`返回之后,`temp`立刻被销毁,调用了一次`std::string`的析构函数。
|
||||
|
||||
对于性能执着的人不禁注意到是否存在一种方法可以获取字符串字面量并将其直接传入到步骤2里在`std::vector`内构造`std::string`的代码中,可以避免临时对象`temp`的创建与销毁。这样的效率最好,对于性能执着的人也不会有什么意见了。
|
||||
|
||||
因为你是一个C++开发者,所以你更大可能是一个对性能执着的人。如果你不是C++开发者,你可能也会同意这个观点。(如果你根本不考虑性能,为什么你没在用Python?)所以我很高兴告诉你有一种方法,恰是在调用`push_back`中实现效率最大化。它不叫`push_back`。`push_back`函数不正确。你需要的是`emplace_back`。
|
||||
|
||||
`emplace_back`就是像我们想要的那样做的:使用传递给它的任何实参直接在`std::vector`内部构造一个`std::string`。没有临时变量会生成:
|
||||
|
||||
```cpp
|
||||
vs.emplace_back(50, 'x'); // insert std::string consisting of 50 'x' characters
|
||||
vs.emplace_back("xyzzy"); //直接用“xyzzy”在vs内构造std::string
|
||||
```
|
||||
|
||||
`emplace_back`可以用于每个支持`push_back`的容器。类似的,每个支持`push_front`的标准容器支持`emplace_front`。每个支持`insert`(除了`std::forward_list`和`std::array`)的标准容器支持`emplace。`关联容器提供`emplace_hint`来补充带有“hint”迭代器的插入函数,`std::forward_list`有`emplace_after`来匹配`insert_after`。
|
||||
`emplace_back`使用完美转发,因此只要你没有遇到完美转发的限制(参见[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)),就可以传递任何实参以及组合到`emplace_back`。比如,如果你想通过接受一个字符和一个数量的`std::string`构造函数,在`vs`中创建一个`std::string`,代码如下:
|
||||
|
||||
使得emplacement函数功能优于insertion函数的原因是它们灵活的接口。insertion函数接受对象来插入,而emplacement函数接受构造器接受的参数插入。这种差异允许emplacement函数避免临时对象的创建和销毁。
|
||||
```cpp
|
||||
vs.emplace_back(50, 'x'); //插入由50个“x”组成的一个std::string
|
||||
```
|
||||
|
||||
因为可以传递容器内类型给emplacement函数(该参数使函数执行复制或者移动构造器),所以即使insertion函数不会构造临时对象,也可以使用emplacement函数。在这种情况下,insertion和emplacement函数做的是同一件事,比如:
|
||||
`emplace_back`可以用于每个支持`push_back`的标准容器。类似的,每个支持`push_front`的标准容器都支持`emplace_front`。每个支持`insert`(除了`std::forward_list`和`std::array`)的标准容器支持`emplace`。关联容器提供`emplace_hint`来补充接受“hint”迭代器的`insert`函数,`std::forward_list`有`emplace_after`来匹配`insert_after`。
|
||||
|
||||
使得置入(emplacement)函数功能优于插入函数的原因是它们有灵活的接口。插入函数接受**对象**去插入,而置入函数接受**对象的构造函数接受的实参**去插入。这种差异允许置入函数避免插入函数所必需的临时对象的创建和销毁。
|
||||
|
||||
因为可以传递容器内元素类型的实参给置入函数(因此该实参使函数执行复制或者移动构造函数),所以在插入函数不会构造临时对象的情况,也可以使用置入函数。在这种情况下,插入和置入函数做的是同一件事,比如:
|
||||
|
||||
```cpp
|
||||
std::string queenOfDisco("Donna Summer");
|
||||
```
|
||||
|
||||
下面的调用都是可行的,效率也一样:
|
||||
下面的调用都是可行的,对容器的实际效果也一样:
|
||||
|
||||
```cpp
|
||||
vs.push_back(queenOfDisco); // copy-construct queenOfDisco
|
||||
vs.emplace_back(queenOfDisco); // ditto
|
||||
vs.push_back(queenOfDisco); //拷贝构造queenOfDisco
|
||||
vs.emplace_back(queenOfDisco); //同上
|
||||
```
|
||||
|
||||
因此,emplacement函数可以完成insertion函数的所有功能。并且有时效率更高,至少在理论上,不会更低效。那为什么不在所有场合使用它们?
|
||||
因此,置入函数可以完成插入函数的所有功能。并且有时效率更高,至少在理论上,不会更低效。那为什么不在所有场合使用它们?
|
||||
|
||||
因为,就像说的那样,理论上,在理论和实际上没有什么区别,但是实际,区别还是有的。在当前标准库的实现下,有些场景,就像预期的那样,emplacement执行性能优于insertion,但是,有些场景反而insertion更快。这种场景不容易描述,因为依赖于传递的参数类型、容器类型、emplacement或insertion的容器位置、容器类型构造器的异常安全性和对于禁止重复值的容器(即`std::set,std::map,std::unorder_set,set::unorder_map`)要添加的值是否已经在容器中。因此,大致的调用建议是:通过benchmakr测试来确定emplacment和insertion哪种更快。
|
||||
因为,就像说的那样,只是“理论上”,在理论和实际上没有什么区别,但是实际上区别还是有的。在当前标准库的实现下,有些场景,就像预期的那样,置入执行性能优于插入,但是,有些场景反而插入更快。这种场景不容易描述,因为依赖于传递的实参的类型、使用的容器、置入或插入到容器中的位置、容器中类型的构造函数的异常安全性,和对于禁止重复值的容器(即`std::set`,`std::map`,`std::unordered_set`,`set::unordered_map`)要添加的值是否已经在容器中。因此,大致的调用建议是:通过benchmark测试来确定置入和插入哪种更快。
|
||||
|
||||
当然这个结论不是很令人满意,所以还有一种启发式的方法来帮助你确定是否应该使用emplacement。如果下列条件都能满足,emplacement会优于insertion:
|
||||
当然这个结论不是很令人满意,所以你会很高兴听到还有一种启发式的方法来帮助你确定是否应该使用置入。如果下列条件都能满足,置入会优于插入:
|
||||
|
||||
- **值是通过构造器添加到容器,而不是直接赋值。** 例子就像本Item刚开始的那样(添加"xyzzy"到`std::string的std::vector`中)。新值必须通过`std::string`的构造器添加到`std::vector`。如果我们回看这个例子,新值放到已经存在对象的位置,那情况就完全不一样了。考虑下:
|
||||
- **值是通过构造函数添加到容器,而不是直接赋值。** 例子就像本条款刚开始的那样(用“`xyzzy`”添加`std::string`到`std::vector`容器`vs`中),值添加到`vs`末尾——一个先前没有对象存在的地方。新值必须通过构造函数添加到`std::vector`。如果我们回看这个例子,新值放到已经存在了对象的一个地方,那情况就完全不一样了。考虑下:
|
||||
|
||||
```cpp
|
||||
std::vector<std::string> vs; // as before
|
||||
... // add elements to vs
|
||||
vs.emplace(vs.begin(), "xyzzy"); // add "xyzzy" to beginning of vs
|
||||
std::vector<std::string> vs; //跟之前一样
|
||||
… //添加元素到vs
|
||||
vs.emplace(vs.begin(), "xyzzy"); //添加“xyzzy”到vs头部
|
||||
```
|
||||
|
||||
对于这份代码,没有实现会在已经存在对象的位置`vs[0]`构造添加的`std::string`。而是,通过移动赋值的方式添加到需要的位置。但是移动赋值需要一个源对象,所以这意味着一个临时对象要被创建,而emplacement优于insertion的原因就是没有临时对象的创建和销毁,所以当通过赋值操作添加元素时,emplacement的优势消失殆尽。
|
||||
对于这份代码,没有实现会在已经存在对象的位置`vs[0]`构造这个添加的`std::string`。而是,通过移动赋值的方式添加到需要的位置。但是移动赋值需要一个源对象,所以这意味着一个临时对象要被创建,而置入优于插入的原因就是没有临时对象的创建和销毁,所以当通过赋值操作添加元素时,置入的优势消失殆尽。
|
||||
|
||||
而且,向容器添加元素是通过构造还是赋值通常取决于实现者。但是,启发式仍然是有帮助的。基于节点的容器实际上总是使用构造器添加新元素,大多数标准库容器都是基于节点的。例外的容器只有`std::vector, std::deque, std::string`(`std::array`也不是基于节点的,但是它不支持emplacement和insertion)。在不是基于节点的容器中,你可以依靠`emplace_back`来使用构造向容器添加元素,对于`std::deque`,`emplace_front`也是一样的。
|
||||
而且,向容器添加元素是通过构造还是赋值通常取决于实现者。但是,启发式仍然是有帮助的。基于节点的容器实际上总是使用构造添加新元素,大多数标准库容器都是基于节点的。例外的容器只有`std::vector`,`std::deque`,`std::string`。(`std::array`也不是基于节点的,但是它不支持置入和插入,所以它与这儿无关。)在不是基于节点的容器中,你可以依靠`emplace_back`来使用构造向容器添加元素,对于`std::deque`,`emplace_front`也是一样的。
|
||||
|
||||
- **传递的参数类型与容器的初始化类型不同。** 再次强调,emplacement优于insertion通常基于以下事实:当传递的参数不是容器保存的类型时,接口不需要创建和销毁临时对象。当将类型为T的对象添加到container<T>时,没有理由期望emplacement比insertion运行的更快,因为不需要创建临时对象来满足insertion接口。
|
||||
- **传递的实参类型与容器的初始化类型不同。** 再次强调,置入优于插入通常基于以下事实:当传递的实参不是容器保存的类型时,接口不需要创建和销毁临时对象。当将类型为`T`的对象添加到`container<T>`时,没有理由期望置入比插入运行的更快,因为不需要创建临时对象来满足插入的接口。
|
||||
|
||||
- **容器不拒绝重复项作为新值。** 这意味着容器要么允许添加重复值,要么你添加的元素都是不重复的。这样要求的原因是为了判断一个元素是否已经存在于容器中,emplacement实现通常会创建一个具有新值的节点,以便可以将该节点的值与现有容器中节点的值进行比较。如果要添加的值不在容器中,则链接该节点。然后,如果值已经存在,emplacement创建的节点就会被销毁,意味着构造和析构时浪费的开销。这样的创建就不会在insertion函数中出现。
|
||||
- **容器不拒绝重复项作为新值。** 这意味着容器要么允许添加重复值,要么你添加的元素大部分都是不重复的。这样要求的原因是为了判断一个元素是否已经存在于容器中,置入实现通常会创建一个具有新值的节点,以便可以将该节点的值与现有容器中节点的值进行比较。如果要添加的值不在容器中,则链接该节点。然后,如果值已经存在,置入操作取消,创建的节点被销毁,意味着构造和析构时的开销被浪费了。这样的节点更多的是为置入函数而创建,相比起为插入函数来说。
|
||||
|
||||
本Item开始的例子中下面的调用满足上面的条件。所以调用比`push_back`运行更快。
|
||||
本条款开始的例子中下面的调用满足上面的条件。所以`emplace_back`比`push_back`运行更快。
|
||||
|
||||
```cpp
|
||||
vs.emplace_back("xyzzy"); // construct new value at end of container; don't pass the type in container; don't use container rejecting duplicates
|
||||
vs.emplace_back(50, 'x'); // ditto
|
||||
vs.emplace_back("xyzzy"); //在容器末尾构造新值;不是传递的容器中元
|
||||
//素的类型;没有使用拒绝重复项的容器
|
||||
|
||||
vs.emplace_back(50, 'x'); //同上
|
||||
```
|
||||
|
||||
在决定是否使用emplacement函数时,需要注意另外两个问题。**首先**是资源管理。假定你有一个`std::shared_ptr<Widget>s`的容器,
|
||||
在决定是否使用置入函数时,需要注意另外两个问题。首先是资源管理。假定你有一个盛放`std::shared_ptr<Widget>`s的容器,
|
||||
|
||||
```cpp
|
||||
std::list<std::shared_ptr<Widget>> ptrs;
|
||||
```
|
||||
|
||||
然后你想添加一个通过自定义deleted释放的`std::shared_ptr`(参见Item 19)。Item 21说明你应该使用`std::make_shared`来创建`std::shared_ptr`,但是它也承认有时你无法做到这一点。比如当你要指定一个自定义deleter时。这时,你必须直接创建一个原始指针,然后通过`std::shared_ptr`来管理。
|
||||
然后你想添加一个通过自定义删除器释放的`std::shared_ptr`(参见[Item19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md))。[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)说明你应该使用`std::make_shared`来创建`std::shared_ptr`,但是它也承认有时你无法做到这一点。比如当你要指定一个自定义删除器时。这时,你必须直接`new`一个原始指针,然后通过`std::shared_ptr`来管理。
|
||||
|
||||
如果自定义deleter是这个函数,
|
||||
如果自定义删除器是这个函数,
|
||||
|
||||
```cpp
|
||||
void killWidget(Widget* pWidget);
|
||||
```
|
||||
|
||||
使用insertion函数的代码如下:
|
||||
使用插入函数的代码如下:
|
||||
|
||||
```cpp
|
||||
ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));
|
||||
```
|
||||
|
||||
也可以像这样
|
||||
也可以像这样:
|
||||
|
||||
```cpp
|
||||
ptrs.push_back({new Widget, killWidget});
|
||||
```
|
||||
|
||||
不管哪种写法,在调用`push_back`中会生成一个临时`std::shared_ptr`对象。`push_back`的参数是`std::shared_ptr`的引用,因此必须有一个`std::shared_ptr`。
|
||||
不管哪种写法,在调用`push_back`前会生成一个临时`std::shared_ptr`对象。`push_back`的形参是`std::shared_ptr`的引用,因此必须有一个`std::shared_ptr`。
|
||||
|
||||
`std::shared_ptr`的临时对象创建应该可以避免,但是在这个场景下,临时对象值得被创建。考虑如下可能的时间序列:
|
||||
用`emplace_back`应该可以避免`std::shared_ptr`临时对象的创建,但是在这个场景下,临时对象值得被创建。考虑如下可能的时间序列:
|
||||
|
||||
1. 在上述的调用中,一个`std::shared_ptr<Widget>`的临时对象被创建来持有`new Widget`对象。称这个对象为*temp*。
|
||||
2. `push_back`接受*temp*的引用。在节点的分配一个副本来复制*temp*的过程中,OOM异常被抛出
|
||||
3. 随着异常从`push_back`的传播,*temp*被销毁。作为唯一管理Widget的弱指针`std::shared_ptr`对象,会自动销毁`Widget`,在这里就是调用`killWidget`。
|
||||
1. 在上述的调用中,一个`std::shared_ptr<Widget>`的临时对象被创建来持有“`new Widget`”返回的原始指针。称这个对象为`temp`。
|
||||
2. `push_back`通过引用接受`temp`。在存储`temp`的副本的*list*节点的内存分配过程中,内存溢出异常被抛出。
|
||||
3. 随着异常从`push_back`的传播,`temp`被销毁。作为唯一管理这个`Widget`的`std::shared_ptr`,它自动销毁`Widget`,在这里就是调用`killWidget`。
|
||||
|
||||
这样的话,即使发生了异常,没有资源泄露:在调用`push_back`中通过`new Widget`创建的`Widget`在`std::shared_ptr`管理下自动销毁。生命周期良好。
|
||||
这样的话,即使发生了异常,没有资源泄漏:在调用`push_back`中通过“`new Widget`”创建的`Widget`在`std::shared_ptr`管理下自动销毁。生命周期良好。
|
||||
|
||||
考虑使用`emplace_back`代替`push_back`
|
||||
考虑使用`emplace_back`代替`push_back`:
|
||||
|
||||
```cpp
|
||||
ptrs.emplace_back(new Widget, killWidget);
|
||||
```
|
||||
|
||||
1. 通过`new Widget`的原始指针完美转发给`emplace_back`的内部构造器。如果分配失败,还是抛出OOM异常
|
||||
2. 当异常从`emplace_back`传播,原始指针是仅有的访问途径,但是因为异常丢失了,这就发生了资源泄露
|
||||
1. 通过`new Widget`创建的原始指针完美转发给`emplace_back`中,*list*节点被分配的位置。如果分配失败,还是抛出内存溢出异常。
|
||||
2. 当异常从`emplace_back`传播,原始指针是仅有的访问堆上`Widget`的途径,但是因为异常而丢失了,那个`Widget`的资源(以及任何它所拥有的资源)发生了泄漏。
|
||||
|
||||
在这个场景中,生命周期不良好,这个失误不能赖`std::shared_ptr`。`std::unique_ptr`使用自定义deleter也会有同样的问题。根本上讲,像`std::shared_ptr和std::unique_ptr`这样的资源管理类的有效性取决于资源被**立即**传递给资源管理对象的构造函数。实际上,这就是`std::make_shared和std::make_unique`这样的函数如此重要的原因。
|
||||
在这个场景中,生命周期不良好,这个失误不能赖`std::shared_ptr`。使用带自定义删除器的`std::unique_ptr`也会有同样的问题。根本上讲,像`std::shared_ptr`和`std::unique_ptr`这样的资源管理类的高效性是以资源(比如从`new`来的原始指针)被**立即**传递给资源管理对象的构造函数为条件的。实际上,`std::make_shared`和`std::make_unique`这样的函数自动做了这些事,是使它们如此重要的原因。
|
||||
|
||||
在对存储资源管理类的容器调用insertion函数时(比如`std::list<std::shared_ptr<Widget>>`),函数的参数类型通常确保在资源的获取和管理资源对象的创建之间没有其他操作。在emplacement函数中,完美转发推迟了资源管理对象的创建,直到可以在容器的内存中构造它们为止,这给异常导致资源泄露提供了可能。所有的标准库容器都容易受到这个问题的影响。在使用资源管理对象的容器时,比如注意确保使用emplacement函数不会为提高效率带来降低异常安全性的后果。
|
||||
在对存储资源管理类对象的容器(比如`std::list<std::shared_ptr<Widget>>`)调用插入函数时,函数的形参类型通常确保在资源的获取(比如使用`new`)和资源管理对象的创建之间没有其他操作。在置入函数中,完美转发推迟了资源管理对象的创建,直到可以在容器的内存中构造它们为止,这给“异常导致资源泄漏”提供了可能。所有的标准库容器都容易受到这个问题的影响。在使用资源管理对象的容器时,必须注意确保在使用置入函数而不是插入函数时,不会为提高效率带来的降低异常安全性付出代价。
|
||||
|
||||
坦白说,无论如何,你不应该将`new Widget`传递给`emplace_back或者push_back`或者大多数这种函数,因为,就像Item 21中解释的那样,这可能导致我们刚刚讨论的异常安全性问题。使用独立语句将从`new Widget`获取指针然后传递给资源管理类,然后传递这个对象的右值引用给你想传递`new Widget`的函数(Item 21 有这个观点的详细讨论)。代码应该如下:
|
||||
坦白说,无论如何,你不应该将“`new Widget`”之类的表达式传递给`emplace_back`或者`push_back`或者大多数这种函数,因为,就像[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)中解释的那样,这可能导致我们刚刚讨论的异常安全性问题。消除资源泄漏可能性的方法是,使用独立语句把从“`new Widget`”获取的指针传递给资源管理类对象,然后这个对象作为右值传递给你本来想传递“`new Widget`”的函数([Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)有这个观点的详细讨论)。使用`push_back`的代码应该如下:
|
||||
|
||||
```cpp
|
||||
std::shared_ptr<Widget> spw(new Widget, killWidget); // create Widget and have spw manage it
|
||||
ptrs.push_back(std::move(spw)); // add spw as rvalue
|
||||
std::shared_ptr<Widget> spw(new Widget, //创建Widget,让spw管理它
|
||||
killWidget);
|
||||
ptrs.push_back(std::move(spw)); //添加spw右值
|
||||
```
|
||||
|
||||
emplace_back的版本如下:
|
||||
`emplace_back`的版本如下:
|
||||
|
||||
```cpp
|
||||
std::shared_ptr<Widget> spw(new Widget, killWidget); // create Widget and have spw manage it
|
||||
std::shared_ptr<Widget> spw(new Widget, killWidget);
|
||||
ptrs.emplace_back(std::move(spw));
|
||||
```
|
||||
|
||||
无论哪种方式,都会产生spw的创建和销毁成本。给出选择emplacement函数优于insertion函数的动机是避免临时对象的开销,但是对于swp的概念来讲,当根据正确的方式确保获取资源和连接到资源管理对象上之间无其他操作,添加资源管理类型对象到容器中,emplacement函数不太可能胜过insertion函数。
|
||||
无论哪种方式,都会产生`spw`的创建和销毁成本。选择置入而非插入的动机是避免容器元素类型的临时对象的开销。但是对于`spw`的概念来讲,当添加资源管理类型对象到容器中,并根据正确的方式确保在获取资源和连接到资源管理对象上之间无其他操作时,置入函数不太可能胜过插入函数。
|
||||
|
||||
emplacement函数的**第二个**值得注意的方面是它们与显式构造函数的交互。对于C++11正则表达式的支持,假设你创建了一个正则表达式的容器:
|
||||
置入函数的第二个值得注意的方面是它们与`explicit`的构造函数的交互。鉴于C++11对正则表达式的支持,假设你创建了一个正则表达式对象的容器:
|
||||
|
||||
```cpp
|
||||
std::vector<std::regex> regexes;
|
||||
@ -174,67 +186,65 @@ std::vector<std::regex> regexes;
|
||||
由于你同事的打扰,你写出了如下看似毫无意义的代码:
|
||||
|
||||
```cpp
|
||||
regexes.emplace_back(nullptr); // add nullptr to container of regexes?
|
||||
regexes.emplace_back(nullptr); //添加nullptr到正则表达式的容器中?
|
||||
```
|
||||
|
||||
你没有注意到错误,编译器也没有提示你,所以你浪费了大量时间来调试。突然,你发现你插入了空指针到正则表达式的容器中。但是这怎么可能?指针不是正则表达式,如果你试图下面这样写
|
||||
你没有注意到错误,编译器也没有提示你,所以你浪费了大量时间来调试。突然,你发现你插入了空指针到正则表达式的容器中。但是这怎么可能?指针不是正则表达式,如果你试图下面这样写,
|
||||
|
||||
```cpp
|
||||
std::regex r = nullptr; // error! won't compile
|
||||
std::regex r = nullptr; //错误!不能编译
|
||||
```
|
||||
|
||||
编译器就会报错。有趣的是,如果你调用`push_back`而不是`emplace_back`,编译器就会报错
|
||||
编译器就会报错。有趣的是,如果你调用`push_back`而不是`emplace_back`,编译器也会报错:
|
||||
|
||||
```cpp
|
||||
regexes.push_back(nullptr); // error! won't compile
|
||||
regexes.push_back(nullptr); //错误!不能编译
|
||||
```
|
||||
|
||||
当前你遇到的奇怪行为由于可能用字符串构造`std::regex`的对象,这就意味着下面代码合法:
|
||||
当前你遇到的奇怪行为来源于“可能用字符串构造`std::regex`对象”的事实,这就意味着下面代码合法:
|
||||
|
||||
```cpp
|
||||
std::regex upperCaseWorld("[A-Z]+");
|
||||
```
|
||||
|
||||
通过字符串创建`std::regex`要求相对较长的运行时开销,所以为了最小程度减少无意中产生此类开销的可能性,采用`const char*`指针的`std::regex`构造函数是显式的。这就是为什么下面代码无法编译的原因:
|
||||
通过字符串创建`std::regex`要求相对较长的运行时开销,所以为了最小程度减少无意中产生此类开销的可能性,采用`const char*`指针的`std::regex`构造函数是`explicit`的。这就是下面代码无法编译的原因:
|
||||
|
||||
```cpp
|
||||
std::regex r = nullptr; // error! won't compile
|
||||
regexes.push_back(nullptr); // error
|
||||
std::regex r = nullptr; //错误!不能编译
|
||||
regexes.push_back(nullptr); //错误
|
||||
```
|
||||
|
||||
在上面的代码中,我们要求从指针到`std::regex`的隐式转换,但是显式构造的要求拒绝了此类转换。
|
||||
在上面的代码中,我们要求从指针到`std::regex`的隐式转换,但是构造函数的`explicit`ness拒绝了此类转换。
|
||||
|
||||
但是在`emplace_back`的调用中,我们没有声明传递一个`std::regex`对象。代替的是,我们传递了一个`std::regex`构造器参数。那不是隐式转换,而是显式的:
|
||||
但是在`emplace_back`的调用中,我们没有说明要传递一个`std::regex`对象。然而,我们传递了一个`std::regex`**构造函数实参**。那不被认为是个隐式转换要求。相反,编译器看你像是写了如下代码:
|
||||
|
||||
```cpp
|
||||
std::regex r(nullptr); // compiles
|
||||
std::regex r(nullptr); //可编译
|
||||
```
|
||||
|
||||
如果简洁的注释“compiles”表明缺乏直观理解,好的,因为这个代码可以编译,但是行为不确定。使用`const char*`指针的`std::regex`构造器要求字符串是一个有效的正则表达式,nullptr不是有效的。如果你写出并编译了这样的代码,最好的希望就是运行时crash掉。如果你不幸运,就会花费大量的时间调试。
|
||||
如果简洁的注释“可编译”缺乏直观理解,好的,因为这个代码可以编译,但是行为不确定。使用`const char*`指针的`std::regex`构造函数要求字符串是一个有效的正则表达式,空指针并不满足要求。如果你写出并编译了这样的代码,最好的希望就是运行时程序崩溃掉。如果你不幸运,就会花费大量的时间调试。
|
||||
|
||||
先把`push_back, emplace_back`放在一边,注意到相似的初始化语句导致了多么不一样的结果:
|
||||
先把`push_back`,`emplace_back`放在一边,注意到相似的初始化语句导致了多么不一样的结果:
|
||||
|
||||
```cpp
|
||||
std::regex r1 = nullptr; // error ! won't compile
|
||||
std::regex r2(nullptr); // compiles
|
||||
std::regex r1 = nullptr; //错误!不能编译
|
||||
std::regex r2(nullptr); //错误
|
||||
```
|
||||
|
||||
在标准的官方术语中,用于初始化r1的语法是所谓的复制初始化。相反,用于初始化r2的语法是(也被称为braces)被称为直接初始化。复制初始化不是显式调用构造器的,直接初始化是。这就是r2可以编译的原因。
|
||||
在标准的官方术语中,用于初始化`r1`的语法(使用等号)是所谓的**拷贝初始化**。相反,用于初始化`r2`的语法是(使用小括号,有时也用花括号)被称为**直接初始化**。拷贝初始化不被允许使用`explicit`构造函数(译者注:即没法调用相应类的`explicit`拷贝构造函数),直接初始化却被允许。这就是初始化`r1`不能编译,而初始化`r2`可以编译的原因。
|
||||
|
||||
然后回到`push_back和emplace_back`,更一般来说,insertion函数对比emplacment函数。emplacement函数使用直接初始化,这意味着使用显式构造器。insertion函数使用复制初始化。因此:
|
||||
然后回到`push_back`和`emplace_back`,更一般来说是,插入函数和置入函数的对比。置入函数使用直接初始化,这意味着可能使用`explicit`的构造函数。插入函数使用拷贝初始化,所以不能用`explicit`的构造函数。因此:
|
||||
|
||||
```cpp
|
||||
regexes.emplace_back(nullptr); // compiles. Direct init permits use of explicit std::regex ctor taking a pointer
|
||||
regexes.push_back(nullptr); // error! copy init forbids use of that ctor
|
||||
regexes.emplace_back(nullptr); //可编译。直接初始化允许使用接受指针的
|
||||
//std::regex的explicit构造函数
|
||||
regexes.push_back(nullptr); //错误!拷贝初始化不允许用那个构造函数
|
||||
```
|
||||
|
||||
要汲取的是,当你使用emplacement函数时,请特别小心确保传递了正确的参数,因为即使是显式构造函数,编译器可以尝试解释你的代码称为有效的(译者注:这里意思是即使你写的代码逻辑上不对,显式构造器时编译器可能能解释通过即编译成功)
|
||||
获得的经验是,当你使用置入函数时,请特别小心确保传递了正确的实参,因为即使是`explicit`的构造函数也会被编译器考虑,编译器会试图以有效方式解释你的代码。
|
||||
|
||||
### 需要记住的事
|
||||
**请记住:**
|
||||
|
||||
- 原则上,emplacement函数有时会比insertion函数高效,并且不会更差
|
||||
- 实际上,当执行如下操作时,emplacement函数更快
|
||||
1. 值被构造到容器中,而不是直接赋值
|
||||
2. 传入的类型与容器类型不一致
|
||||
3. 容器不拒绝已经存在的重复值
|
||||
- emplacement函数可能执行insertion函数拒绝的显示构造
|
||||
- 原则上,置入函数有时会比插入函数高效,并且不会更差。
|
||||
- 实际上,当以下条件满足时,置入函数更快:(1)值被构造到容器中,而不是直接赋值;(2)传入的类型与容器的元素类型不一致;(3)容器不拒绝已经存在的重复值。
|
||||
- 置入函数可能执行插入函数拒绝的类型转换。
|
||||
|
Loading…
Reference in New Issue
Block a user