移动语义——它是什么?

11

5
@Armen: "Google跟这有什么关系?我最近查看的情况是,对于编程问题,SO比Google更好的资源。我相信SO的目标甚至是成为更好的编程问题资源。那么为什么有人要在Google搜索上浪费时间呢?90%的情况下,它们会直接指向SO上的问题,反正是一样的东西。” - jalf
3
@jalf: "请给我提供一个好的来源来解释什么是移动语义。" 我认为首先应该尝试通过谷歌搜索来了解,查看相关链接,如果不满意,再在问题中指出已经阅读了哪些内容并说明不满意的原因。但这只是我的个人观点。 - Armen Tsirunyan
以下是原始简介链接:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2027.html - Howard Hinnant
4个回答

35

暂时不要考虑C++0x。移动语义是一种与语言无关的东西——C++0x仅提供了一种使用移动语义进行操作的标准方法。

定义

移动语义定义了某些操作的行为。大多数情况下,它们与复制语义相对比,因此首先需要定义它们。

采用复制语义进行赋值的行为如下:

// Copy semantics
assert(b == c);
a = b;
assert(a == b && b == c);

即,a 最终等于 b,而我们不改变 b

使用移动语义的赋值操作具有较弱的后置条件:

// Move semantics
assert(b == c);
move(a, b); // not C++0x
assert(a == c);

请注意,使用移动语义进行赋值后,不再保证 b 不发生变化。这是关键的区别。

用途

移动语义的一个好处是,在某些情况下可以进行优化。考虑以下常规值类型:

struct A { T* x; };

假设我们定义两个类型为 A 的对象相等,当且仅当它们的 x 成员指向相等的值。
bool operator==(const A& lhs, const A& rhs) { return *lhs.x == *rhs.x; }

假设我们定义一个对象A,它拥有其x成员的指针的独占所有权。

A::~A() { delete x; }
A::A(const A& rhs) : x(new T(rhs.x)) {}
A& A::operator=(const A& rhs) { if (this != &rhs) *x = *rhs.x; }

现在假设我们想要定义一个函数来交换两个A对象。

我们可以用常规的复制语义来完成它。

void swap(A& a, A& b)
{
    A t = a;
    a = b;
    b = t;
}

然而,这样做是不必要的低效率。我们在做什么?
- 我们将`a`复制到`t`中。 - 然后将`b`复制到`a`中。 - 接着将`t`复制到`b`中。 - 最后销毁`t`。
如果复制`T`对象的成本很高,则这种做法是浪费的。如果我让你交换计算机上的两个文件,您不会在销毁临时文件之前创建第三个文件,然后复制和粘贴文件内容吧?不需要复制数据,您会将一个文件移开,将第二个移动到第一个位置,最后将第一个文件移到第二个位置。
在我们的情况下,可以轻松移动类型为`A`的对象。
// Not C++0x
void move(A& lhs, A& rhs)
{
    lhs.x = rhs.x;
    rhs.x = nullptr;
}

我们只需将rhs的指针移动到lhs,然后放弃rhs对该指针的所有权(通过将其设置为null)。这应该可以解释为什么移动语义的较弱后置条件允许优化。
有了这个新的移动操作定义,我们可以定义一个优化的交换:
void swap(A& a, A& b)
{
    A t;
    move(t, a);
    move(a, b);
    move(b, t);
}

移动语义的另一个优点是它允许您移动无法被复制的对象。一个主要的例子就是 std::auto_ptr

C++0x

C++0x通过其右值引用特性实现了移动语义。具体来说,这种操作:

a = b;

b是右值引用(拼写为T&&)时,具有移动语义,否则具有复制语义。当b不是右值引用时,您可以使用std::move函数(与我之前定义的move不同)来强制使用移动语义:

a = std::move(b);

std::move 是一个简单的函数,它本质上将其参数转换为右值引用。请注意,表达式(例如函数调用)的结果自动成为右值引用,因此您可以在这些情况下利用移动语义而无需更改代码。

要定义移动优化,您需要定义移动构造函数和移动赋值运算符:

T::T(T&&);
T& operator=(T&&);

由于这些操作具有移动语义,您可以自由修改传递的参数(前提是保持对象处于可析构状态)。

结论

基本上就是这样。请注意,rvalue引用也用于允许C++0x中的完美转发(由于特别制作的类型系统相互作用),但这与移动语义并没有真正相关,因此我没有在此处讨论它。


4
基本上,右值引用允许您检测对象是否为临时对象,并且您无需保留其内部状态。这样可以实现更高效的代码,在C++03中需要一直复制的情况下,在C++0x中可以重复使用相同的资源。此外,右值引用还实现了完美转发。
请参阅此答案

移动语义和右值引用是不同(但相关)的概念。它们之间的关系是,对右值引用的赋值保证了移动语义的有效性。移动语义可以存在而没有右值引用,而右值引用也可以存在而没有与移动语义的操作。它们只是很好地协同工作(就像不可变性和并发性一样)。 - Peter Alexander

2

我阅读了一年的文本解释,但并没有完全理解有关r-value引用的所有内容,直到我观看了Scott Meyer的这个精彩演讲http://skillsmatter.com/podcast/home/move-semanticsperfect-forwarding-and-rvalue-references

他以幽默慢条斯理的方式解释,让人可以理解每个过程中发生的事情。

我知道,时长1小时30分钟,但实际上,这是我去年听到的最好的解释。

在阅读了文章(像其他答案一样)之后,观看这个视频将它们融合在我的脑海中,使我能够连贯地向同事解释如何使用std::unique_ptr(因为它仅允许移动语义,而不是复制),需要理解std::move(),它又需要理解移动语义。


2
很高兴看到这样的问题,我很愿意分享我的观点。我认为你在问关于C++语言本身指定的错误修复,而不仅仅是另一个C++语言特性。这个“错误”已经存在了数十年。即,拷贝构造函数
如果你知道物理学中有很多东西不能被复制,比如能量和质量,那么拷贝构造函数似乎就很奇怪。这只是个玩笑,但实际上,在编程世界中,像独占文件描述符这样的对象是不可复制的。因此,C++程序员和设计师发明了一些技巧来处理这个问题。其中有三个著名的技巧:NRVO、boost::noncopyablestd::auto_ptr
NRVO(命名返回值优化)是一种技术,它使函数可以通过值返回对象,而不调用拷贝构造函数。但是,NRVO的问题在于,虽然实际上没有调用拷贝构造函数,但仍需要一个public拷贝构造函数声明,这意味着boost::noncopyable对象与NRVO不兼容。 std::auto_ptr是另一种绕过拷贝构造函数的尝试。你可能已经看到过它的“拷贝构造函数”的实现方式。
template <typename _T>
auto_ptr(auto_ptr<_T>& source)
{
     _ptr = source._ptr; // where _ptr is the pointer to the contained object
     source._ptr = NULL;
}

这并不是一份复制,而是一份"移动"。你可以把这种行为看作是移动语义的原型。
但是,std::auto_ptr也有自己的问题:它与STL容器不兼容。所以,不幸的是,任何关于不可复制的东西都是痛苦的。
直到C++0x移动语义最终由编译器制造商发布和实现之前,这都是令人痛苦的。
简单来说,你可以将移动语义视为与std::auto_ptr的"复制"行为相同的东西,但由于语言特性的全面支持,它可以很好地与容器和算法一起使用。
顺便说一下,在C++0x中,std::auto_ptr已被弃用,推荐使用一个新的模板类型std::unique_ptr。
我的故事到此结束。如果你想了解更多关于奇怪的语法和右值系统等方面的知识,请参考其他文章。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接