理解 std::move

作为 C++ 使用移动语义的重要工具,终于出场了。

前言

真正理解引用理解移动语义中,我们已经知道了std::move是标准库提供给我们使用移动语义的重要工具,它可以将一个左值转换为对应的右值引用。由于std::move可以接受任何类型的实参,所以它显然是一个函数模板。
同时,我们前面也简单讨论过了模板的一些用法,现在就开始理解std::move吧。

std::remove_reference

在理解std::move之前,我们得先知道std::remove_reference的功能。
std::remove_reference是 C++11 标准库提供的类型转换(type transformation)模板,它有一个模板类型参数T和一个名为type的(public)类型成员。如果我们用一个左值引用类型实例化std::remove_reference,那么type就是被引用的类型。比如,实例化std::remove_reference<int&>,那么type就是int;实例化std::remove_reference<std::string&>,那么type就是std::string
PS:实际上,这个模板的功能从它的命名中也可以发现。

std::move

好了,现在进入主题。

定义

首先直接给出std::move的定义(以下定义来自 MSVC 2019):

1
2
3
4
5
6
7
template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;
//...
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

实际上,MSVC 这个std::move的定义是 C++14 开始使用的,我们把它改成 C++11 的版本并删去和 MSVC 相关的内容:

1
2
3
4
template <typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename remove_reference<T>::type&&>(t);
}

现在看起来清爽了一些,从上面的代码中,我们可以知道:std::move是一个函数模板,它不会抛出异常,返回类型是remove_reference<T>::type&&,这是一个右值引用,整个函数做的事情也很简单,就是将传递给std::move的参数t使用static_cast转换为与返回值相同的类型并返回。

如何工作

接着,我们来分析一下std::move如何工作的,以下面这段代码为例:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <utility>

int main() {
std::string s;
s = std::move(std::string("xxx"));

return 0;
}

这段代码很简单,我们定义了一个std::string变量s,并将一个临时std::string对象通过移动赋值运算符赋值给了s,我们来分析其中的过程:

  1. std::move的模板参数是T&&,这是一个右值引用,而传递给它的参数是一个临时对象(右值,或者说将亡值),此时推断出T的类型是std::string,而t的类型就是std::string&&
  2. std::remove_reference使用std::string实例化,从而std::remove_referecnce<std::string>::type就是std::string
  3. std::move的返回类型就是std::string&&,所以这个函数实例化后的版本可以写出:

    1
    2
    3
    std::string&& move(std::string&& t) noexcept {
    return static_cast<std::string&&>(t);
    }
  4. 由于t的类型已经是std::string&&,于是static_cast什么也不做,最终std::move返回的结果就是它接受的右值引用。

下面,我们假设传递给std::move的是一个左值来分析其中的过程:

  1. 推断出T的类型是std::string&,按照引用折叠t的类型就是std::string&
  2. std::remove_reference使用std::string&实例化,从而std::remove_referecnce<std::string&>::type就是std::string
  3. std::move的返回类型就是std::string&&,所以这个函数实例化后的版本可以写出:

    1
    2
    3
    std::string&& move(std::string& t) noexcept {
    return static_cast<std::string&&>(t);
    }
  4. 由于t的类型已经是std::string&,于是static_cast会将其转换为std::string&&,最终std::move返回的结果还是它接受的右值引用。

小结

在上面的讨论中,我们发现:

  1. 无论std::move的参数是左值还是右值,最终得到的都是一个右值引用。
  2. 无法隐式的将一个左值转换为右值引用,但我们可以使用static_cast显式的将一个左值转换为一个右值引用。

结语

本文讨论了std::move的工作原理,根据我们的讨论,可以发现std::move的理论基础包括:类型转换、函数模板参数推导、右值引用、引用折叠等一系列知识点,缺任何一个部分,好像都无法完全理解它😓。
另外,在之前的文章中,我们还提到过不要在函数的返回语句中使用std::move,比如:

1
2
3
4
5
6
7
8
9
class A {
public:
~A() { }
};

A func() {
A a;
return std::move(a);
}

上述代码中,A类没有移动构造函数,若直接返回a,此时编译器会做返回值优化(RVO),从而避免多余的拷贝。但若是上面的形式,则会将std::move(a)这个表达式得到的右值进行拷贝后返回,反而产生了多余的拷贝。

最后,其实我们还有一个问题没有提到,那就是在使用move的场景中,我们全部写成了std::move的形式,这是为什么呢?
这里就不继续讨论了,留给下次吧。


Buy me a coffee ? :)
0%