在泛型编程中,常常需要将参数原封不动的转发给另外一个函数,比如std::make_shared
std增加了forward工具函数, 完美转发主要目的一般都是为了避免拷贝,同时调用正确的函数版本。
为了理解完美转发,首先要理解左值与右值。
一、 为了更深刻的理解左值与右值,我们先来复习一下decltype表达式
decltype(expression) 可以获取表达式的类型:
1,expression 是一个函数时,decltype 返回函数的返回值类型,没有任何问题
2,expression 是单个变量时,decltype 返回变量的类型,依旧没有任何问题
2,当expression 是某个表达式时,decltype 返回表达式的值的类型,这里会有一点让人迷惑的地方,看下面的代码:
void f(int&& a ) { int i = 0; int &ri = i; cout << boolalpha; cout << is_same::value << endl; //true cout << is_same ::value << endl; // true cout << is_same ::value << endl; // true cout << is_same ::value << endl; //true cout << is_same ::value << endl; //true cout << is_same ::value << endl; //true cout << is_same ::value << endl; //false }
首先, ri 是一个类型为 int& 的单个变量,decltype(ri) 得到 int& 没有任何问题, 同理, decltype (i) 也理所当然的得到 int, 因为 i 是 int 型变量并且在定义时分配了sizeof(int)的栈空间的。
接下来3 + 4.0 是一个表达式, 这个表达式返回一个右值, 该右值的类型为 double, 所以 decltype(3 + 4.0) 是 double
(i) 是一个表达式, 对于只有单个变量的表达式,c++编译器的处理是返回一个绑定到该变量的左值引用,所以decltype((i)) 返回 int&
下面重点来了,decltype((a)) 返回的类型依旧是 int&, 理由同上,对于(x) 这样的表达式,返回值是 x 的左值引用, 即使 x 的类型是 右值引用,这也就是说,我们可以将左值引用绑定到右值引用上,但是右值引用必须绑定到右值或者匿名的右值引用上 (如std::move()的返回值就是一个匿名右值引用)。
在进行右值绑定的时候,与 decltype 只会把形如 (a) 的单个变量看成表达式不同, 右值绑定时会把所有的 单个变量 都看成变量表达式, 然而单个变量的表达式返回的是一个左值引用, 然而右值引用无法绑定到左值引用, 最终造成的结果是我们无法把一个右值引用直接绑定到另一个右值引用上, 看下面的代码:
void f(int&& a ) { int i = 0; int &&ri = a; //编译错误,等价于 int &&ri = (a) 无法将一个右值引用绑定到另一个右值引用上 cout << boolalpha; cout << is_same::value << endl; //true }
其实也很好理解,单个右值引用表达式属于 具名的右值引用, 这个引用本身也是得等到离开作用于才消失的, 也就不是短暂的,所以自然也就无法将一个右值引用绑定到另一个具名的右值引用上了, 所以非得这么搞, 只能绑定到匿名的右值引用, 如:
int &&rri = std::move(a); // 正确, std::move 返回一个匿名右值引用 cout << is_same::value < ::value << endl; //true
如果你觉得这种错误毫无意义的话,那么看看下面这段代码:
void g(int &&) { cout << "right reference" << endl; } void g(const int&) { cout << "left reference" << endl; } void f(int&& a ) { /* do something*/ g(a); /* do something*/ }
对于 void g(int && arg), 试图把右值引用 arg 绑定到另一个右值引用 a 上, 然而在绑定时 a 被当成单个变量表达式从而得到的类型是 int&, 从而 f 调用的是 void g (int & arg); 在进行右值绑定时,凡是有名字的都必然是左值/左值引用, 所以为了使用 g (int &&) 我们必须显式的造出一个匿名的右值引用来:
void f(int&& a ) { /* do something*/ g(a); // g ( (std::move(a)) ) 同样会调用右值引用版本, 因为 ( std::move(a) ) 不属于 单个变量表达式, 也返回了一个匿名右值引用 /* do something*/ }
从上面的例子可以看出来, 要实现完美转发需要这样:
void f (const int& x) { g (x); // g 接受 const int& }
或者这样 :
void f (const int& x) { g (const_cast(x)); // g 接受int& }
或者
void f (int&& x) { g (std::move(x)); //g 接受右值引用 }
那如果在模板中应该怎么做呢?
现在说明使用std::forward实现完美转发的原理:
1,forward
2,凡是使用forward来实现完美转发的,接受参数时都应该写成 void f(T&& x) { g(std::forward
那么就以
templatevoid f (T&& arg) { g ( std::forward (arg) ); }
为例来说明完美转发:
1, 当 f 收到一个类型为 const int 的左值/左值引用 时, 由于引用折叠,T的类型为 const int&, f 被实例化成
void (const int &arg) { g (std::forward(arg)); }
此时 g 收到的类型为 const int &&& ---> const int&,一个常量左值引。
2, 当 f 收到一个类型为 int 的左值/左值引用, 同样的,g 收到的类型为 int&
3, 当 f 收到一个 int 类型的右值时, T 被推断成 int, 从而 std::forward
4, 再次强调为什么 f 一定要写成 void f (T&&), 首先最最基本的要求就是, 传递给 g 的参数不是 f 拷贝后的参数, 而是 f 收到的参数,所以一定要是引用,绝对不能是拷贝,至于写成右值引用的形式, 是为了接受右值。不管 f 接受到的是左值还是左值引用,最后 T 都会变成 左值引用并且不影响 g 的接受, 不管接受的是右值还是匿名右值引用(上面提到具名右值引用,会在绑定时当成表达式而变成左值引用,int &&a; 是具名右值引用, std::move(a) 则得到一个匿名右值引用, 4 + 3 这样的得到的是右值), 最终都会被std::forward变成匿名右值引用被 g 正确接受
所以最后来体会一次完美转发吧:
templatevoid f (T&&) { cout << boolalpha; cout << "is int ? : " << is_same ::value << endl; cout << "is int& ? : " << is_same ::value << endl; cout << "is int&& ? : " << is_same ::value << endl; cout << endl; } int main() { int i = 0; int &ri = i; int &&rri = 4; f(i); //左值 f(ri); // 左值引用 f(0); //右值 f(std::move(i)); // 匿名的右值引用, 正确识别 f(rri); //实际上 f 收到的是左值引用, 右值引用只能绑定到 右值 上,而不能绑定到另一个右值引用上, 左值引用却可以绑定到另一个右值引用或者另一个左值引用或者左值上 return 0; }