C++右值引用和完美转发
C++11开始增加了移动语义和右值引用,这使得函数的重载变得更加复杂:你可能需要单独为右值参数设计一个函数,而这个函数明显在功能和实现上与左值参数是一样的,那么就需要有一个东西去统一函数参数的左值和右值,于是完美转发被设计出来。
std::forward
上一篇文章C++ std::move中提到过 std::remove_reference 和 static_cast 用于实现 std::move,而 std::forward 也是用这两个组件实现的:
std::forward会将输入的参数原封不动地传递到下一个函数中,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
引用折叠
引用折叠是C++为了实现完美转发的语法,由于C++无论在应用上还是语义上都不需要对引用的引用,所以C++选择将引用的引用转化成直接的引用,具体规则如下:
T&& && -> T&&T& & -> T&T& && -> T&T&& & -> T&
引用折叠用于模板的参数类型推导,auto和decltype。
万能引用
所谓万能引用,实际上是引用折叠的一个部分:
T&& && -> T&&T&& & -> T&
换句话说,用右值引用作为参数声明,实参可为左值引用和右值引用,而用左值引用作为参数声明,实参只能为左值引用。
右值引用
右值引用只能绑定到右值上,左值除了可以绑定到左值上,在某些条件下还可以绑定到右值上。这里某些条件绑定右值为:常量左值引用绑定到右值,非常量左值引用不可绑定到右值。
g 是合法的,原因是 s 是个左值,类型是常量引用,而 f() 返回右值,前面提到常量左值引用可以绑定到右值。
可以用下面的例子来说明引用折叠:
右值引用最常见的特性是延长临时对象的生命周期:
虽然 c 是右值引用,但是在使用的过程中,还是如同普通的左值引用。
尤其需要注意的是,不要对临时对象使用 std::move,因为 std::move 不更改临时对象的生命周期,因此临时对象会在当前语句执行完成时销毁,导致这个引用变为悬垂引用。
完美转发
完美转发是指在一个函数接收右值时,可以将这个右值继续传递给下一个函数。
右值引用是左值,所以当右值进入函数后,右值变为具名对象,即左值,此时再次传递这个对象,传递的是左值。
而完美转发是指让右值可以不断地以右值的身份传递下去:
这段代码实际上无法编译,但是证实了完美转发的存在,并且包含了右值引用,引用折叠。
分析上面的代码,G(i) 将一个左值传递给了 G,a 为左值引用,并且通过 std::forward 传递给了 void F(int x),中间经历了引用折叠 T&& & -> T&
而 G(5) 将一个右值传递给了 G,5 是右值,通过 std::forward 传递后还是右值,由于 5 既是右值,又是 int,所以编译器无法判断使用 F 的哪个重载版本,于是编译错误。
如果 G 不使用 std::forward,那么无论给 G 传入什么值,都会变为具名 a,即左值,在下一次传递时永远会传递给 void F(int x),因为右值引用永远只能传入右值或者常量。
注意,引用折叠只发生在模板的参数类型推导,auto 和 decltype,所以函数 F 不存在引用折叠,而 G 是函数模板,所以存在引用折叠,也可以做到万能引用。