C++ 引用折叠可以说是 C++ 一系列高级模板编程技巧(例如万能引用,完美转发)的基础。C++ 中的左值和右值都可以有引用,分别为左值引用(T&)和右值引用(T&&)。左值引用和右值引用也都是类型,所以它们也可以有引用。对于如何解释引用的引用,C++ 有一套规定:除了右值引用的右值引用折叠为右值引用之外,其他对引用的引用均折叠为左值引用。
也就是说,总共四种情况,用下面的代码总结一下:
// 左值引用的左值引用折叠为左值引用
T& & → T&
// 左值引用的右值引用折叠为左值引用
T& && → T&
// 右值引用的左值引用折叠为左值引用
T&& & → T&
// 右值引用的右值引用折叠为右值引用
T&& && → T&&
一言以蔽之,只要沾了左值引用,就永远是左值引用。通过 Compiler Explorer 验证一下以上的论点。
一些反思
如果读者了解移动语义的话,那么引用折叠会使得我们回味一下移动语义并得到一些新的见解。参考以下的代码,我们重载了函数 foo,使得它可以接受右值的入参,这是很常见的一个场景。
void foo(int &x){ std::cout << "lvalue reference version called." << std::endl; }
void foo(int &&x){ std::cout << "rvalue reference version called." << std::endl; }
int main() {
int x = 1;
foo(x); // 调用了左值引用版本的构造函数
foo(1); // 调用了右值引用版本的构造函数
return 0;
}
不过,如果我传入一个左值引用类型的右值呢,哪个重载版本会被调用呢?
foo(static_cast<int&>(x)); // 传入的参数是左值引用类型的右值
如果你像我之前只根据左值或者右值进行判断的话,那就会认为右值引用的版本会被调用。但是实际上被调用的是左值引用的版本,也就是 foo(int &x) 这个版本。因为我们传入的参数的类型是 int&,而且是一个右值,那么实际上对应的形参应该是 foo(int& &&),而根据引用折叠的规则,这实际上就是 foo(int&) 这个函数。那么对于如下这种入参,调用的又会是哪个版本呢?
foo(static_cast<int&&>(x)); // 传入的参数是右值引用类型的左值
被调用的会是右值引用的版本。因为入参是右值引用类型的右值,那么对应的形参是 int&& &&,根据引用折叠规则,这就是 int&&。