元芳你怎么看

本网站主要用于记录我个人学习的内容,希望对你有所帮助

0%

条款23:理解std::move 和 std::forward

须知

std::move 不做任何移动, std::forward 不做任何转发。它们在运行的时候什么都没干。
它们两个都只做了强制类型转换std::move 无条件将实参强制转换成右值,std::forward 是有条件的执行强制类型转换。

std::move

我们来看一看c++11中std::move的示例实现:

1
2
3
4
5
template <typename T>
typename std::remove_reference<T>::type&& move(T&& param) {
using ReturnType = typename std::remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}

这里也和条款9呼应,也就是别名声明(using)压倒typedef的优势:支持模板化!而typedef需要结构体来辅助完成同样功能。如果我们要使用typedef来实现同样的功能的话,代码如下:

1
2
3
4
5
6
7
8
9
10
template <typename T>
struct StructReturnType {
typedef typename std::remove_reference<T>::type&& type;
};

template <typename T>
typename std::remove_reference<T>::type&& move(T&& param) {
return static_cast<typename StructReturnType<T>::type>(param);
}

如果是c++14,有了返回值型别推导,实现std::move就更加方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
decltype(auto) move(T&& param) {
using ReturnType = std::remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}

int main() {
int x = 42;
int& x_ref = x;
int&& rvalue_ref_1 = move(x); //编译通过,说明强制转换成功
int&& rvalue_ref_2 = move(x_ref);
return 0;
}

std::move只做强制类型转换,不做移动。当然右值是可以实施移动的,所以一个对象实施了std::move后就告诉编译器该对象可能具备移动的条件。为什么是可能呢?通常情况下的确没有问题,具备移动的条件。看下面这个例子:

1
2
3
4
5
class Entity{
public:
explicit Entity(std::string tmp);
//...
};

这里的explicit 是一个关键字,通常用于类的构造函数声明中,用于阻止隐式类型转换。当一个构造函数被标记为 explicit 时,它告诉编译器不要执行隐式类型转换,只有显式调用时才会使用该构造函数。

Entity类的构造函数不需要修改tmp,根据优良传统“只要有可能使用const就使用”,将代码更改成了下面的形式:

1
2
3
4
5
6
7
class Entity{
private:
std::string value;
public:
explicit Entity(const std::string tmp)
: value(std::move(tmp)){}
};

代码顺利运行,但是tmp是被复制近value的,而不是移动。std::move(tmp)后结果是右值const std::string,常量性保留下来了。
可以浅浅的看一下string的头文件:

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
26
27
28
29
30
31
32
33
34
35
36
   //string 文件
using string = basic_string<char>;

//basic_string.h 文件

//复制构造函数
basic_string(const basic_string& __str)
: _M_dataplus(_M_local_data(),
_Alloc_traits::_S_select_on_copy(__str._M_get_allocator()))
{
_M_construct(__str._M_data(), __str._M_data() + __str.length(),
std::forward_iterator_tag());
}

//移动构造函数
basic_string(basic_string&& __str) noexcept
: _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator()))
{
if (__str._M_is_local())
{
traits_type::copy(_M_local_buf, __str._M_local_buf,
__str.length() + 1);
}
else
{
_M_data(__str._M_data());
_M_capacity(__str._M_allocated_capacity);
}

// Must use _M_length() here not _M_set_length() because
// basic_stringbuf relies on writing into unallocated capacity so
// we mess up the contents if we put a '\0' in the string.
_M_length(__str.length());
__str._M_data(__str._M_local_data());
__str._M_set_length(0);
}

可以看到移动构造的函数只能接受非常量的string类型的右值引用作为形参。因为指涉到常量的左值引用允许绑定在一个常量右值性别的形参,最终调用的是string的复制构造函数(即使tmp为右值)。

通过这个例子,我们可以学习到:

  • 如果想取得对某个对象执行移动操作的能力,不要将其声明为常量。
  • std::move不能保证强制型别转换的对象具备可移动的能力。
  • 唯一可以确定的,结果是个右值。

std::forward

需要注意的是:传递给 std::forward 的实参型别应当是个非引用型别,因为习惯上它编码的所传递
实参应该是个右值(参见条款 28)。
std::forward 仅仅在特定情况下会实施强制类型转换。最常见的就是某个函数模板取用了万能引用型别作为形参,传递给另一个函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void func(const Entity& left);
void func(Entity&& right);

template<typename T>
void func2(T&& param)
{
// ...
func(param);
func(std::forward<T>(param));
// ...
}

{
std::string str("hello");
Entity e(str);
func2(e);
func2(std::move(e));
}

我们希望func2传入的是一个左值时,执行func的左值的版本,传入的是个右值的时候执行func重载的右值的版本。但是函数形参都是左值,也就是说这里的param一直是左值,不论传入func2的是左值还是右值,都只会执行func的左值版本。此时,std::forward就做到了这件事:仅当实参是右值完成初始化的时候才会执行向右值的强制类型转换。

那么std::forward是如何知道实参是否通过右值来完成初始化的呢?其实是通过传入的函数模板形参T来实现的(详细参见条款 28)。