我刚听完Scott Meyers关于C++0x的软件工程广播播客采访。大多数新特性对我来说都是有意义的,我现在对C++0x非常兴奋,只有一个例外。我仍然不懂移动语义。。。到底是什么?


当前回答

这是Bjarne Stroustrup的《C++编程语言》一书中的答案。如果你不想看视频,可以看下面的文字:

考虑一下这个片段。从运算符+返回涉及将结果从局部变量res复制到调用者可以访问的地方。

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

我们真的不想要副本;我们只是想从函数中得到结果。所以我们需要移动Vector而不是复制它。我们可以如下定义移动构造函数:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&&表示“右值引用”,是我们可以绑定右值的引用。“rvalue”'旨在补充“lvalue”,它大致意思是“可以出现在赋值左侧的值”。因此,rvalue大致意思是:“不能赋值的值”,例如函数调用返回的整数,以及Vectors运算符+()中的res局部变量。

现在,语句返回res;不会复制!

其他回答

你知道复制语义是什么意思吗?这意味着您有可复制的类型,对于用户定义的类型,您可以明确地编写复制构造函数和赋值运算符,或者编译器隐式地生成它们。这将复制一份。

移动语义基本上是一种用户定义的类型,带有构造函数,它接受一个非常量的r值引用(使用&&(是两个&符号)的新类型引用),这称为移动构造函数,赋值运算符也是如此。那么,移动构造函数做什么呢?它不是从源参数复制内存,而是将内存从源“移动”到目标。

你什么时候想这样做?那么std::vector就是一个例子,假设您创建了一个临时std::vector,然后从一个函数返回它,比如:

std::vector<foo> get_foos();

当函数返回时,复制构造函数将产生开销,如果(在C++0x中也是如此)std::vector有一个移动构造函数,而不是复制它。它只需要设置指针并将动态分配的内存“移动”到新实例。这有点像std::auto_ptr的所有权转移语义。

这就像复制语义,但不必复制所有数据,而是从“移动”的对象中窃取数据。

移动语义是指在没有人需要源值时转移资源,而不是复制资源。

在C++03中,对象经常被复制,只有在任何代码再次使用该值之前才会被销毁或赋值。例如,当您从函数中按值返回时,除非RVO踢入您返回的值,否则该值将被复制到调用方的堆栈帧,然后超出范围并被销毁。这只是众多示例中的一个:当源对象是临时对象时,请参阅passbyvalue;当超过其capacity()时,可以使用排序等算法重新排列项目。

当这种复制/销毁对很昂贵时,通常是因为对象拥有一些重量级资源。例如,vector<string>可能拥有一个动态分配的内存块,其中包含一个字符串对象数组,每个字符串对象都有自己的动态内存。复制这样的对象代价很高:必须为源中每个动态分配的块分配新的内存,并复制所有值。然后需要释放刚才复制的所有内存。然而,移动一个大向量<string>意味着只需将几个指针(指动态内存块)复制到目标,并在源中将它们清零。

如果你真的对移动语义的一个好的、深入的解释感兴趣,我强烈建议你阅读关于它们的原始论文《向C++语言添加移动语义支持的建议》

这本书非常容易阅读和阅读,为他们提供的好处提供了极好的理由。WG21网站上还有其他关于移动语义的最新论文,但这篇文章可能是最直接的,因为它从顶层的角度处理问题,并没有深入到语言的细节。

为了说明移动语义的需要,让我们考虑这个没有移动语义的示例:

下面是一个函数,它接受T类型的对象并返回相同类型的对象:

T f(T o) { return o; }
  //^^^ new object constructed

上述函数使用值调用,这意味着当调用此函数时,必须构造一个对象以供函数使用。由于函数也按值返回,因此为返回值构造另一个新对象:

T b = f(a);
  //^ new object constructed

已经构造了两个新对象,其中一个是仅在函数期间使用的临时对象。

当从返回值创建新对象时,调用复制构造函数将临时对象的内容复制到新对象b。函数完成后,函数中使用的临时对象超出范围并被销毁。


现在,让我们考虑一下复制构造函数的作用。

它必须首先初始化对象,然后将所有相关数据从旧对象复制到新对象。根据类的不同,它可能是一个包含大量数据的容器,那么这可能会占用大量的时间和内存

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

使用移动语义,现在可以通过简单地移动数据而不是复制来减少大部分工作的不愉快。

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

移动数据涉及将数据与新对象重新关联。而且根本不会复制。

这是通过右值引用完成的。右值引用的工作方式与左值引用非常相似,但有一个重要区别:右值引用可以移动,左值不能移动。

来自cpreference.com:

为了使强异常保证成为可能,用户定义的移动构造函数不应抛出异常。事实上,当需要重新定位容器元素时,标准容器通常依赖std::move_if_noexcept在移动和复制之间进行选择。如果同时提供了复制构造函数和移动构造函数,则重载解析会在参数为右值(prvalue,如无名临时值或xvalue,如std::move的结果)时选择移动构造函数,而在参数为左值(命名对象或返回左值引用的函数/运算符)时选择复制构造函数。如果只提供了复制构造函数,则所有参数类别都会选择它(只要它引用const,因为rvalues可以绑定到const引用),这使得当移动不可用时,复制成为移动的备用方法。在许多情况下,即使移动构造函数会产生可观察到的副作用,也会对其进行优化,请参见复制省略。当构造函数将右值引用作为参数时,它被称为“移动构造函数”。它没有义务移动任何东西,类不需要有要移动的资源,并且“移动构造函数”可能无法移动资源,因为在允许(但可能不合理)的情况下,参数是常量值引用(constT&&)。