前言
C++11中的右值引用的出现,在特定情况下减少了对象的拷贝,提升了C++程序的效率,伴随而来的 std::move
和 std::forward
也大量出现在程序代码中,但是这两个函数究竟干了啥呢?其实他们的本质都是转换函数,也就是完成左值和右值之间的转换,需要注意的是左值可以转换成右值,但是右值无法转换成左值。
关于左值、右值、左值引用和右值引用的概念可以看看之前的总结:
虽然温故不一定知新,但绝对可以增强记忆,本章的内容说起来很绕,我也是边学边总结,有不对的地方还请大佬们指出来。
左值引用和右值引用
了解过基础的引用知识之后我们都知道左值引用的形式为 T& t
,一般会像成下面这样使用:
1 | class A{ |
而右值引用是在左值引用的基础上多加一个&
,形式变为 T&& t
,使用方式如下:
1 | class A{ |
这种通过 &
的个数区分左值引用和右值引用的方法,在大多数的普通函数中没有问题,但是放到模板参数或者 auto
关键字之后的位置就不太灵了,因为这些地方会推导实际的类型,正是有了参数推导,才使得模板中出现了“万能引用”的说法,也就是下面这样:
1 |
|
函数 func
即能接受变量 year
这样的左值作为参数,也能接受 2020
这样的常数作为右值,简直太完美。那么这里是怎样推导的呢?这就要请出一个引用的“折叠”规则了,描述如下:
A& & 折叠成 A&
A& && 折叠成 A&
A&& & 折叠成 A&
A&& && 折叠成 A&&
根据这个规则,func
函数在接受 year
作为参数时应该是一个左值引用,那么模板参数 T
会被推到为 A&
与后面的 &&
折叠为 A&
,接受 year
没问题。而这个函数在接受 2020
作为参数时应该是一个右值引用,那么模板参数 T
会被推导成 A
,与后面的 &&
形成 A&&
,可以接受右值,知道了这些基础知识我们接着往后看。
std::move
这个函数听起来好像是一个小人移动时调用的函数,但它却是一个把左值转化成右值的转化函数,我们看一下 std::move
函数的实现:
1 | /** |
这是一个模板函数,一共才4行,好像最麻烦的就是这个 std::remove_reference<_Tp>::type&&
了,先来看看它是什么,其实它的作用就是,移除类型的引用,返回原始类型。
std::remove_reference
它的可能实现如下:
1 | template <typename T> |
它的作用可以参考 cppreference.com - remove_reference,示例如下:
1 |
|
运行结果
1 | true |
从这个例子可以清晰的看出 std::remove_reference
就是返回去掉引用的原始类型。
static_cast
明白了上面 std::remove_reference
的作用,整个 std::move
函数就剩下一个 static_cast
函数了,其实到这里也就清晰了,std::move
函数的作用就先通过 std::remove_reference
函数得到传入参数的原始类型 X
,然后再把参数强转成 X&&
返回即可,参数的 _Tp
的推导参考引用折叠规则。
std::move 到底干了啥
通过前面的一通分析我们发现,std::move
的内部只做了一个强制类型转换,除此之外啥也没干,其实就是对传入的参数重新解释了一下,并没有实质性的动作。
那么为什么要使用 std::move
这个名字呢?这个名字更多的是起到提醒的作用,告诉使用者这里可能进行了到右值的转化,相关的对象后续可能发生移动,“被掏空”了,如果你继续使用这个对象,行为是未定义的,后果自负。
std::forward
std::forward
被称为完美转发,听起来和 “万能引用”一样厉害,使用的头文件为 <utility>
,在 /usr/include/c++/5/bits/move.h
文件中的定义如下:
1 | /** |
std::forward
用于函数模板中完成参数转发任务,我们必须在相应实参为左值,该形参成为左值引用时把它转发成左值,在相应实参为右值,该形参成为右值引用时把它转发成右值。
有了前面的铺垫我们直接来分析代码吧,第一个版本接受参数苏为左值引用的情况,因为 std::remove_reference<_Tp>::type
是 _Tp
的原始类型,所以 t
就是左值引用类型,调用这个函数时,_Tp
为 X&
类型,经过引用这的 _Tp&& => X& && => X&
,所以返回值也是左值引用。
同理,第二个版本接受右值引用参数,返回值也是一个右值引用。
从目前的情况来看,std::forward
好像什么也没做,只是将参数强转以后返回,如果不使用这个函数会有什么问题呢?
必要性
为什么要使用 std::forward
我们可以通过一个例子来看一下:
1 |
|
看到这个例子可以先思考一下,运行结果会是什么呢?可能和你想的有点不一样哦,看看下面的答案:
1 | lvalue refrence: val=1021 |
有点出乎意料啊,为什么 Print(int&& val)
这个函数没有被调用呢?原因在于“右值引用是一个左值”,很懵对不对,接着往下看:
1 | int i = 101; |
这段代码中哪些是左值,哪些是右值呢?可以肯定的是 i
、li
是左值, 101
、120
是右值,而ri
也是左值,因为它也一个可以取地址并长期有效的变量啊,只不过这个左值引用了一个右值而已。
接着回到刚才的例子,TPrint(501);
调用模板函数时,T被推导为 int
,所以模板被实例化为:
1 | void TPrint(int&& t) { |
运行到这里,t
实际上是一个左值,所以调用了 void Print(int& val)
这个函数,那么怎样才能调用 void Print(int&& val)
这个版本呢?是时候请出 std::forward
函数了,将模板函数进行如下修改:
1 | template<typename T> |
修改之后再来分析一下,TPrint(501);
调用模板函数时,T被推导为 int
,所以模板被实例化为:
1 | void TPrint(int&& t) { |
这里会调用 std::forward
的这个版本:
1 | template<typename _Tp> |
函数的返回类型为 int&&
,然后就调用了 void Print(int&& val)
这个版本的打印函数。
疑惑
可能有人会说,这不对啊,使用 std::forward
修改之前函数参数就是 int&&
类型,修改之后得到的返回值还是 int&&
类型,这有什么区别吗?
这里的区别就在于,使用 std::forward
之前的 int&&
是有名字的变量 t
,它是一个左值,而使用 std::forward
之后的 int&&
是有个匿名变量,它是一个右值,真正的差距就在这里。
std::forward 到底干了啥
它和 std::move
一样,std::forward
也是做了一个强制类型转换,当形参成为左值引用时把它转换成左值引用返回,当形参成为右值引用时把它转换成右值引用返回。
总结
std::move
并没有实际的“移动”操作,只是在内部进行了强制类型转换,返回一个相关类型的右值引用std::move
的名字主要标识它后续可能会被其他人“掏空”,调用它之后如果继续使用,行为未定义,后果自负std::forward
的本质也是进行强制类型转换,形参为左值时返回左值引用,形参为右值时返回右值引用- 从定义入手可以理解很多花里胡哨的东西,透过现象看其本质。
日拱一卒无有尽,功不唐捐终入海~
我们追求的样子:十分沉静,九分气质,八分资产,七分现实,三分颜值,二分糊涂,一份自知之明。
2021-7-18 21:23:01