From C biancheng

http://c.biancheng.net/view/7868.html

C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。

template <typename T>
class A {
    // 注意这里并非完美转发,因为T是类模板参数,而非函数模板参数
    void foo(T&&);  // 只能接收右值引用
 
    template <typename U>
    void bar(U&&);  // 这里就是完美转发,可以接受左值/右值引用
};

典型案例

#include <iostream>
using namespace std;
 
void foo(int& ) { cout << "lvalue\n"; }
void foo(int&&) { cout << "rvalue\n"; } // 这是普通的右值引用,非万能引用
 
template<class T>
void bar(T&& x) { // `T&&` 称为万能引用,此外 `auto&&` 也是万能引用的常见形式
    foo(std::forward<T>(x)); // 使用 std::forward 配合万能引用实现完美转发
}
 
int main() {
    int a = 1;
    bar(a); // lvalue, 模板实例:bar<int&>(int&)
    bar(1); // rvalue, 模板实例:bar<int>(int&&)
    bar(std::move(a)); // rvalue, 模板实例:bar<int>(int&&)
    return 0;
}

通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?

std::forward解君愁

bar函数内部,x已经具有名字,无论无何都是一个左值。只有通过std::forward才能将左/右值属性正确传递。下面是标准库中的实现,

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }
 
  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

可以看到有两个模板重载,对于左值和右值会分别匹配一个模板。返回值的类型都是万能引用。下面分类讨论,但须得注意,_Tp的类型是推导出来的,并非传参指定的(推导规则日后再叙)。

  1. 如果传了一个左值int_Tp的类型为int&,匹配第一个重载
  2. 如果传了一个右值int_Tp的类型为int&&,匹配第二个重载

接着,经过static_cast<_Tp&&>(__t),触发引用折叠。

  1. 第一个重载中,__t的类型为int&,然后static_cast<int&&>(int&)折叠为int&,返回出去就得到一个左值引用,这保留了参数的左值属性。
  2. 第二个重载中,__t的类型为int&&,然后static_cast<int&&>(int&&)折叠为int&&,返回出去就得到一个右值引用,这保留了参数的右值属性。

这个时候反过来看上面的典型案例,应该能够理解完美转发的原理了。

  1. 当传a时,是一个左值,所以模板实例化bar<int&>(int&)
  2. 1时,是一个右值,所以模板实例化bar<int>(int&&)bar<int&&>(int&&)
  3. move(a)时,是一个右值,省略……

下面构造一段代码,触发第二个重载的断言,

#include <utility>
 
template<class T>
void g(T&& x) {
    std::forward<T>(std::move(x)); // 故意写错
}
 
int main() {
    int a = 1;
    g(a); // #1
    g<int&>(a); // #2
}

上面代码中,1和2是等价的调用。因为a是左值,1推导出来就是2,2只是手工写出来而已。但是编译会报错,因为g的模板类型参数实例化为int&,而对forward的传参会调用接收右值版本的重载。所以会触发这个断言,

static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");

总结一下,std::forward的之所以能奏效归根结底是C++模板匹配/类型推导的能力,并非什么黑魔法。

引用折叠

C++不允许定义引用的引用。但在模板实例化,autodecltype/using等类型推导时,经常会出现引用的引用。因此必须定义一套规则对此进行处理,这个规则就是引用折叠。具体规则为,

  • & + & = &
  • & + && = &
  • && + & = &
  • && + && = &&

一言蔽之,只有两个都是&&才会折叠为&&

模板实例化触发引用折叠

void bar(int&)  { cout << "L\n"; }
void bar(int&&) { cout << "R\n"; }
 
template <class T>
void foo(T&& x) {
	std::is_lvalue_reference<T>::value ?
		cout << "lvalue\n" :
		cout << "rvalue\n";
	bar(std::forward<T>(x));
	bar(x); // note that the expression `x` is a lvalue
			// whatever `x` itself is lvaue or rvalue.
}
 
int main() {
	int a = 1;
	foo(a);
	foo(1);
	return 0;
}
 
/* outputs:
lvalue
L
L
rvalue
R
L
*/
  • T推导为int&int& &&折叠为int&
  • T推导为int&&int&& &&折叠为int&&

auto 推导触发引用折叠

int a = 1;
auto r1 = a;
auto& r2 = a;
auto&& r3 = a;
auto&& r4 = 1;
cout << std::boolalpha;
cout << "r1 is lvalue reference? " << std::is_lvalue_reference<decltype(r1)>::value << "\n";
cout << "r2 is lvalue reference? " << std::is_lvalue_reference<decltype(r2)>::value << "\n";
cout << "r3 is lvalue reference? " << std::is_lvalue_reference<decltype(r3)>::value << "\n";
cout << "r4 is lvalue reference? " << std::is_lvalue_reference<decltype(r4)>::value << "\n";
 
/* outputs:
r1 is lvalue reference? false
r2 is lvalue reference? true
r3 is lvalue reference? true
r4 is lvalue reference? false
*/
  1. 普通auto会去处cv修饰和引用,auto=int,r1推导为int,没问题
  2. 普通auto依然推导为int,auto=int,(a是左值,也可推导为int&,加上引用得到int& &,引用折叠后)得到r2的类型是int&
  3. 万能引用auto,此时auto=int&(a是左值),加上引用得到int& &&,引用折叠后r3的类型为int&
  4. 万能引用auto,auto=int,(也可推到为int&&,加上引用得到int&& &&,引用折叠后)r4的类型为int&&

注意,这里有一个原则。对于r2,其实auto推导为int&,引用折叠后r2的类型还是int&,秉着越简单越好的原则,直接将auto推导为int。对于r4,由于1是纯右值,所以auto可以推导为int&&,但还有更简单的int,也不影响最终r4的类型,所以编译器倾向于将auto推导为int。

decltype/using 触发引用折叠

int a = 1;
decltype(a) r1 = a;
decltype((a)) r2 = a;
decltype((a))&& r3 = a;
using MyType = decltype((a));
MyType& r4 = a;
// MyType&& r5 = 1; // int& && -> int& -> err:
				 // Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
decltype(move(a)) r5 = 1; // ok, r5 is int&&
decltype(move(a))& r6 = 1; // same error above
decltype(move(a))&& r7 = 1; // ok, r7 is int&& && -> int&&
 
cout << std::boolalpha;
cout << "r1 is lvalue reference? " << std::is_lvalue_reference<decltype(r1)>::value << "\n";
cout << "r2 is lvalue reference? " << std::is_lvalue_reference<decltype(r2)>::value << "\n";
cout << "r3 is lvalue reference? " << std::is_lvalue_reference<decltype(r3)>::value << "\n";
cout << "r4 is lvalue reference? " << std::is_lvalue_reference<decltype(r4)>::value << "\n";
  1. r1推导为int
  2. 表达式(a)是左值,r2推导为int&
  3. r3推导为int& &&,引用折叠为int&
  4. r4推导为int& &,引用折叠为int&

小结

引用折叠只发生在类型推导的过程中。常出现在模板实例化、auto、decltype、using等场景。

One more pitfall

在典例代码中,如果,

int a = 1;
int&& b = move(a);
bar(b);

会输出什么呢?