C++11中std::move和std::forward到底干了啥

前言

C++11中的右值引用的出现,在特定情况下减少了对象的拷贝,提升了C++程序的效率,伴随而来的 std::movestd::forward 也大量出现在程序代码中,但是这两个函数究竟干了啥呢?其实他们的本质都是转换函数,也就是完成左值和右值之间的转换,需要注意的是左值可以转换成右值,但是右值无法转换成左值。

关于左值、右值、左值引用和右值引用的概念可以看看之前的总结:

虽然温故不一定知新,但绝对可以增强记忆,本章的内容说起来很绕,我也是边学边总结,有不对的地方还请大佬们指出来。

左值引用和右值引用

了解过基础的引用知识之后我们都知道左值引用的形式为 T& t,一般会像成下面这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
class A{

private:
int n;
};

void test(A& obj) {
//...
}

A obj;
test(obj);

而右值引用是在左值引用的基础上多加一个&,形式变为 T&& t,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
class A{

private:
int n;
};

void test(A&& obj) {
//...
}

test(A());

这种通过 & 的个数区分左值引用和右值引用的方法,在大多数的普通函数中没有问题,但是放到模板参数或者 auto 关键字之后的位置就不太灵了,因为这些地方会推导实际的类型,正是有了参数推导,才使得模板中出现了“万能引用”的说法,也就是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
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

这个函数听起来好像是一个小人移动时调用的函数,但它却是一个把左值转化成右值的转化函数,我们看一下 std::move 函数的实现:

1
2
3
4
5
6
7
8
9
/**
* @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&& 了,先来看看它是什么,其实它的作用就是,移除类型的引用,返回原始类型。

std::remove_reference

它的可能实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream> // std::cout
#include <type_traits> // 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>();
}

运行结果

1
2
3
4
5
6
true
false
false
true
true
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @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 就是左值引用类型,调用这个函数时,_TpX& 类型,经过引用这的 _Tp&& => X& && => X&,所以返回值也是左值引用。

同理,第二个版本接受右值引用参数,返回值也是一个右值引用。

从目前的情况来看,std::forward 好像什么也没做,只是将参数强转以后返回,如果不使用这个函数会有什么问题呢?

必要性

为什么要使用 std::forward 我们可以通过一个例子来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <utility>

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;
}

看到这个例子可以先思考一下,运行结果会是什么呢?可能和你想的有点不一样哦,看看下面的答案:

1
2
lvalue refrence: val=1021
lvalue refrence: val=501

有点出乎意料啊,为什么 Print(int&& val) 这个函数没有被调用呢?原因在于“右值引用是一个左值”,很懵对不对,接着往下看:

1
2
3
4
int i = 101;
int& li = i;

int&& ri = 120;

这段代码中哪些是左值,哪些是右值呢?可以肯定的是 ili 是左值, 101120 是右值,而ri也是左值,因为它也一个可以取地址并长期有效的变量啊,只不过这个左值引用了一个右值而已。

接着回到刚才的例子,TPrint(501); 调用模板函数时,T被推导为 int,所以模板被实例化为:

1
2
3
void TPrint(int&& t) {
return Print(t);
}

运行到这里,t 实际上是一个左值,所以调用了 void Print(int& val) 这个函数,那么怎样才能调用 void Print(int&& val) 这个版本呢?是时候请出 std::forward 函数了,将模板函数进行如下修改:

1
2
3
4
template<typename T>
void TPrint(T&& t) {
return Print(std::forward<T>(t));
}

修改之后再来分析一下,TPrint(501); 调用模板函数时,T被推导为 int,所以模板被实例化为:

1
2
3
void TPrint(int&& t) {
return Print(std::forward<int>(t));
}

这里会调用 std::forward 的这个版本:

1
2
3
4
5
6
7
8
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::forward 到底干了啥

它和 std::move 一样,std::forward 也是做了一个强制类型转换,当形参成为左值引用时把它转换成左值引用返回,当形参成为右值引用时把它转换成右值引用返回。

总结

  • std::move 并没有实际的“移动”操作,只是在内部进行了强制类型转换,返回一个相关类型的右值引用
  • std::move 的名字主要标识它后续可能会被其他人“掏空”,调用它之后如果继续使用,行为未定义,后果自负
  • std::forward 的本质也是进行强制类型转换,形参为左值时返回左值引用,形参为右值时返回右值引用
  • 从定义入手可以理解很多花里胡哨的东西,透过现象看其本质。

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

日拱一卒无有尽,功不唐捐终入海~

我们追求的样子:十分沉静,九分气质,八分资产,七分现实,三分颜值,二分糊涂,一份自知之明。

2021-7-18 21:23:01

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客