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&
,但有两种可能的实现方式:①通过模板参数推断,T
是int&
指向的对象类型int
(即推断实参为int
来初始化形参),于是函数形参就是int&
②模板参数推断T
为int&
,即推断实参为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:允许将一个左值传递给函数的右值引用参数,且此右值指向模板类型参数
}
上面fun
和fun1
的函数参数都是万能引用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
是保持属性,原来实参是左值就返回左值,原来实参是右值就返回右值,虽然实现很相似,但功能性是完全不同的