如何理解移动语义

C++11引入了移动语义(move semantic)的概念,如何简明快速的理解它呢?

在stackoverflow上看见了一个简化版,阐述的非常清楚,原文是英文,翻译如下。

 

通过示例代码,理解移动语义是最容易的。

让我们从一个非常简单的字符串类开始,它只保存一个指向堆分配内存块的指针。代码如下:

#include <cstring>

#include <algorithm>

 

class string

{

    char* data;

 

public:

 

    string(const char* p)

    {

        size_t size = strlen(p) + 1;

        data = new char[size];

        memcpy(data, p, size);

    }

因为我们选择自己管理内存,所以需要遵循三条规则。

我将推迟编写赋值操作符函数,仅仅先实现析构函数和拷贝构造函数:

    ~string()

    {

        delete[] data;

    }

 

    string(const string& that)

    {

        size_t size = strlen(that.data) + 1;

        data = new char[size];

        memcpy(data, that.data, size);

    }

拷贝构造函数定义了拷贝字符串对象的方法。

参数const string& that 绑定到字符串类型的所有表达式,它允许在以下示例中创建副本:

string a(x); // Line 1

string b(x + y); // Line 2

string c(some_function_returning_a_string()); // Line 3

现在来了解移动语义的关键内涵。

注意只有第一行的拷贝x是真正需要深度拷贝的,因为后续我们可能需要检查x,如果x以某种方式发生了变化,我们都会惊讶。

有没有注意到,x我说了三次(如果包括这句话,四次),而每次都意味着是完全相同的对象?

我们把像x这样的表达式称为“左值”。

第二行和第三行中的参数都不是左值(lvalues),而是右值(rvalues),

因为底层字符串对象没有名字,所以用户无法在以后再次检查它们。

右值表示在下一个分号处被销毁的临时对象(更确切地说:在完整表达式的末尾包含着右值)。

这很重要,因为在初始化b和c的过程中,我们可以对源字符串做任何我们想做的事情,而用户无法区分。

C ++ 0x引入了一种称为“右值引用”(rvalue reference)的新机制(mechanism),允许我们通过函数重载检测右值参数。

我们要做的只是编写一个参数为右值引用的构造函数。

在这个构造函数内部我们可以对原数据做任何操作,只要我们把它放置到一个有效状态。

    string(string&& that) // string&& is an rvalue reference to a string

    {

        data = that.data;

        that.data = nullptr;

    }

我们做了些什么? 不是深度拷贝堆上的数据,仅仅是复制了指针,然后把原始指针置为null。

效果上,我们把原来属于原始字符串的数据“偷”走了。

关键内涵是,在任何情况下,用户都无法检测到源(string&& that)已经被修改。

由于我们在这里没有真正实现一个副本,我们称这个构造函数为“移动构造函数”。

它的工作就是把资源从一个对象转移到另一个对象上,而不是复制他们。

    

 

恭喜,你已经理解了移动语义的基本含义。

现在我们来实现赋值操作符。

如果你不熟悉“拷贝交换”用法,去学习一下再回来,因为这是一个非常棒的与异常安全相关的C ++习惯用法。

string& operator=(string that)

    {

        std::swap(data, that.data);

        return *this;

    }

};

 

呃,就是这样? “右值引用在哪里?” 你可能会问。 

“我们这里不需要它!” 是我的回答 :)

请注意,我们按值传递参数,因此必须像其他字符串对象一样进行初始化。

到底如何进行初始化呢?

 在C ++ 98的古老日子里,答案应该是“通过拷贝构造函数”。

在C++0x时代,编译器会根据赋值操作符函数的参数是左值还是右值来选择拷贝构造函数和移动构造函数。

 

如果你说a=b,拷贝构造函数将会初始化that (因为表达式b是一个左值),赋值操作符用新创建的深拷贝交换内容。 这就是“拷贝交换”习惯用法的定义 - 制作副本,与副本交换内容,然后通过离开范围销毁副本。 没什么新东西。

如果你说a=x+y,移动构造函数将会初始化that (因为表达式x+y是一个右值),所以这里没有深拷贝调用,只是有效的移动。that 仍然是一个独立的对象,但它的构造微不足道,因为堆数据没有拷贝,仅仅是移动。因为x+y是一个右值,所以没有必要复制它。同样,从表示为rvalues的字符串对象移动也是ok的。

总之,拷贝构造函数会生成深层副本,因为源必须保持不变。 另一方面,移动构造函数可以仅仅复制指针,然后将源中的指针设置为null。以这种方式“废止”源对象是ok的,因为用户是无法再次检查对象的。

希望这个例子抓住了重点。

有关右值引用和移动语义的知识非常多,这里我有意简化它们。

如果想了解更多细节,请参考我的补充回答。

 

https://en.wikipedia.org/wiki/Rule_of_three_%28C++_programming%29

https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom

https://en.wikipedia.org/wiki/C++#Standardization

原文:https://stackoverflow.com/questions/3106110/what-are-move-semantics/11540204#11540204

 

posted @ 2018-03-25 18:55  envoy  阅读(576)  评论(0编辑  收藏  举报