This page looks best with JavaScript enabled

关于 std::move

 ·  ☕ 4 min read

最近在看一些框架的 c++ 源码中, 发现它们在许多函数传参时会使用 std::move 调用, 于是想弄清这个目的是什么.

std::move 函数定义

1
2
template< class T >
typename std::remove_reference<T>::type&& move(T&& t ) noexcept; // since C++11, until C++14
1
2
template< class T >
constexpr typename std::remove_reference<T>::type&& move(T&& t ) noexcept; // since C++14

概述: std::move 用于指示对象 t 可以“被移动”,即允许从 t 到另一对象的有效率的资源传递。

首先可以看到 move 函数的形参为T&& t, 返回值为 std::remove_reference<T>::type&&. 在 c++ 里, 我知道 T& 表示引用传参, 那 T&& 是什么意思 ?

&& 表示右值引用, 什么是右值引用?

右值引用

rvalue references

右值引用是相对于左值的一个概念, 在一个表达式中:

1
int i = 0;

i 就是左值, 左值能被赋值, 值能改变. 0 就是右值, 一般就是运算数. 在 c++11 之前, 右值是不能引用的, 最多就是用一个常量引用绑定右值:

1
const int& a = 0;

左值和右值传参

重载两个函数, 一个接收引用传参, 一个接收右值引用

1
2
3
4
5
6
7
void process(int& i) {
    cout << "LValue process: " << i << endl;
}

void process(int&& i) {
    cout << "RValue process: " << i << endl;
}
1
2
3
4
5
int main() {
    int a = 1;
    process(a);
    process(1);
}

输出:

LValue process: 1
RValue process: 1

右值引用的优点

右值引用是用来支持转移语义的。转移语义(Move Semantic)可以将资源 (堆,系统对象等) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。通过转移语义,临时对象中的资源能够转移其它的对象里。

  • 在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。
  • 普通的函数和操作符也可以利用右值引用操作符实现转移语义。

实例

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class StdMoveTest {
private:
    char* _data;
    size_t len;
    void _init_data(const char* d) {
        _data = new char[len+1];
        memcpy(_data, d, len);
        _data[len] = '\0';
    }

public:
    StdMoveTest(){
        len = 0;
        _data = NULL;
    }

    StdMoveTest(const char* d) {
        len = strlen(d);
        _init_data(d);
    }

    StdMoveTest(const StdMoveTest& obj) {
        cout << "Copy constructor: " << obj._data << endl;
        len = obj.len;
        _init_data(obj._data);
    }

    StdMoveTest& operator=(const StdMoveTest& obj) {
        cout << "Copy assgin: " << obj._data << endl;
        if(this != &obj){
            len = obj.len;
            _init_data(obj._data);
        }
        return *this;
    }

    virtual ~StdMoveTest(){
        cout << "destructor: " << this << " ";
        if(_data) {
            cout << _data;
            free(_data);
        }
        cout << endl;
    }
};

int main(int argc, const char * argv[]) {
    StdMoveTest t;
    t = StdMoveTest("Hello");

    vector<StefanJi::StdMoveTest> vec;
    vec.push_back(StdMoveTest("Word"));
    return 0;
}

输出:

Copy assgin: Hello
destructor: 0x7ffeefbff500 Hello
Copy constructor: Word
destructor: 0x7ffeefbff4c0 Word
destructor: 0x1006066f0 Word
destructor: 0x7ffeefbff518 Hello

可以看到将一个右值赋给左值时, 会调用赋值构造函数, 赋值构造函数里又会进行内存的分配. 将右值传递给 vector 时会调用拷贝构造函数生成一个新的对象. 从最后析构函数的日志中也能看出释放了两个 Hello, 两个 Word.

加上转移赋值构造函数转移拷贝构造函数重载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
StdMoveTest(StdMoveTest&& obj) {
    cout << "Move constructor: " << obj._data << endl;
    len = obj.len;
    _data = obj._data;
    obj.len = 0;
    obj._data = NULL;
}

StdMoveTest& operator=(StdMoveTest&& obj) {
    cout << "Move assgin: " << obj._data << endl;
    if(this != &obj){
        len = obj.len;
        _data = obj._data;
        obj.len = 0;
        obj._data = NULL;
    }
    return *this;
}

输出:

Move assgin: Hello
destructor: 0x7ffeefbff500
Move constructor: Word
destructor: 0x7ffeefbff4c0
destructor: 0x100603b20 Word
destructor: 0x7ffeefbff518 Hello

可以看到调用了重载的转移赋值构造函数转移拷贝构造函数, 而且析构函数的调用中能看出实际也是否了一个 Hello 和一个 Word, 从而避免了多余的内存分配.

在添加转移重载时需要注意几个点:

  • 形参的符号是右值引用符号 &&
  • 形参就不能使用 const 修饰了, 因为我们需要修改右值
  • 形参对对象的引用必须断开(obj._data = NULL), 因为当右值的析构函数调用时, 如果还存在对对象的引用, 那转移到新引用的资源会被释放

std::move

从上面的实例中看到只有右值引用才能调用转移构造函数转移赋值函数, 而所有命名对象都只能是左值引用. 如果想将左值引用右值引用来使用, 则能使用 std::move 函数将左值引用转换为右值引用.

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
    StdMoveTest hello = StdMoveTest("hello");
    StdMoveTest word = StdMoveTest("Word");
    
    vector<StefanJi::StdMoveTest> vec;
    vec.push_back(hello);
    vec.push_back(std::move(word));
    return 0;
}

输出:

Copy constructor: hello
Move constructor: Word
Copy constructor: hello
destructor: 0x1030979a0 hello
destructor: 0x10309b818 Word
destructor: 0x10309b800 hello
destructor: 0x7ffeefbff500 
destructor: 0x7ffeefbff518 hello

注意到 std::move 的形参为 T&& t, 但是它如何接收左值引用的 ? 因为 && 还有另外一个语义, 如果形参传递的是左值引用, 那么 t 就是左值引用, 如果传递的是右值引用, 那么 t 就是右值引用. 利用这个特点, 就能定义同时支持左值和右值得函数. 这个特性又叫 Perfect Forwarding(完美转发).

总结

  • 使用右值引用和转移语义能提高程序的性能, 在形参中使用 && 能让方法更简洁的同时支持左值和右值引用.
  • 利用 std::move 能方便的利用右值引用的特性

参考

Support the author with
alipay QR Code
wechat QR Code

Yang
WRITTEN BY
Yang
Developer