提问



我刚刚听完了Scott Meyers关于C ++ 0x的软件工程电台播客采访。大多数新功能对我来说都很有意义,我现在对C ++ 0x感到兴奋,除了一个。我仍然没有移动语义 ......它们究竟是什么?[256] [257]

最佳参考


我发现用示例代码理解移动语义最容易。让我们从一个非常简单的字符串类开始,该类只保存指向堆分配的内存块的指针:


#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = strlen(p) + 1;
        data = new char[size];
        memcpy(data, p, size);
    }


由于我们自己选择管理内存,因此我们需要遵循三个规则。我将推迟编写赋值运算符,现在只实现析构函数和复制构造函数:[258]


    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = strlen(that.data) + 1;
        data = new char[size];
        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,因为底层字符串对象没有名称,因此客户端无法在以后再次检查它们。
rvalues表示在下一个分号处被销毁的临时对象(更准确地说:在词法上包含rvalue的全表达式的末尾)。这很重要,因为在bc的初始化过程中,我们可以用源字符串做任何我们想做的事情,客户端无法区分!


C ++ 0x引入了一种名为rvalue reference的新机制,除其他外,
允许我们通过函数重载检测rvalue参数。我们所要做的就是编写一个带有右值引用参数的构造函数。在构造函数内部,只要我们将它留在某些有效状态,我们就可以使用源我们想要的任何内容:


    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }


我们在这做了什么?我们刚刚复制了指针,然后将原始指针设置为null,而不是深度复制堆数据。实际上,我们窃取了原来属于源字符串的数据。同样,关键的见解是,在任何情况下客户都无法检测到源已被修改。由于我们不是真的在这里复制,我们称这个构造函数为移动构造函数。它的工作是将资源从一个对象移动到另一个对象而不是复制它们。


恭喜,您现在了解移动语义的基础知识!让我们继续实现赋值运算符。如果你不熟悉拷贝和交换习语,那就学习它并回来吧,因为它是一个与异常安全相关的一个很棒的C ++习语。


    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};


嗯,那是吗?左右参考在哪里?你可能会问。 我们不需要它!是我的答案:)


请注意,我们通过值传递参数that ,因此必须像任何其他字符串对象一样初始化that。究竟如何that初始化?在C ++ 98的旧时代,答案将是通过复制构造函数。在C ++ 0x中,编译器根据赋值运算符的参数是左值还是右值来选择复制构造函数和移动构造函数。[260]


因此,如果你说a = b,复制构造函数将初始化that(因为表达式b是一个左值),赋值运算符将内容交换为新鲜创作,深层复制。这就是复制和交换习惯用语的定义 - 制作副本,将内容与副本交换,然后通过离开作用域来删除副本。这里没什么新鲜的。


但是如果你说a = x + y,移动构造函数将初始化that(因为表达式x + y是一个rvalue),所以不涉及深度复制,只有一个有效的举措。
that仍然是争论的一个独立对象,但它的构造是微不足道的,
由于堆数据不必复制,只需移动。没有必要复制它,因为x + y是一个右值,再次,可以从rvalues表示的字符串对象移动。


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


我希望这个例子得到了重点。 rvalue引用和移动语义还有很多,我故意省略它以保持简单。如果您想了解更多详情,请参阅我的补充答案。

其它参考1


我的第一个答案是对移动语义的极其简化的介绍,并且许多细节都是为了保持简单而故意留下的。
然而,移动语义还有很多,我认为现在是时候填补空白的第二个答案了。
第一个答案已经很老了,用一个完全不同的文本替换它是不对的。我认为它仍然可以作为第一个介绍。但如果你想深入挖掘,请继续阅读:)


Stephan T. Lavavej花时间提供有价值的反馈。非常感谢,斯蒂芬!


简介



移动语义允许对象在某些条件下拥有其他对象的外部资源。这在两个方面很重要:



  1. 将昂贵的副本变成便宜的动作。请参阅我的第一个答案。请注意,如果对象不管理至少一个外部资源(直接或间接通过其成员对象),则移动语义将不会提供优于复制语义的任何优势。在这种情况下,复制对象和移动对象意味着完全相同的事情:


    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    

  2. 实施安全的仅限移动类型;也就是说,复制没有意义,但移动的类型。示例包括锁,文件句柄和具有唯一所有权语义的智能指针。注意:这个答案讨论了std::auto_ptr,一个弃用的C ++ 98标准库模板,在C ++ 11中被std::unique_ptr取代。中级C ++程序员可能至少对std::auto_ptr有点熟悉,并且由于它显示的移动语义,它似乎是在C ++ 11中讨论移动语义的一个很好的起点。因人而异。



什么是行动?



C ++ 98标准库提供了一个具有独特所有权语义的智能指针,称为std::auto_ptr<T>。如果您不熟悉auto_ptr,其目的是保证动态分配的对象始终被释放,即使面对异常:


{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically


关于auto_ptr的不寻常之处在于它的复制行为:


auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+


注意ba的初始化如何不复制三角形,而是将三角形的所有权从a传递到b。我们还说a 移入 b或三角形已移动从a 移至 b。这可能听起来令人困惑,因为三角形本身总是停留在内存中的相同位置。



  移动对象意味着将其管理的某些资源的所有权转移给另一个对象。



auto_ptr的复制构造函数可能看起来像这样(有点简化):


auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}


危险无害的举动



关于auto_ptr的危险之处在于语法上看起来像副本实际上是一个动作。试图在move [[from [[auto_ptr上调用成员函数将调用未定义的行为,因此在移动之后你必须非常小心不要使用auto_ptr:


auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior


auto_ptr 总是危险。工厂功能是auto_ptr的完美用例:


auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe


请注意两个示例如何遵循相同的语法模式:


auto_ptr<Shape> variable(expression);
double area = expression->area();


然而,其中一个调用未定义的行为,而另一个则不调用。那么表达式amake_triangle()之间有什么区别? Aren他们两个都是同一类型吗?确实他们是,但他们有不同的价值类别。


值类别



显然,表达式a表示auto_ptr变量,表达式make_triangle()表示调用返回auto_ptr的函数的表达式之间必定存在一些深刻的差别。 value,从而在每次调用时创建一个新的临时auto_ptr对象。 a是左值的示例,而make_triangle()是右值的示例。


从诸如a之类的左值移动是危险的,因为我们稍后可以尝试通过a调用成员函数,从而调用未定义的行为。另一方面,从诸如make_triangle()的rvalues移动是完全安全的,因为在复制构造函数完成其工作之后,我们不能再次使用临时工具。没有表示所述临时表达的表达;如果我们再次写make_triangle(),我们会得到一个不同的临时。事实上,移动临时已经在下一行中消失了:


auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here


请注意,字母lr在赋值的左侧和右侧具有历史原点。这在C ++中已不再适用,因为有左值不能出现在赋值的左侧(如数组或没有赋值运算符的用户定义类型),并且有rvalues可以(类的所有rvalues)有一个赋值运算符)。



  类类型的右值是一个表达式,其评估创建一个临时对象。
  在正常情况下,同一范围内的其他表达式不表示相同的临时对象。



Rvalue参考



我们现在明白,从左撇子移动是有潜在危险的,但从右撇子移动是无害的。如果C ++有语言支持来区分左值参数和右值参数,我们可以完全禁止从左值移动,或者至少从呼叫站点的lvalues explicit 移动,这样我们就不会意外移动了。


C ++ 11对这个问题的回答是 rvalue references 。右值引用是一种新的引用,它只绑定到rvalues,语法是X&&。好的旧引用X&现在称为左值引用。(注意X&& 不是对引用的引用;在C ++中没有这样的东西。)


如果我们将const抛到混合中,我们已经有了四种不同的引用。什么类型的X表达式可以绑定?


            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes


在实践中,你可以忘记const X&&。被限制从rvalues读取并不是很有用。



  右值引用X&&是一种仅与rvalues绑定的新引用。



隐式转换



Rvalue引用经历了几个版本。从版本2.1开始,只要存在从YX的隐式转换,右值引用X&&也会绑定到不同类型Y的所有值类别。在这种情况下,创建一个临时类型X,并将右值引用绑定到该临时值:


void some_function(std::string&& r);

some_function("hello world");


在上面的例子中,"hello world"是类型const char[12]的左值。由于存在从const char[12]const char*std::string的隐式转换,因此创建了std::string类型的临时变量,并且r绑定到该临时变量。这是rvalues(表达式)和temporaries(对象)之间的区别有点模糊的情况之一。


移动构造函数



X&&参数的函数的一个有用示例是移动构造函数 X::X(X&& source)。其目的是将托管资源的所有权从源转移到当前对象。


在C ++ 11中,std::auto_ptr<T>已被std::unique_ptr<T>取代,后者利用了右值引用。我将开发和讨论unique_ptr的简化版本。首先,我们封装一个原始指针并重载运算符->*,所以我们的类感觉就像一个指针:


template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }


构造函数获取对象的所有权,析构函数删除它:


    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }


现在是有趣的部分,移动构造函数:


    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }


这个移动构造函数与auto_ptr复制构造函数完全相同,但它只能提供rvalues:


unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay


第二行无法编译,因为a是左值,但参数unique_ptr&& source只能绑定到右值。这正是我们想要的;危险的举动永远不应该隐含。第三行编译得很好,因为make_triangle()是一个右值。移动构造函数将所有权从临时转移到c。再次,这正是我们想要的。



  移动构造函数将受管资源的所有权转移到当前对象中。



移动赋值运算符



最后一个缺失的部分是移动赋值运算符。它的工作是释放旧资源并从其参数中获取新资源:


    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};


注意移动赋值运算符的这种实现如何复制析构函数和移动构造函数的逻辑。你熟悉复制和交换习语吗?它也可以应用于移动语义作为移动和交换习语:


    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};


既然sourceunique_ptr类型的变量,它将由移动构造函数初始化;也就是说,参数将被移动到参数中。该参数仍然需要是一个rvalue,因为移动构造函数本身有一个rvalue引用参数。当控制流到达operator=的右括号时,source超出范围,自动释放旧资源。



  移动分配运算符将受管资源的所有权转移到当前对象中,从而释放旧资源。
  移动和交换习惯简化了实现。



从左值移动



有时,我们想要从左手边移动。也就是说,有时我们希望编译器将左值视为rvalue,因此它可以调用移动构造函数,即使它可能是不安全的。
为此,C ++ 11在标题<utility>中提供了一个名为std::move的标准库函数模板。
这个名字有点不幸,因为std::move只是将一个左值投射到右值;它不自行移动任何东西。它只是启用移动。也许它应该被命名为std::cast_to_rvaluestd::enable_move,但我们现在仍然坚持这个名字。


以下是您如何明确地从左值移动:


unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay


请注意,在第三行之后,a不再拥有三角形。那没关系,因为明确写std::move(a),我们明确表达了我们的意图:亲爱的构造函数,为了初始化a而用a做任何你想做的事情。c]]。我不再关心a了。随意a



  std::move(some_lvalue)将左值投射到右值,从而实现后续移动。



Xvalues



请注意,即使std::move(a)是右值,其评估不创建临时对象。这个难题迫使委员会引入第三个价值类别。可以绑定到右值引用的东西,即使它不是传统意义上的右值,也称为 xvalue (eXpiring值)。传统的右值被重命名为 prvalues (Pure rvalues)。


prvalues和xvalues都是rvalues。 Xvalues和左值都是 glvalues (广义左值)。使用图表更容易掌握关系:


        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues


请注意,只有xvalues才是新的;剩下的就是重命名和分组。



  C ++ 98 rvalues在C ++ 11中称为prvalues。用prvalue将前面几段中出现的所有rvalue替换为prvalue。



退出职能



到目前为止,我们已经看到了局部变量和函数参数的变化。但也可能在相反的方向上移动。如果函数按值返回,则调用站点上的某个对象(可能是局部变量或临时变量,但可以是任何类型的对象)使用return语句之后的表达式作为移动构造函数的参数进行初始化:


unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());


也许令人惊讶的是,自动对象(未声明为static的局部变量)也可以隐式移出函数:


unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}


为什么移动构造函数接受左值result作为参数? result的范围即将结束,它将在堆栈展开期间被销毁。之后没有人可能会抱怨result以某种方式改变了;当控制流回到呼叫者时,result不再存在!出于这个原因,C ++ 11有一个特殊的规则,允许从函数返回自动对象,而不必写std::move。实际上,你应该永远使用std::move将自动对象移出函数,因为这会禁止命名返回值优化(NRVO)。



  切勿使用std::move将自动对象移出功能。



请注意,在两个工厂函数中,返回类型是值,而不是右值引用。 Rvalue引用仍然是引用,并且一如既往,您永远不应该返回对自动对象的引用;如果你欺骗编译器接受你的代码,调用者最终会得到一个悬空引用,如下所示:


unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}



  永远不要通过右值参考返回自动对象。移动仅由移动构造函数执行,而不是由std::move执行,而不是仅仅通过将右值绑定到右值引用。



进入成员



迟早,你要编写这样的代码:


class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};


基本上,编译器会抱怨parameter是左值。如果你看它的类型,你会看到一个右值引用,但是右值引用只是意味着一个绑定到右值的引用; 不意味着引用本身是一个右值!实际上,parameter只是一个带名字的普通变量。你可以在构造函数体内随意使用parameter,它总是表示同一个对象。隐含地从它移动将是危险的,因此语言禁止它。



  命名的右值引用是左值,就像任何其他变量一样。



解决方案是手动启用移动:


class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};


您可以争辩member初始化后不再使用parameter。为什么没有像返回值一样静默插入std::move的特殊规则?可能是因为编译器实现者的负担太大了。例如,如果构造函数体在另一个翻译单元中怎么办?相反,返回值规则只需检查符号表以确定return关键字之后的标识符是否表示自动对象。


您还可以按值传递parameter。对于像unique_ptr这样的仅移动类型,似乎还没有确定的习惯用语。就个人而言,我更喜欢按值传递,因为它会减少界面中的混乱。


特别会员职能



C ++ 98根据需要隐式声明了三个特殊的成员函数,即在某处需要它们时:复制构造函数,复制赋值运算符和析构函数。


X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor


Rvalue引用经历了几个版本。从3.0版开始,C ++ 11根据需要声明了两个额外的特殊成员函数:移动构造函数和移动赋值运算符。请注意,VC10和VC11都不符合3.0版,因此您必须自己实现它们。


X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator


如果没有特殊成员func,则仅隐式声明这两个新的特殊成员函数tions是手动声明的。此外,如果声明自己的移动构造函数或移动赋值运算符,则不会隐式声明复制构造函数和复制赋值运算符。


这些规则在实践中意味着什么?



  如果您编写的类没有非托管资源,则无需自己声明任何五个特殊成员函数,您将获得正确的复制语义并免费移动语义。否则,您必须自己实现特殊成员函数。当然,如果您的类没有受益于移动语义,则无需实现特殊移动操作。



请注意,复制赋值运算符和移动赋值运算符可以融合到单个统一赋值运算符中,并按值获取其参数:


X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}


这样,要实现的特殊成员函数的数量从五个减少到四个。这里的异常安全和效率之间存在权衡,但我不是这个问题的专家。


转发引用(以前称为通用引用)



考虑以下功能模板:[262]


template<typename T>
void foo(T&&);


您可能希望T&&仅绑定到右值,因为乍一看,它看起来像一个右值引用。事实证明,T&&也与左值结合:


foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&


如果参数是X类型的右值,T推导为X,因此T&&表示X&&。这是任何人都期望的。
但如果论证是X类型的左值,由于一个特殊的规则,T被推断为X&​​]],因此T&&意味着类似X& &&]]。但由于C ++仍然没有引用引用的概念,因此类型X& && 折叠到X&。这可能听起来有点困惑和无用,但参考折叠对于完美转发至关重要(这里不再讨论)。



  T&安培;&安培;不是右值引用,而是转发引用。它也绑定到左值,在这种情况下TT&&都是左值引用。



如果要将函数模板约束为rvalues,可以将SFINAE与类型特征结合起来:[263]


#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);


移动的实施



现在您已了解参考折叠,以下是std::move的实现方式:


template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}


如您所见,由于转发引用T&&move接受任何类型的参数,并返回一个右值引用。 std::remove_reference<T>::type元函数调用是必要的,因为否则,对于X类型的左值,返回类型将是X& &&,它将崩溃为X&。由于t总是左值(请记住命名的右值引用是左值),但我们想将t绑定到右值引用,我们必须显式地将t转换为正确值返回类型。
返回rvalue引用的函数调用本身就是一个xvalue。现在你知道xvalues的来源了;)



  返回rvalue引用的函数(如std::move)的调用是xvalue。



请注意,在此示例中,通过右值引用返回很好,因为t不表示自动对象,而是表示调用者传入的对象。

其它参考2


移动语义基于 右值引用

rvalue是一个临时对象,它将在表达式的末尾被销毁。在当前的C ++中,rvalues仅绑定到const引用。 C ++ 1x将允许非const rvalue引用,拼写为T&&,它们是对右值对象的引用。

由于rvalue将在表达式结尾处消亡,因此您可以窃取其数据。您可以将其数据移动到其他对象中,而不是将复制到另一个对象中。


class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor


在上面的代码中,对于旧编译器,使用X的复制构造函数将f()的结果 复制 导入x。你的编译器支持移动语义,X有一个移动构造函数,然后调用它。由于它的rhs参数是 rvalue ,我们知道它不需要任何更长,我们可以窃取它的价值。

因此,从f()返回到x的未命名临时值 移动 (同时x的数据,初始化为空X,被移入临时,这将在分配后被销毁)。

其它参考3


假设您有一个返回实体对象的函数:


Matrix multiply(const Matrix &a, const Matrix &b);


当你编写这样的代码时:


Matrix r = multiply(a, b);


然后普通的C ++编译器将为multiply()的结果创建一个临时对象,调用复制构造函数初始化r,然后销毁临时返回值。在C ++中移动语义0x允许通过复制其内容来调用移动构造函数来初始化r,然后丢弃临时值而不必破坏它。


如果(就像上面的Matrix例子那样),这一点尤其重要,被复制的对象会在堆上分配额外的内存来存储其内部表示。复制构造函数必须要么制作内部表示的完整副本,要么在内部使用引用计数和写时复制语义。移动构造函数会单独留下堆内存,只需将指针复制到Matrix对象中。

其它参考4


如果你真的对移动语义的一个好的,深入的解释感兴趣,我强烈建议阅读关于它们的原始论文,为C ++语言添加移动语义支持的提案。[264]


它非常容易阅读,并且很容易获得它们提供的好处。在WG21网站上还有其他关于移动语义的最新和最新的论文,但这个可能是最直接的它从顶层视图中处理事物,并没有深入研究坚韧不拔的语言细节。[265]

其它参考5


当没有人需要源值时,移动语义是关于传输资源而不是复制它们


在C ++ 03中,对象经常被复制,只有在任何代码再次使用该值之前才被销毁或分配。例如,当您从函数返回值时 - 除非RVO启动 - 您返回的值将被复制到调用者的堆栈帧,然后它将超出范围并被销毁。这只是众多例子中的一个:当源对象是临时的时,请参阅值传递,像sort这样的算法只重新排列项目,vector在超出capacity()时重新分配等


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

其它参考6


简单(实用)术语:


复制对象意味着复制其静态成员并为其动态对象调用new运算符。对?


class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};


然而,对于移动一个对象(我重复一遍,从实际的角度来看)意味着只复制动态对象的指针,而不是创建新的指针。


但是,那不危险吗?当然,您可以两次破坏动态对象(分段错误)。因此,为避免这种情况,您应该使源指针无效以避免两次破坏它们:


class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};


好的,但如果我移动一个对象,源对象就变得无用了,不是吗?当然,但在某些非常有用的情况下。最明显的是当我用匿名对象调用函数时(temporal,rvalue对象,......,你可以用不同的名字来调用它):


void heavyFunction(HeavyType());


在这种情况下,将创建一个匿名对象,然后将其复制到函数参数,然后删除。所以,这里最好移动对象,因为你不需要匿名对象,你可以节省时间和内存。


这导致了右值参考的概念。它们仅存在于C ++ 11中,用于检测接收到的对象是否是匿名的。我想你已经知道左值是一个可赋值实体(=运算符的左边部分),所以你需要一个对象的命名引用才能充当左值。右值正好相反,没有命名引用的对象。因此,匿名对象和右值是同义词。所以:


class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};


在这种情况下,当A类型的对象应该被复制时,编译器会根据传入的对象是否被命名来创建左值引用或右值引用。如果没有,您的move-constructor被调用,您知道该对象是暂时的,您可以移动其动态对象而不是复制它们,从而节省空间和内存。


重要的是要记住静态对象总是被复制。没有办法移动静态对象(堆栈中的对象而不是堆上的对象)。因此,当对象没有动态成员(直接或间接)时,区别移动/复制是无关紧要的。


如果您的对象很复杂并且析构函数具有其他辅助效果,例如调用库的函数,调用其他全局函数或其他任何函数,也许最好用标志来表示运动:


class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};


因此,您的代码更短(您不需要为每个动态成员执行nullptr分配)并且更通用。


其他典型问题:A&&const A&&之间有什么区别?当然,在第一种情况下,你可以修改对象而在第二种情况下,但是,实际意义?在第二种情况下,你不能修改它,所以你没有办法使对象无效(除了带有可变标志或类似的东西),并且复制构造函数没有实际的区别。


什么是完美转发?重要的是要知道右值引用是对调用者范围中的命名对象的引用。但在实际范围中,右值引用是对象的名称,因此,它充当命名如果将rvalue引用传递给另一个函数,则传递一个命名对象,因此,该对象不会像临时对象那样被接收。


void some_function(A&& a)
{
   other_function(a);
}


对象a将被复制到other_function的实际参数。如果希望对象a继续被视为临时对象,则应使用std::move函数:


other_function(std::move(a));


使用此行,std::movea转换为右值,other_function将接收对象作为未命名的对象。当然,如果other_function没有特定的重载来处理未命名的对象,这种区别并不重要。


那是完美的转发吗?不,但我们非常接近。完美转发仅对模板有用,目的是:如果我需要将一个对象传递给另一个函数,我需要如果我收到一个命名对象,那么该对象将作为命名对象传递,如果没有,我想像未命名的对象一样传递它:


template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}


这是使用完美转发的原型函数的签名,通过std::forward在C ++ 11中实现。此函数利用了一些模板实例化规则:


 `A& && == A&`
 `A&& && == A&&`


因此,如果TA的左值参考( T =A&),a也是( A& & & => A&)。如果TA的右值参考,a也是(A&&& => A&&)。在这两种情况下,a是实际范围内的命名对象,但T包含来自调用者范围的引用类型的信息。此信息(T]])作为模板参数传递给forward,并根据T的类型移动a。

其它参考7


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

其它参考8


你知道复制语义意味着什么吗?它意味着你有可复制的类型,对于你定义的用户定义类型,要么明确地写一个拷贝构造函数&赋值运算符或编译器隐式生成它们。这将做一个副本。


移动语义基本上是一个用户定义的类型,其构造函数采用r值引用(使用&&(是两个&符)的新类型引用)非const,这称为移动构造函数,同样适用于赋值运算符。那么移动构造函数是做什么的,而不是从它的源参数复制内存,它将内存从源移动到目标。


你什么时候想做那个? well std :: vector就是一个例子,假设您创建了一个临时的std :: vector,并从函数中返回它:


std::vector<foo> get_foos();


当函数返回时,你将从复制构造函数中获得开销,如果(并且它将在C ++ 0x中)std :: vector有一个移动构造函数而不是复制它可以只设置它的指针和移动动态分配内存到新实例。它有点像std :: auto_ptr的所有权转移语义。

其它参考9


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


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


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


上面的函数使用按值调用,这意味着当调用此函数时,对象必须构造才能被函数使用。

因为函数按值返回,所以为返回值构造了另一个新对象:


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


构建了两个新对象,其中一个是临时对象,仅用于函数的持续时间。


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





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


它必须首先初始化对象,然后将旧对象中的所有相关数据复制到新对象
根据类,也许它是一个包含大量数据的容器,然后可能代表 time 和内存使用


// 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);
}


移动数据涉及将数据与新对象重新关联。并且根本没有复制。


这是通过rvalue引用来完成的
rvalue引用与lvalue引用非常相似,但有一个重要区别:

可以移动右值引用,而左值则不能。


来自cppreference.com: [266]



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


其它参考10


我写这篇文章是为了确保我理解得当。


创建了移动语义以避免不必要的大型对象复制。 Bjarne Stroustrup在他的书The C ++ Programming Language中使用了两个默认情况下进行不必要复制的例子:一个是交换两个大对象,另外两个是从一个方法返回一个大对象。


交换两个大对象通常涉及将第一个对象复制到临时对象,将第二个对象复制到第一个对象,以及将临时对象复制到第二个对象。对于内置类型,这非常快,但对于大型对象,这三个副本可能会花费大量时间。 移动分配允许程序员覆盖默认的复制行为,而是交换对象的引用,这意味着根本没有复制,交换操作要快得多。可以通过调用std :: move()方法来调用移动赋值。


默认情况下,从方法返回对象涉及在调用者可访问的位置创建本地对象及其关联数据的副本(因为调用方无法访问本地对象,并且在方法完成时消失)。当返回内置类型时,此操作非常快,但如果返回大对象,则可能需要很长时间。移动构造函数允许程序员覆盖此默认行为,而是通过将返回的对象指向调用程序以堆叠与本地对象关联的数据来重用与本地对象关联的堆数据。因此不需要复制。


在不允许创建本地对象(即堆栈中的对象)的语言中,这些类型的问题不会发生,因为所有对象都在堆上分配,并且始终通过引用访问。