c++ - 什么是 move 语义?

标签 c++ c++-faq c++11 move-semantics

我刚刚听完有关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绑定到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(如果包括此句子,则四遍),每次都表示完全相同的对象吗?我们将诸如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是左值),并且赋值运算符将内容与新创建的深层副本交换。这就是复制和交换惯用法的确切定义-制作一个副本,将内容与该副本交换,然后通过保留范围来摆脱该副本。这里没有新内容。

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

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

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

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

相关文章:

c++ - 具有可变参数模板和 va_args 的 function_traits

c++ - 什么是 undefined reference /未解析的外部符号错误以及如何修复它?

c++ - 为什么必须在哪里放置 “template”和 “typename”关键字?

c++ - 在 vector <unique_ptr> 上使用 is_copy_constructible 误报

c++ - 如何强制 std::sort 使用 move 构造函数和 move 赋值?

c++ - 无法调用 getter 方法

c++ - 从 demangled 名称中获取 mangled 名称

c++ - 通用引用产生具有相同地址的不同实例

c++ - 如何使用 C++11 为 x86(-64) 安全地编写测试和测试并设置 (TATAS) 自旋锁?

c++ - 什么是 undefined reference /未解析的外部符号错误,我该如何解决?