引用折叠和完美转发

模板参数推导

模板参数有三种写法:

1
2
3
4
5
6
7
8
template <typename T>
void Foo(T) { static_assert(false); } // 直接使用T

template <typename T>
void FooWithRRef(T&) { static_assert(false); } // 使用T&

template <typename T>
void FooWithLRef(T&&) { static_assert(false); } // 使用T&&

根据《C++ Templates 2》的说法,第一种写法会decay(等同于调用std::decay),即:

  • 所有的const, volatile限定符会被移除
  • 所有的引用(不管左值右值)会被移除
  • 数组会退化为指向数组的指针,函数会退化为指向函数的指针

所以会有:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Foo(1) -> 参数真正类型:int&&  T推导结果int
    
int a;
Foo(a) -> 参数真正类型:int  T推导结果int
    
int&& a = 123;
Foo(std::move(a)) -> 参数真正类型:int&&  T推导结果int
    
const int& a = b;
Foo(a) -> 参数真正类型:const int&  T推导结果int

第二种写法会保留所有参数原本的类型,const不会被丢弃,引用也会被保留。

但是有个问题:引用折叠,即当存在多个引用时,各个引用之间会发生折叠:

类型1 类型2 结果类型
T T& T&
T& T& T&
T&& T& T&
T&& T&& T&&

也就是说,只有两个类型都是右值引用的时候,最后推导结果才会是左值引用。否则全部变为左值引用。

那么由于引用折叠的存在,FooWithRRef()的参数类型一定是右值引用(因为左值会被折叠掉变为右值)。

那么要想做到传递左值,就必须使用FooWithLRef()并且传入参数是左值引用类型,这样引用折叠才会折叠为左值引用。

完美转发

使用FooWithLRef(T&&)仍旧有一个问题,就是在此函数内将参数传递给另一个函数时会有问题:

1
2
3
4
5
6
7
void anotherFunc(int& a);
void anotherFunc(int&& a);

template <typename T>
void FooWithLRef(T&& value) {
    anotherFunc(value);	// 调用的是哪个?
} 

这里,如果传递左值FooWithLRef(std::move(a)),那么T会被推导为int,那么value的类型就是int&&。这样传入的是左值引用。

但是左值引用本身是右值,所以在传入anotherFunc的时候,仍旧是按照右值传递,这样会调用第一个函数,永远不会调用第二个函数。

当然,可以强制使用anotherFunc(std::move(a))进行左值版本调用。那这样右值版本将永不调用。

这个时候就需要使用完美转发std::forward<T>(a),它会将左值引用再次变为左值,而右值引用仍旧是右值。

其他

关于FooWithLRef(T&&)还有一个注意点,就是如果传入一个左值,如:

1
FooWithLRef(1);

T会被推导为int,所以value的类型是T&&->int&&

而如果传入右值或普通值:

1
2
int& a = b;	// 或int a = 123;
FooWithLRef(a);

那么T会被推导为int&,根据引用折叠,value类型是int& && ->int&

而很离谱的是,如果你使用的是FooWithRRef(T&),传入普通类型并不会使T变为int&而是int,最后value会被推导为int&

updatedupdated2023-08-102023-08-10