我刚刚听完有关podcast interview with Scott Meyers的软件工程电台C++0x。大多数新功能对我来说都是有意义的,除了一个功能,我现在对C++ 0x感到非常兴奋。我仍然不了解移动语义...到底是什么?

最佳答案

我发现用示例代码理解移动语义是最容易的。让我们从一个非常简单的字符串类开始,该类仅包含指向堆分配的内存块的指针:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

由于我们选择自己管理内存,因此我们需要遵循rule of three。我将推迟编写赋值运算符,现在仅实现析构函数和复制构造函数:
    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

复制构造函数定义复制字符串对象的含义。参数const string& that绑定(bind)到所有类型为string的表达式,使您可以在以下示例中进行复制:
string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

现在是对移动语义的关键了解。请注意,仅在复制x的第一行中才真正需要此深拷贝,因为我们可能想稍后再检查x,如果x有所更改,将感到非常惊讶。您是否注意到我只是说了x 3次(如果包括此句子,则说了4次),并且每次都表示完全相同的对象吗?我们将诸如x之类的表达式称为“左值”。

第2行和第3行中的参数不是左值,而是右值,因为基础字符串对象没有名称,因此客户端无法在以后的时间再次检查它们。
rvalues表示在下一个分号处销毁的临时对象(更精确地说:在词法上包含rvalue的完整表达式的末尾)。这一点很重要,因为在bc初始化期间,我们可以对源字符串做我们想做的任何事情,而客户端却无法分辨!

C++ 0x引入了一种称为“右值引用”的新机制,其中包括:
允许我们通过函数重载来检测右值参数。我们要做的就是编写一个带有右值引用参数的构造函数。在该构造函数中,我们可以对源执行任何操作,只要将其保持在某个有效状态即可:
    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

我们在这里做了什么?我们没有深度复制堆数据,而是仅复制了指针,然后将原始指针设置为null(以防止源对象的析构函数中的'delete []'释放我们的“被盗数据”)。实际上,我们已经“窃取”了最初属于源字符串的数据。同样,关键的见解是,在任何情况下客户都无法检测到源已被修改。由于我们实际上并未在此处进行复制,因此我们将此构造函数称为“移动构造函数”。它的工作是将资源从一个对象移动到另一个对象,而不是复制它们。

恭喜,您现在已经了解了移动语义的基础!让我们继续实现赋值运算符。如果您不熟悉copy and swap idiom,请学习并返回,因为它是与异常安全性相关的很棒的C++习惯用法。
    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

恩,就是这样吗? “右值引用在哪里?”你可能会问。 “我们在这里不需要它!”是我的答案:)

请注意,我们按值传递了参数that,因此that必须像其他任何字符串对象一样进行初始化。究竟that如何初始化?在C++98的旧时代,答案应该是“通过复制构造函数”。在C++ 0x中,编译器根据赋值运算符的参数是左值还是右值,在复制构造函数和move构造函数之间进行选择。

因此,如果您说a = b,则复制构造函数将初始化that(因为表达式b是一个左值),并且赋值运算符将内容与新创建的深拷贝交换。这就是 copy-and-swap 惯用法的确切定义-制作一个副本,将内容与该副本交换,然后通过保留范围来摆脱该副本。这里没有新内容。

但是,如果您说a = x + y,则move构造函数将初始化that(因为x + y表达式是一个右值),因此不涉及深层复制,仅涉及有效的移动。that仍然是该参数的独立对象,但其构造很简单,
由于不必复制堆数据,因此只需移动即可。无需复制它,因为x + y是一个右值,并且再次可以从右值表示的字符串对象中移出。

总而言之,复制构造函数会进行深层复制,因为源必须保持不变。
另一方面,move构造函数可以只复制指针,然后将源中的指针设置为null。可以用这种方式“无效化”源对象,因为客户端无法再次检查对象。

我希望这个例子能说明重点。重估引用和移动语义还有很多,我有意省略以保持简单。如果您需要更多详细信息,请参见my supplementary answer

关于c++ - 什么是 move 语义?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/3106110/

10-11 19:18