引用折叠、万能引用、完美转发

  2021-9-15 


C++11右值引用产生的一揽子问题

为什么引入右值引用?

为什么要引入右值引用?为了引入移动语义。移动操作防止copy,性能优化

但是右值引用引入之后又引出了许多问题

在C++中,左值引用初始化右值 和 引用的引用是禁止的,但是有一种情况下会造成左值引用初始化右值 与 引用的引用,即模板实参推断(typedef也能)。(注意,若明确调用模板函数不会触发模板实参推断)

template<typename T>
void fun(T &&uref){ }

int main()
{
    int a = 1;
    int &ref = a;
    fun(ref)   //此时,模板函数fun的【模板参数T被推断为int &】,则fun函数的参数类型为int & &&
}  //即出现了左值引用初始化右值 和 引用的引用是禁止

为了方便代码复用(好处是这个,这也是模板的目标),C++11支持了这两个特例(只能应用于模板实参推断造成的情况下)。标准库的std::move就用到了这个特性,可以非常方便地让我们实现接收任何类型转化为右值

具体如何方便就是下面的内容

再次强调,以下所有特性只支持编译器对模板实参进行推断造成的情况下,不能用户自己定义,也不能显示声明模板参数,否则不会触发模板实参推断

准备:两个特例

因此,C++11为这两种情况下开了特例

左值引用初始化右值:当我们将一个左值传递给函数的右值引用参数,且此右值指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型

引用的引用:如果间接造成了(模板实参推断/typedef)一个引用本身来初始化另一个引用,那么这种情况是支持的,且会发生引用折叠。

注意引用的引用 和 用引用的对象来初始化另一个引用的区别

int &b=c; int &a=b; //这是用b引用的对象c来初始化a引用,即新引用a指向对象c
int &b=c; int & &a=b; //这是用b引用本身来初始化a引用,即新引用a指向引用b 
//引用底层是个指针,所以这么也是合情合理的
//但上面第二行是错的,不能自己定义,只有编译器造成的这两个特例被支持。这里只是演示一下。

有了这两个特例,引用的引用就出现了四种情况:R2R,L2L,R2L,L2R

X& &  //左值引用的左值引用 L2L 
X& &&  //右值引用的左值引用 R2L
X&& &  //左值引用的右值引用 L2R
X&& &&  //右值引用的右值引用 R2R

但实际上X& &X& &&是不一定会发生的(不同编译器实现方式不同,见下面一段),因为这种情况下会被推断为用引用指向的对象初始化形参,避免了引用的引用。而后面几种就无法避免了,因为用左值(左值引用,右值引用都是左值)来初始化右值是无法成立的,所以只能被推断为用引用本身来初始化引用,这就成了引用的引用了。

对于用一个int&来初始化T&,其结果是一定的,初始化后模板函数的形参肯定为int&,但有两种可能的实现方式:①通过模板参数推断,Tint&指向的对象类型int(即推断实参为int来初始化形参),于是函数形参就是int& ②模板参数推断Tint&,即推断实参为int&来初始化形参,于是函数形参就是int& &,也就是引用的引用,触发引用折叠,变为int&。不通编译器的具体底层实现可能不一样,但它们的结果是一致的。

引用折叠

对于模板参数,引用的引用有四种情况,那就要写四个重载,非常麻烦

为了简化四种情况,引入引用折叠

引用折叠将模板实参推断后的函数参数表X& &,X& &&,X&& &折叠为左值,将X&& &&折叠为右值

万能引用

引用折叠的好处是,对于模板参数T,若模板函数参数表为T&&就可以同时接收左值和右值

这个 T&&就是指向模板参数类型的右值引用函数参数,即万能引用

因为X& &&折叠为X&X&& &&折叠为X&&正好模板实参T推断出来是左值X&则函数参数就折叠为左值,模板实参推断出来是右值X&&则函数参数就折叠为右值

万能引用就用到了上面:左值引用本身初始化右值特例+引用的引用特例+引用折叠特性 三种编译器支持特性

const T&也可以同时接收左值和右值,但是它无法修改引用的对象,使用很局限。

完美转发

完美转发即是std::forward(),修补了引入右值带来的右值短暂无法保持的问题。

右值一但沾上左值(比如传参,引用本身也是左值),就会变成左值。所以为了保持右值属性,就可以使用完美转发

完美转发:万能引用,左进左出,右进右出。当用于一个指向模板参数类型的右值引用函数参数(T&&;即万能引用)时,forward会保持实参类型的所有细节(左右值、const)

完美转发最有用的场合是配合万能引用使用的,因为万能引用才能一种形式同时可以接收左值右值const非const,所以才会向用到forward来保持特性。对于非万能引用的情况也能用,但和用move区别不大。

eg:

/*forward结合万能引用进行模板编程,大大提高程序复用性*/
template<typename T>
void fun1(T &&obj){  }

template<typename T>
void fun(T &&obj)  
{
    fun1(std::forward<T&&>(obj));  
}

int main()
{
    Obj obj;
    fun(std::move(obj));  //传右值引用ok
    Obj &ref = obj;  
    fun(ref);  //传左值引用ok
    fun(obj); //直接传左值变量,这里没有用到折叠,而是只用到了特例1:允许将一个左值传递给函数的右值引用参数,且此右值指向模板类型参数
}

上面funfun1的函数参数都是万能引用T&&,可以接收各种属性的值,然后再用forward保持传入特性,让我们方便对各种属性的值进行处理又不丢失属性。

下面是一般函数中保持右值属性。

/*这么用也彳亍,但用move也没区别,不过,为了语义明确,保持属性的常见推荐用forward*/
void fun1(Obj &&obj){  }
void fun(Obj &&obj)
{
    fun1(std::forward<Obj&&>(obj));  //main中传入右值,则保持右值特性,所以才能调用fun1函数
}
 int main()
 {
     Obj obj;
     fun(std::move(obj));
 }

move与forward

move本质上是通过static_cast<T&&>完成左值到右值的转换。std::move的参数表是一个万能引用,这样std::move就可以同时接受左值和右值。(若传入右值,则无事发生)

forward的实现和move是差不多的,唯一区别就是模板参数处理不同。

std::forward需要显示声明模板参数

为了保持右值性,为何用forward不用move

forward还能保持const特性

move是任何值强制转换为右值;forward是保持属性,原来实参是左值就返回左值,原来实参是右值就返回右值,虽然实现很相似,但功能性是完全不同的


且听风吟