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