c++ - 转移构造函数 - 什么是移动语义?




移动语义完美转发 (8)

Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you're returning is copied to the caller's stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it's typically because the object owns some heavyweight resource. For example, vector<string> may own a dynamically-allocated memory block containing an array of string objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector<string> means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.

我刚刚听完了关于C++0x关于Scott Meyers的软件工程无线电播客采访 。 大部分新功能对我来说都很有意义,而且我现在对C ++ 0x感到非常兴奋,除了一个。 我仍然没有得到移动语义 ...他们究竟是什么?


I'm writing this to make sure I understand it properly.

Move semantics were created to avoid the unnecessary copying of large objects. Bjarne Stroustrup in his book "The C++ Programming Language" uses two examples where unnecessary copying occurs by default: one, the swapping of two large objects, and two, the returning of a large object from a method.

Swapping two large objects usually involves copying the first object to a temporary object, copying the second object to the first object, and copying the temporary object to the second object. For a built-in type, this is very fast, but for large objects these three copies could take a large amount of time. A "move assignment" allows the programmer to override the default copy behavior and instead swap references to the objects, which means that there is no copying at all and the swap operation is much faster. The move assignment can be invoked by calling the std::move() method.

Returning an object from a method by default involves making a copy of the local object and its associated data in a location which is accessible to the caller (because the local object is not accessible to the caller and disappears when the method finishes). When a built-in type is being returned, this operation is very fast, but if a large object is being returned, this could take a long time. The move constructor allows the programmer to override this default behavior and instead "reuse" the heap data associated with the local object by pointing the object being returned to the caller to heap data associated with the local object. Thus no copying is required.

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


In easy (practical) terms:

Copying an object means copying its "static" members and calling the new operator for its dynamic objects. 对?

class A
{
   int i, *p;

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

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should "invalidate" the source pointers to avoid destructing them twice:

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. 
};

Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that's very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, ..., you can call it with different names):

void heavyFunction(HeavyType());

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don't need the anonymous object and you can save time and memory.

This leads to the concept of an "rvalue" reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an "lvalue" is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. 所以:

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

In this case, when an object of type A should be "copied", the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that "static" objects are always copied. There's no ways to "move" a static object (object in stack and not on heap). So, the distinction "move"/ "copy" when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library's function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

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

So, your code is shorter (you don't need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&& ? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can't modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding ? It is important to know that a "rvalue reference" is a reference to a named object in the "caller's scope". But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn't received like a temporal object.

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

The object a would be copied to the actual parameter of other_function . If you want the object a continues being treated as a temporary object, you should use the std::move function:

other_function(std::move(a));

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

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

That's the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward . This function exploits some rules of template instantiation:

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

So, if T is a lvalue reference to A ( T = A&), a also ( A& && => A&). If T is a rvalue reference to A , a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its "reference type" from the caller scope's point of view. This information ( T ) is passed as template parameter to forward and 'a' is moved or not according to the type of T .


It's like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being "moved" from.


You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it's source argument it 'moves' memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

std::vector<foo> get_foos();

You're going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it's pointers and 'move' dynamically allocated memory to the new instance. It's kind of like transfer-of-ownership semantics with std::auto_ptr.


假设你有一个返回实质对象的函数:

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

当你编写这样的代码时:

Matrix r = multiply(a, b);

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

如果(比如上面的Matrix示例)这个特别重要,被复制的对象会在堆上分配额外的内存来存储其内部表示。 复制构造函数将不得不制作内部表示的完整副本,或者间接使用引用计数和写入时复制语义。 移动构造函数将单独留下堆内存,并将指针复制到Matrix对象内。


我的第一个回答是移动语义的一个非常简单的介绍,并且许多细节被保留下来以保持简单。 然而,移动语义还有很多,我认为是第二次填补空白的时候了。 第一个答案已经很老了,而且把它换成完全不同的文本并不合适。 我认为它作为第一次介绍仍然很好。 但如果你想深入挖掘,请阅读:)

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 | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

注意如何用a初始化b 不会复制三角形,而是将三角形的所有权从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的危险之处在于语法上看起来像副本的内容实际上是一种移动。 尝试在移出的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()之间有什么区别? 他们不是同一类型吗? 事实上他们是,但他们有不同的价值类别

价值类别

显然,表达式a中的auto_ptr变量和表达式make_triangle()之间必定存在某种深刻的区别,它表示调用一个函数的调用,该函数按值返回一个auto_ptr ,因此每次调用它时都会创建一个新的临时auto_ptr对象。 a是一个左值的例子,而make_triangle()是一个右值的例子。

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

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

请注意,字母lr在作业的左侧和右侧具有历史渊源。 在C ++中这不再是真的,因为有左值不能出现在赋值的左侧(比如数组或者没有赋值操作符的用户定义类型),并且有一些右值(类型的所有右值与一个赋值操作符)。

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

右值引用

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

C ++ 11对这个问题的答案是右值引用 。 右值引用是一种新的引用,它只绑定到右值,语法是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&&是一种新的引用,只能绑定到右值。

隐式转换

右值引用经历了几个版本。 从版本2.1开始,右值引用X&&也绑定到不同类型Y所有值类别,前提是存在从YX的隐式转换。 在这种情况下,会创建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(表达式)和临时对象(对象)之间的区别有点模糊的情况之一。

移动构造函数

具有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类型的变量,它将由移动构造函数初始化; 也就是说,参数将被移入参数中。 该参数仍然需要是右值,因为移动构造函数本身具有右值引用参数。 当控制流达到operator=的右大括号时, source超出范围,自动释放旧的资源。

移动赋值操作符将托管资源的所有权转移到当前对象中,释放旧资源。 移动和交换习惯用法简化了实现。

从左值移动

有时候,我们想从左值移动。 也就是说,有时我们希望编译器将左值视为右值,因此它可以调用移动构造函数,即使它可能不安全。 为此,C ++ 11在头文件<utility>提供了一个名为std::move的标准库函数模板。 这个名字有点不幸,因为std::move只是将左值转换为右值; 它本身不会移动任何东西。 它只是使移动。 也许它应该被命名为std::cast_to_rvalue或者std::enable_move ,但是现在我们被std::cast_to_rvalue了这个名字。

以下是你如何从一个左值显式移动:

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

请注意,在第三行之后,不再拥有一个三角形。 没关系,因为通过明确地std::move(a) ,我们明确地表达了我们的意图:“亲爱的构造函数,为了初始化c做任何你想要的;我不再关心a了。用你的方式。“

std::move(some_lvalue)将左值转换为右值,从而启用后续移动。

Xvalues

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

prvalues和xvalues都是rvalues。 Xvalues和Lvalues都是glvalues (广义左值 )。 用关系图更容易理解关系:

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

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

C ++ 98 rvalues在C ++ 11中被称为prvalues。 在前面的段落中将所有的“右值”用“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将自动对象移出函数。

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

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
    {}
};

您可能会争辩说, parametermember初始化后不再使用。 为什么没有像返回值一样静静插入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

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

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

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

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

如果你编写一个没有非托管资源的类,不需要自己声明任何五个特殊成员函数,并且你将得到正确的复制语义并且免费移动语义。 否则,你将不得不自己实现特殊的成员函数。 当然,如果你的类没有从移动语义中获益,就不需要实现特殊移动操作。

请注意,可以将复制赋值运算符和移动赋值运算符合并为一个统一的赋值运算符,并按值赋值:

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

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

转发引用( previously称为通用引用

考虑下面的函数模板:

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&&都是左值引用。

如果你想限制一个函数模板为右值,你可以将SFINAE和类型特征结合起来:

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

正如你所看到的, move通过转发引用T&&接受任何类型的参数,并且它返回一个右值引用。 std::remove_reference<T>::type元函数调用是必须的,因为否则,对于X类型的左值,返回类型将是X& && ,它会折叠为X& 。 由于t总是一个左值(记住一个名为右值的引用是一个左值),但我们希望将t绑定到右值引用,所以我们必须明确地将t赋给正确的返回类型。 一个返回右值引用的函数本身就是一个xvalue。 现在你知道xvalues来自哪里;)

调用返回右值引用的函数(如std::move )是一个xvalue。

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


移动语义基于右值引用
右值是一个临时对象,它将在表达式的末尾被销毁。 在当前的C ++中,右值只能绑定到const引用。 C ++ 1x将允许非常量右值引用,拼写T&& ,它们是对右值对象的引用。
由于右值将在表达式的末尾死亡,因此可以窃取其数据 。 不是复制到另一个对象中,而是其数据移入其中。

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 (而x的数据,初始化为空X ,移动到临时文件中,这会在分配后被销毁)。





move-semantics