我刚刚听完有关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的完整表达式的末尾)。这一点很重要,因为在
b
和c
初始化期间,我们可以对源字符串做我们想做的任何事情,而客户端却无法分辨!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/