C++ 右值引用和完美转发
C++11 开始增加了移动语义和右值引用,这使得函数的重载变得更加复杂:你可能需要单独为右值参数设计一个函数,而这个函数明显在功能和实现上与左值参数是一样的,那么就需要有一个东西去统一函数参数的左值和右值,于是完美转发被设计出来。
std::forward
上一篇文章 C++ std::move 中提到过 std::remove_reference 和 static_cast 用于实现 std::move,而 std::forward 也是用这两个组件实现的:
template <typename T>
constexpr T &&
forward(typename std::remove_reference<T>::type &__t) noexcept
{
return static_cast<T &&>(__t);
}
template <typename T>
constexpr T &&
forward(typename std::remove_reference<T>::type &&__t) noexcept
{
static_assert(!std::is_lvalue_reference<T>::value, "template argument"
" substituting T must not be an lvalue reference type");
return static_cast<T &&>(__t);
}
std::forward 会将输入的参数原封不动地传递到下一个函数中,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
引用折叠
引用折叠是 C++ 为了实现完美转发的第二个语法,由于 C++ 无论在应用上还是语义上都不支持对引用的引用,所以 C++ 选择将引用的引用转化成直接的引用,具体规则如下:
T&& && -> T&&
T& & -> T&
T& && -> T&
T&& & -> T&
引用折叠用于模板的参数类型推导,auto 和 decltype。
万能引用
所谓万能引用,实际上是引用折叠的一个部分:
T&& && -> T&&
T&& & -> T&
换句话说,用右值引用作为参数声明,实参可为左值引用和右值引用,而用左值引用作为参数声明,实参只能为左值引用。
右值引用
右值引用只能绑定到右值上,左值除了可以绑定到左值上,在某些条件下还可以绑定到右值上。这里某些条件绑定右值为:常量左值引用绑定到右值,非常量左值引用不可绑定到右值。
声明中带 “&&” 的,可能结果是左值引用或者右值引用,参考上一篇文章 C++ std::move 最后面的一段代码。
std::string f()
{
return string("abc");
}
void g()
{
const std::string &s = f(); // still legal?
std::cout << s << std::endl;
}
g 是合法的,原因是 s 是个左值,类型是常量引用,而 f() 返回右值,前面提到常量左值引用可以绑定到右值。
可以用下面的例子来说明引用折叠:
template<typename T>
void f(T&& param);
int a;
f(a); // 传入左值,那么上述的 T&& 是左值引用
f(1); // 传入右值,那么上述的 T&& 是右值引用
右值引用最常见的特性是延长临时对象的生命周期:
class A
{
public:
int a = 0;
};
template <class T1, class T2>
void print_is_same()
{
std::cout << std::is_same<T1, T2>() << std::endl;
}
int main()
{
A a{}; // 这里使用了拷贝构造和默认构造,C++17 开始会直接被优化为默认构造(使用了返回值优化)
A b = A();
A &&c = A(); // 这里使用了右值引用,b 接管了临时对象,右值引用是左值
A &d = a;
print_is_same<A, decltype(a)>();
print_is_same<A, decltype(b)>();
print_is_same<A, decltype(c)>();
print_is_same<A &&, decltype(c)>();
std::cout << (typeid(a) == typeid(A)) << std::endl;
std::cout << (typeid(A) == typeid(b)) << std::endl;
std::cout << (typeid(A) == typeid(c)) << std::endl;
std::cout << (typeid(A) == typeid(d)) << std::endl;
}
虽然 c 是右值引用,但是在使用的过程中,还是如同普通的左值引用。
尤其需要注意的是,不要对临时对象使用 std::move,因为 std::move 不更改临时对象的生命周期,并且由于在声明的时候声明为非常量引用,会导致这个引用变为悬垂引用。
完美转发
完美转发是指在一个函数接收右值时,可以将这个右值继续传递给下一个函数。
右值引用是左值 ,所以当右值进入函数后,右值变为具名对象,即左值,此时再次传递这个对象,传递的是左值。
而完美转发是指让右值可以不断地以右值的身份传递下去:
#include <iostream>
void F(int x)
{
std::cout << "右值" << std::endl;
}
void F(int &&x)
{
std::cout << "左值" << std::endl;
}
template <class A>
void G(A &&a)
{
return F(std::forward<A>(a)); //1
//return F(a); //2
}
int main()
{
int i = 2;
G(i); // 正确
G(5); // 错误
}
这段代码实际上无法编译,但是证实了完美转发的存在,并且包含了右值引用,引用折叠。
分析上面的代码,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 函数是模板,所以 G 函数存在引用折叠,G 函数也可以做到万能引用。