关于C++:什么是移动语义学?

What are move semantics?

我刚刚听了Scott Meyers关于C++0X的软件工程无线电播客访谈。大多数新的特性对我来说都是有意义的,我现在对C++0X感到兴奋,除了一个。我还是不懂移动语义学…它们到底是什么?


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cstring>
#include

class string
{
    char* data;

public:

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

因为我们选择了自己管理记忆,所以我们需要遵循三原则。我将推迟编写赋值运算符,现在只实现析构函数和复制构造函数:好的。

1
2
3
4
5
6
7
8
9
10
11
    ~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绑定到字符串类型的所有表达式,这些表达式允许您在以下示例中进行复制:好的。

1
2
3
string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

现在有了对移动语义的关键洞察。请注意,只有在我们复制x的第一行中,这种深度复制才是真正必要的,因为我们可能希望稍后检查x,如果x发生了某种变化,我们会非常惊讶。你注意到我刚才说了三次(如果你包括这句话的话,是四次)并且每次都意味着完全相同的目标吗?我们把x这样的表达式称为"lvalues"。好的。

第2行和第3行中的参数不是lvalue,而是rvalue,因为底层字符串对象没有名称,所以客户机无法在以后的时间点再次检查它们。rvalues表示临时对象,这些对象在下一个分号处被破坏(更精确地说:在词法上包含rvalue的完整表达式的末尾)。这一点很重要,因为在初始化bc的过程中,我们可以使用源字符串做任何我们想做的事情,而客户机无法分辨出不同之处!好的。

C++0x引入了一种新的机制,称为"RealValk",除此之外,允许我们通过函数重载检测右值参数。我们所要做的就是用右值引用参数编写一个构造函数。在这个构造函数中,只要保持源代码处于某个有效状态,我们就可以对它做任何我们想做的事情:好的。

1
2
3
4
5
    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

我们在这里做了什么?我们没有深度复制堆数据,只是复制了指针,然后将原始指针设置为空(以防止源对象的析构函数中的"delete[]"释放我们的"刚刚被盗的数据")。实际上,我们已经"窃取"了最初属于源字符串的数据。同样,关键的洞察是,在任何情况下,客户机都无法检测到源代码已被修改。因为我们没有在这里进行复制,所以我们称这个构造函数为"移动构造函数"。它的工作是将资源从一个对象移动到另一个对象,而不是复制它们。好的。

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

1
2
3
4
5
6
    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

嗯,就是这样吗?"右值引用在哪里?"你可能会问。"我们这里不需要它!"我的回答是:好的。

注意,我们通过值传递参数that,所以that必须像其他字符串对象一样初始化。究竟如何初始化that?在C++ 98的旧时代,答案可能是"复制构造函数"。在C++0x中,编译器基于复制运算符的参数是否为LValk或RUnt来在复制构造函数和移动构造函数之间进行选择。好的。

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

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

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

我希望这个例子能说明要点。还有很多关于值引用和移动语义的内容,我故意忽略了这些内容以保持简单。如果你想了解更多细节,请参阅我的补充答案。好的。好啊。


我的第一个答案是对移动语义的一个非常简单的介绍,为了保持简单,故意遗漏了许多细节。然而,还有很多要移动的语义,我认为是时候第二个答案来填补空白了。第一个答案已经很老了,用完全不同的文本来代替它是不合适的。我认为这仍然是一个很好的第一次介绍。但如果你想深入了解,请继续阅读:)好的。

Stephan T.Lavavej花时间提供了宝贵的反馈。非常感谢,斯蒂芬!好的。介绍

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

  • 把昂贵的拷贝变成廉价的拷贝。请看我的第一个答案。注意,如果一个对象不管理至少一个外部资源(直接或通过其成员对象间接管理),那么移动语义将不会比复制语义提供任何优势。在这种情况下,复制对象并移动对象意味着完全相同的事情:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

        // ...
    };
  • 实现安全的"只移动"类型;也就是说,复制没有意义,但移动有意义的类型。示例包括具有唯一所有权语义的锁、文件句柄和智能指针。注意:这个答案讨论了EDCOX1×0,一个不受欢迎的C++ 98标准库模板,它被EcOX1 1中的C++ 11所代替。中间的C++程序员可能对EDOCX1的0度有点熟悉,并且因为它显示的"移动语义",它似乎是讨论C++ 11中的移动语义的一个很好的起点。YMMV。好的。

  • 什么是行动?

    C++ 98标准库提供了一个具有独特所有权语义的智能指针,称为EDCOX1(3)。如果您对auto_ptr不熟悉,它的目的是确保始终释放动态分配的对象,即使在遇到异常的情况下:好的。

    1
    2
    3
    4
    5
    6
    {
        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的不寻常之处在于它的"复制"行为:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    auto_ptr<Shape> a(new Triangle);

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

    auto_ptr<Shape> b(a);

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

    请注意,使用a初始化b不会复制三角形,而是将三角形的所有权从a转移到b上。我们还说"a移到b中"或"三角形从a移到b中"。这听起来可能让人困惑,因为三角形本身在内存中总是保持在同一个位置。好的。

    To move an object means to transfer ownership of some resource it manages to another object.

    Ok.

    auto_ptr的复制构造函数可能如下所示(有些简化):好的。

    1
    2
    3
    4
    5
    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的危险之处在于,从语法上看,拷贝实际上是一种移动。尝试对从auto_ptr移动的调用成员函数将调用未定义的行为,因此在从以下位置移动后,必须非常小心不要使用auto_ptr:好的。

    1
    2
    3
    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的完美用例:好的。

    1
    2
    3
    4
    5
    6
    7
    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

    注意两个例子是如何遵循相同的句法模式的:好的。

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

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

    显然,表示auto_ptr变量的表达式a和表示按值返回auto_ptr的函数的调用的表达式make_triangle()之间一定有很大的区别,因此每次调用时都会创建一个新的临时auto_ptr对象。a是左值的一个例子,而make_triangle()是右值的一个例子。好的。

    从诸如a这样的lvalue迁移是危险的,因为稍后我们可以尝试通过a调用成员函数,调用未定义的行为。另一方面,从诸如make_triangle()这样的rvalue迁移是完全安全的,因为在复制构造函数完成它的工作之后,我们不能再使用临时的。没有表示所说的临时的表达式;如果我们简单地再写一次make_triangle(),我们会得到一个不同的临时表达式。事实上,从临时移动的已在下一行中删除:好的。

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

    注意,字母lr在任务的左侧和右侧具有历史渊源。在C++中,这不再是真的,因为L赋值不能出现在赋值左侧(如数组或用户定义的类型而没有赋值运算符),并且存在可以使用赋值运算符的所有类值的rVals。好的。

    An rvalue of class type is an expression whose evaluation creates a temporary object.
    Under normal circumstances, no other expression inside the same scope denotes the same temporary object.

    Ok.

    右值引用

    我们现在明白,从lvalues迁移是潜在的危险,但从rvalues迁移是无害的。如果C++有语言支持来区分LValk参数和RValk参数,我们可以完全禁止从LValk中移动,或者至少从调用站点中的LValk移动,这样我们就不会意外地移动。好的。

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

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

    1
    2
    3
    4
    5
    6
                lvalue   const lvalue   rvalue   const rvalue
    ---------------------------------------------------------              
    X&          yes
    const X&    yes      yes            yes      yes
    X&&                                 yes
    const X&&                           yes      yes

    实际上,你可以忘记const X&&。仅限于从rvalues中读取并不十分有用。好的。

    An rvalue reference X&& is a new kind of reference that only binds to rvalues.

    Ok.

    隐式转换

    右值引用经历了几个版本。自2.1版以来,如果存在从YX的隐式转换,则引用X&&的右值也绑定到不同类型Y的所有值类别。在这种情况下,将创建一个类型为X的临时变量,并且将右值引用绑定到该临时变量:好的。

    1
    2
    3
    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&&参数的函数的一个有用示例是move构造函数X::X(X&& source)。其目的是将托管资源的所有权从源转移到当前对象。好的。

    在C++ 11中,EDCOX1×26×Ed已被EDCOX1×27所取代,它利用了RValk引用。我将开发和讨论一个简化版的unique_ptr。首先,我们封装了一个原始指针并重载操作符->*,因此我们的类就像一个指针:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<typename T>
    class unique_ptr
    {
        T* ptr;

    public:

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

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

    构造函数取得对象的所有权,析构函数将其删除:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
        explicit unique_ptr(T* p = nullptr)
        {
            ptr = p;
        }

        ~unique_ptr()
        {
            delete ptr;
        }

    接下来是有趣的部分,移动构造函数:好的。

    1
    2
    3
    4
    5
        unique_ptr(unique_ptr&& source)   // note the rvalue reference
        {
            ptr = source.ptr;
            source.ptr = nullptr;
        }

    这个move构造函数与auto_ptrcopy构造函数的作用完全相同,但它只能与rvalues一起提供:好的。

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

    第二行编译失败,因为a是左值,但参数unique_ptr&& source只能绑定到rvalues。这正是我们想要的;危险的举动不应该是含蓄的。第三行编译得很好,因为make_triangle()是一个右值。移动建设者将所有权从临时转移到c。同样,这正是我们想要的。好的。

    The move constructor transfers ownership of a managed resource into the current object.

    Ok.

    移动分配运算符

    最后一个丢失的部分是移动分配运算符。其任务是释放旧资源并从其论点中获取新资源:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        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;
        }
    };

    注意这个move赋值操作符的实现如何复制析构函数和move构造函数的逻辑。您熟悉复制和交换习语吗?它还可以作为移动和交换习语应用于移动语义:好的。

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

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

    The move assignment operator transfers ownership of a managed resource into the current object, releasing the old resource.
    The move-and-swap idiom simplifies the implementation.

    Ok.

    从lvalues迁移

    有时候,我们想从伊瓦卢斯搬过来。也就是说,有时我们希望编译器将左值视为右值,这样它就可以调用移动构造函数,即使它可能不安全。为此,C++ 11提供了一个标准的库函数模板,称为EDCOX1 OR 8,在EDCOX1的头9中。这个名字有点不幸,因为std::move只将左值强制转换为右值;它本身不移动任何东西。它只允许移动。也许它应该被命名为std::cast_to_rvaluestd::enable_move,但我们现在仍然坚持这个名字。好的。

    下面是从左值显式移动的方式:好的。

    1
    2
    3
    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),我们的意图很明确:"亲爱的构造函数,为了初始化c,你想用a做什么都行;我不再关心a。您可以随意使用a。"好的。

    std::move(some_lvalue) casts an lvalue to an rvalue, thus enabling a subsequent move.

    Ok.

    X值

    注意,即使std::move(a)是一个右值,它的评估也不会创建一个临时对象。这个难题迫使委员会引入第三种价值观。一些可以绑定到右值引用的东西,即使在传统意义上它不是右值,也被称为XValue(过期值)。传统的rvalues被重新命名为prvalues(纯rvalues)。好的。

    prvalues和xvalues都是rvalues。xvalues和lvalues都是glvalues(广义lvalues)。用图表更容易理解这些关系:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
            expressions
              /     \
             /       \
            /         \
        glvalues   rvalues
          /  \       /  \
         /    \     /    \
        /      \   /      \
    lvalues   xvalues   prvalues

    请注意,只有XValues才是真正的新值;其余的只是由于重命名和分组。好的。

    C++98 rvalues are known as prvalues in C++11. Mentally replace all occurrences of"rvalue" in the preceding paragraphs with"prvalue".

    Ok.

    退出功能

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    unique_ptr<Shape> make_triangle()
    {
        return unique_ptr<Shape>(new Triangle);
    }          \-----------------------------/
                      |
                      | temporary is moved into c
                      |
                      v
    unique_ptr<Shape> c(make_triangle());

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

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

    为什么move构造函数接受左值result作为参数?result的作用域即将结束,将在堆栈展开时被破坏。后来没人会抱怨result发生了某种变化;当控制流回到调用方时,result就不存在了!出于这个原因,C++ 11有一个特殊的规则,允许从函数返回自动对象,而不必编写EDCOX1×5。实际上,您不应该使用std::move将自动对象移出函数,因为这会抑制"命名返回值优化"(nrvo)。好的。

    Never use std::move to move automatic objects out of functions.

    Ok.

    请注意,在这两个工厂函数中,返回类型都是值,而不是右值引用。右值引用仍然是引用,与往常一样,您不应该返回对自动对象的引用;如果您欺骗编译器接受您的代码,调用方将以悬空引用结束,如下所示:好的。

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

    Never return automatic objects by rvalue reference. Moving is exclusively performed by the move constructor, not by std::move, and not by merely binding an rvalue to an rvalue reference.

    Ok.

    移入成员

    迟早,你会写这样的代码:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Foo
    {
        unique_ptr<Shape> member;

    public:

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

    基本上,编译器会抱怨parameter是一个左值。如果您查看它的类型,您会看到一个右值引用,但是右值引用仅仅意味着"绑定到右值的引用";它并不意味着引用本身就是一个右值!实际上,parameter只是一个有名字的普通变量。您可以在构造函数的主体中尽可能频繁地使用parameter,并且它总是表示相同的对象。含蓄地离开它是危险的,因此语言禁止它。好的。

    A named rvalue reference is an lvalue, just like any other variable.

    Ok.

    解决方案是手动启用移动:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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根据需求隐式声明三个特殊成员函数,即当它们需要某个地方时:复制构造函数、复制赋值操作符和析构函数。好的。

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

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

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

    这两个新的特殊成员函数只有在没有手动声明任何特殊成员函数的情况下才隐式声明。另外,如果您声明自己的move构造函数或move赋值运算符,则不会隐式声明复制构造函数或复制赋值运算符。好的。

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

    If you write a class without unmanaged resources, there is no need to declare any of the five special member functions yourself, and you will get correct copy semantics and move semantics for free. Otherwise, you will have to implement the special member functions yourself. Of course, if your class does not benefit from move semantics, there is no need to implement the special move operations.

    Ok.

    请注意,复制分配运算符和移动分配运算符可以合并为一个统一的分配运算符,并按值取其参数:好的。

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

    这样,要实现的特殊成员函数数从5个减少到4个。在异常安全和效率之间有一个权衡,但我不是这个问题的专家。好的。转发引用(以前称为通用引用)

    考虑以下功能模板:好的。

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

    您可能期望T&&只绑定到rvalues,因为乍一看,它看起来像一个rvalue引用。但事实证明,T&&也与lvalues结合:好的。

    1
    2
    3
    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++仍然没有引用引用的概念,所以EDCOX1 OR 11的类型被折叠成EDCOX1(9)。一开始这听起来可能很混乱,毫无用处,但是引用折叠对于完美转发是必不可少的(这里不讨论)。好的。

    T&& is not an rvalue reference, but a forwarding reference. It also binds to lvalues, in which case T and T&& are both lvalue references.

    Ok.

    如果要将函数模板约束到rvalues,可以将sfinae与类型特征结合起来:好的。

    1
    2
    3
    4
    5
    #include <type_traits>

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

    搬迁的实施

    现在您了解了参考崩溃,下面是如何实现std::move:好的。

    1
    2
    3
    4
    5
    6
    template<typename T>
    typename std::remove_reference<T>::type&&
    move(T&& t)
    {
        return static_cast<typename std::remove_reference<T>::type&&>(t);
    }

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

    The call of a function that returns an rvalue reference, such as std::move, is an xvalue.

    Ok.

    注意,在本例中,通过右值引用返回是可以的,因为T并不表示自动对象,而是表示由调用方传入的对象。好的。好啊。


    移动语义基于右值引用。右值是一个临时对象,它将在表达式的末尾被销毁。在当前C++中,rValk只绑定到EDCOX1×5引用。C++1x将允许非EDCOX1的5值r值引用,拼写为EDCOX1×7,这是对RValk对象的引用。因为一个右值将在表达式的末尾消失,所以可以窃取它的数据。您不需要将其复制到另一个对象中,而是将其数据移动到该对象中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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参数是一个右值,我们知道它不再需要了,我们可以窃取它的值。因此,该值从从从f()返回的未命名临时变量移动到x(而初始化为空xx的数据移动到临时变量中,该临时变量将在分配后被销毁)。


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

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

    当您编写这样的代码时:

    1
    Matrix r = multiply(a, b);

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

    如果(像上面的Matrix示例一样),被复制的对象在堆上分配额外的内存来存储其内部表示,这一点尤其重要。复制构造函数必须对内部表示进行完整复制,或者在内部使用引用计数和写时复制语义。移动构造函数将只保留堆内存,只复制Matrix对象内的指针。


    如果你真的对移动语义有一个好的、深入的解释,我强烈推荐阅读他们的原始论文,"一个提议将移动语义支持添加到C++语言中。"

    这是非常容易接近和易于阅读,它是一个很好的理由,他们提供的好处。在wg21网站上还有其他关于移动语义的最新和最新的文章,但是这篇文章可能是最直接的,因为它从一个顶层的角度来处理事情,并且没有太多关于粗糙语言细节的内容。


    移动语义是指当不再有人需要源值时,转移资源而不是复制它们。

    在C++ 03中,对象经常被复制,只在任何代码再次使用该值之前被销毁或分配。例如,当您从一个函数返回值时,除非rvo打乱了您要返回的值,否则将复制到调用方的堆栈帧,然后它将超出范围并被销毁。这只是许多例子中的一个:当源对象是临时对象时,请参见传递值,例如仅重新排列项的sort,当超过capacity()时,在vector中重新分配,等等。

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


    简单(实际)地说:好的。

    复制一个对象意味着复制它的"静态"成员,并为它的动态对象调用new操作符。对吗?好的。

    1
    2
    3
    4
    5
    6
    7
    8
    class A
    {
       int i, *p;

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

    但是,移动一个对象(我在实践中重复)意味着只复制动态对象的指针,而不创建新的指针。好的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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对象,…,您可以使用不同的名称调用它):好的。

    1
    void heavyFunction(HeavyType());

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

    这就引出了"右值"引用的概念。它们存在于C++ 11中,仅检测接收到的对象是否匿名。我认为您已经知道"lvalue"是一个可分配的实体(=运算符的左侧),因此您需要一个对对象的命名引用,以便能够充当lvalue。右值正好相反,是一个没有命名引用的对象。因此,匿名对象和右值是同义词。所以:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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构造函数,并且您知道对象是临时的,您可以移动其动态对象,而不是复制它们,从而节省空间和内存。好的。

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

    如果您的对象很复杂,并且析构函数具有其他副作用,例如调用库的函数、调用其他全局函数或其他全局函数,则最好用标志来表示移动:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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&&有什么区别?当然,在第一种情况下,你可以修改对象,在第二种情况下不能,但是,实际意义是什么?在第二种情况下,您不能修改它,所以您没有方法使对象无效(除了使用可变标志或类似的东西),并且复制构造函数没有实际的区别。好的。

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

    1
    2
    3
    4
    void some_function(A&& a)
    {
       other_function(a);
    }

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

    1
    other_function(std::move(a));

    通过这条线,std::moveA强制转换为右值,other_function将接收对象作为未命名对象。当然,如果other_function没有特定的重载来处理未命名的对象,那么这种区别就不重要了。好的。

    这是完美的转发吗?不是,但我们很亲近。完美转发只对使用模板有用,其目的是说:如果我需要将对象传递给另一个函数,我需要,如果我接收到一个命名对象,该对象将作为命名对象传递,如果不传递,我希望像传递未命名对象一样传递它:好的。

    1
    2
    3
    4
    5
    template<typename T>
    void some_function(T&& a)
    {
       other_function(std::forward<T>(a));
    }

    这是一个使用完美转发的原型函数的签名,通过EDCOX1(0)来实现在C++ 11中。此函数利用模板实例化的一些规则:好的。

    1
    2
     `A& && == A&`
     `A&& && == A&&`

    因此,如果T是对A的左值引用(t=a&;),A也是(a&;&;=>a&;)。如果T是对A的右值引用,则A也是(a&;&;&;=>a&;&;)。在这两种情况下,A都是实际作用域中的命名对象,但T从调用方作用域的角度包含其"引用类型"的信息。此信息(T作为模板参数传递给forward并根据T的类型移动或不移动'A'。好的。好啊。


    这类似于复制语义,但不必复制所有的数据就可以从被"移动"的对象中窃取数据。


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

    move semantics基本上是一个用户定义的类型,其构造函数接受一个r值引用(使用&;&;(yes two ampersands)的新引用类型),它是非常量,这称为move构造函数,赋值运算符也是如此。因此,move构造函数做什么呢?它不是从源参数复制内存,而是从源参数"移动"内存到目标。

    你想什么时候做?例如,假设您创建了一个临时的std::vector,然后从函数say返回它:

    1
    std::vector<foo> get_foos();

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


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

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

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

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

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

    已经构造了两个新对象,其中一个是临时对象,只用于函数的持续时间。

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

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

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

    1
    2
    3
    4
    5
    6
    // 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);
    }

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

    1
    2
    3
    4
    5
    6
    // 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:

    To make strong exception guarantee possible, user-defined move constructors should not throw exceptions. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated.
    If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable.
    In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision.
    A constructor is called a 'move constructor' when it takes an rvalue reference as a parameter. It is not obligated to move anything, the class is not required to have a resource to be moved and a 'move constructor' may not be able to move a resource as in the allowable (but maybe not sensible) case where the parameter is a const rvalue reference (const T&&).


    我写这个是为了确保我能正确理解。

    创建了移动语义以避免不必要地复制大型对象。Bjarne Stroustrup在他的《C++程序设计语言》一书中使用了两个例子:缺省情况下不必要的复制:一个是两个大对象的交换,两个是从一个方法返回一个大对象。

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

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

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