移动语义 - 它的全部意义何在?

时间:2011-07-25 11:53:26

标签: c++ c++11 rvalue-reference move-semantics

  

可能重复:
  Can someone please explain move semantics to me?

有人能指出一个好的消息来源或在这里解释什么是移动语义?

4 个答案:

答案 0 :(得分:34)

暂时忘掉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&&)时有移动语义,否则它们具有复制语义。当std::move不是右值引用时,您可以使用move函数(与我前面定义的b不同)强制移动语义:

a = std::move(b);

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

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

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

由于这些操作具有移动语义,因此您可以自由修改传入的参数(假设您将对象置于可破坏状态)。

结论

基本上就是这一切。请注意,右值引用也用于允许在C ++ 0x中完美转发(由于rvalue引用和其他类型之间特别精心设计的类型系统交互),但这与移动语义无关,所以我没有讨论过它在这里。

答案 1 :(得分:4)

基本上,右值引用允许您检测对象何时是临时对象而您不必保留其内部状态。这允许更高效的代码,其中C ++ 03曾经必须一直复制,在C ++ 0x中,您可以继续重用相同的资源。此外,右值参考可实现完美转发。

查看this answer

答案 2 :(得分:2)

我阅读了大约一年的大量文字解释并且没有掌握关于r值参考的所有内容,直到我看到Scott Meyer的这篇精彩演讲http://skillsmatter.com/podcast/home/move-semanticsperfect-forwarding-and-rvalue-references

他以一种有趣且缓慢的方式解释,以理解过程中发生的每件事。

我知道,1小时30分,但实际上,这是我去年的最佳解释。

在阅读了这些文章之后(就像其他答案一样),观看这段视频确实以一致的方式将它融合在一起,几天后我能够向一些同事解释并解释如何使用std :: unique_ptr(因为它是相关的 - 它只允许移动语义,而不是复制),因为它需要理解std :: move(),这需要理解移动语义。

答案 3 :(得分:2)

很高兴看到这样的问题,我很高兴与大家分享我的观点。我想你是在询问有关C ++语言本身命名的错误修复,而不仅仅是另一种C ++语言特性。 “虫子”已经存在了数十年。也就是说,复制构造函数

如果你在物理学中知道有许多东西无法像能量和质量一样被复制,那么复制构造函数似乎很奇怪。这只是个玩笑,但事实上在编程领域,像独占文件描述符这样的对象是不可复制的。所以C ++程序员和设计师发明了一些技巧来解决这个问题。有3个着名的: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容器不兼容。所以,不幸的是,关于noncopyable的任何事都很痛苦。

这很痛苦,直到C ++ 0x移动语义最终由编译器制造商发布和实现。

以简单的方式,您可以将移动语义看作与std::auto_ptr的“复制”行为相同,但语言功能完全支持,因此它适用于容器和算法。

顺便说一下,在C ++ 0x中,不推荐使用std::auto_ptr,建议使用新的模板类型std::unique_ptr

我的故事现在结束了。如果您想了解更多关于它的信息,请参阅其他帖子,如奇怪的语法和右值系统。