什么是移动语义?

dic*_*oce 1614 c++ c++-faq move-semantics c++11

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

fre*_*low 2366

I find it easiest to understand move semantics with example code. Let's start with a very simple string class which only holds a pointer to a heap-allocated block of memory:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }
Run Code Online (Sandbox Code Playgroud)

Since we chose to manage the memory ourselves, we need to follow the rule of three. I am going to defer writing the assignment operator and only implement the destructor and the copy constructor for now:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }
Run Code Online (Sandbox Code Playgroud)

The copy constructor defines what it means to copy string objects. The parameter const string& that binds to all expressions of type string which allows you to make copies in the following examples:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3
Run Code Online (Sandbox Code Playgroud)

Now comes the key insight into move semantics. Note that only in the first line where we copy x is this deep copy really necessary, because we might want to inspect x later and would be very surprised if x had changed somehow. Did you notice how I just said x three times (four times if you include this sentence) and meant the exact same object every time? We call expressions such as x "lvalues".

第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;
    }
Run Code Online (Sandbox Code Playgroud)

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

恭喜,您现在了解移动语义的基础知识!让我们继续实现赋值运算符.如果您不熟悉复制和交换习惯用法,请学习它并返回,因为它是一个与异常安全相关的令人敬畏的C++习惯用法.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};
Run Code Online (Sandbox Code Playgroud)

嗯,就是这样吗?"右值参考在哪里?" 你可能会问."我们这里不需要它!" 是我的答案:)

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

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

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

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

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

  • @einpoklum因为没有`that.data = 0`,字符会被太早破坏(当临时死亡时),也会被破坏两次.你想窃取数据,而不是分享它! (66认同)
  • @但如果我的ctor得到一个rvalue,以后永远不能使用,为什么我甚至需要打扰它保持一致/安全状态?而不是设置that.data = 0,为什么不留下它? (36认同)
  • @einpoklum定期调度的析构函数仍然运行,因此您必须确保源对象的移动后状态不会导致崩溃.更好的是,您应该确保源对象也可以是赋值或其他写入的接收者. (15认同)
  • @pranitkothari是的,所有对象都必须被破坏,甚至是从对象移动.因为我们不希望在发生这种情况时删除char数组,所以我们必须将指针设置为null. (12认同)
  • @ Virus721`nullptr上的delete []`被C++标准定义为无操作. (6认同)
  • @binaryguy我很高兴即使在7年后,这个答案似乎对我在世界各地的程序员来说有点有用:) (4认同)
  • 为什么这个名为*move*语义?该名称表明内存中的某些对象实际上正在被移动(无论这意味着什么) - 但这仅仅是关于转移数据的所有权. (3认同)
  • @ user1520427一旦程序员自己声明了一个特殊的成员函数(复制/移动构造函数,复制/移动赋值,析构函数),就不会隐式生成它们.在这种情况下,隐式生成的移动赋值运算符无论如何都会做错误的事情,因为`data = std :: move(that.data)`只会复制**指针.这是正确的,移动指针(或任何其他标量数据类型)被定义为具有与复制相同的效果. (3认同)
  • @adnako因为`strlen`不计算终止NUL字符. (2认同)
  • @jco我同意.*转移语义*可能是一个更好的选择:) (2认同)
  • 很好的例子!我正在逐步完成您的示例,但我无法调用移动构造函数。这是复制省略的 b/c 吗? (2认同)

fre*_*low 1023

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

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
    
        // ...
    };
    
    Run Code Online (Sandbox Code Playgroud)
  2. 实施安全的"仅移动"类型; 也就是说,复制没有意义但是移动的类型.示例包括锁,文件句柄和具有唯一所有权语义的智能指针.注意:这个答案讨论了std::auto_ptr一个不推荐使用的C++ 98标准库模板,该模板std::unique_ptr在C++ 11中被替换.中级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
Run Code Online (Sandbox Code Playgroud)

不寻常的auto_ptr是它的"复制"行为:

auto_ptr<Shape> a(new Triangle);

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

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+
Run Code Online (Sandbox Code Playgroud)

注意如何初始化ba复制三角形,而是从转移三角形的所有权ab.我们也可以说" 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
}
Run Code Online (Sandbox Code Playgroud)

危险且无害的动作

危险的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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

auto_ptr<Shape> variable(expression);
double area = expression->area();
Run Code Online (Sandbox Code Playgroud)

And yet, one of them invokes undefined behavior, whereas the other one does not. So what is the difference between the expressions a and make_triangle()? Aren't they both of the same type? Indeed they are, but they have different value categories.

Value categories

Obviously, there must be some profound difference between the expression a which denotes an auto_ptr variable, and the expression make_triangle() which denotes the call of a function that returns an auto_ptr by value, thus creating a fresh temporary auto_ptr object every time it is called. a is an example of an lvalue, whereas make_triangle() is an example of an rvalue.

Moving from lvalues such as a is dangerous, because we could later try to call a member function via a, invoking undefined behavior. On the other hand, moving from rvalues such as make_triangle() is perfectly safe, because after the copy constructor has done its job, we cannot use the temporary again. There is no expression that denotes said temporary; if we simply write make_triangle() again, we get a different temporary. In fact, the moved-from temporary is already gone on the next line:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here
Run Code Online (Sandbox Code Playgroud)

Note that the letters l and r have a historic origin in the left-hand side and right-hand side of an assignment. This is no longer true in C++, because there are lvalues which cannot appear on the left-hand side of an assignment (like arrays or user-defined types without an assignment operator), and there are rvalues which can (all rvalues of class types with an assignment operator).

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.

Rvalue references

We now understand that moving from lvalues is potentially dangerous, but moving from rvalues is harmless. If C++ had language support to distinguish lvalue arguments from rvalue arguments, we could either completely forbid moving from lvalues, or at least make moving from lvalues explicit at call site, so that we no longer move by accident.

C++11's answer to this problem is rvalue references. An rvalue reference is a new kind of reference that only binds to rvalues, and the syntax is X&&. The good old reference X& is now known as an lvalue reference. (Note that X&& is not a reference to a reference; there is no such thing in C++.)

If we throw const into the mix, we already have four different kinds of references. What kinds of expressions of type X can they bind to?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes
Run Code Online (Sandbox Code Playgroud)

In practice, you can forget about const X&&. Being restricted to read from rvalues is not very useful.

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

Implicit conversions

Rvalue references went through several versions. Since version 2.1, an rvalue reference X&& also binds to all value categories of a different type Y, provided there is an implicit conversion from Y to X. In that case, a temporary of type X is created, and the rvalue reference is bound to that temporary:

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

some_function("hello world");
Run Code Online (Sandbox Code Playgroud)

In the above example, "hello world" is an lvalue of type const char[12]. Since there is an implicit conversion from const char[12] through const char* to std::string, a temporary of type std::string is created, and r is bound to that temporary. This is one of the cases where the distinction between rvalues (expressions) and temporaries (objects) is a bit blurry.

Move constructors

A useful example of a function with an X&& parameter is the move constructor X::X(X&& source). Its purpose is to transfer ownership of the managed resource from the source into the current object.

In C++11, std::auto_ptr<T> has been replaced by std::unique_ptr<T> which takes advantage of rvalue references. I will develop and discuss a simplified version of unique_ptr. First, we encapsulate a raw pointer and overload the operators -> and *, so our class feels like a pointer:

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

public:

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

    T& operator*() const
    {
        return *ptr;
    }
Run Code Online (Sandbox Code Playgroud)

The constructor takes ownership of the object, and the destructor deletes it:

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

    ~unique_ptr()
    {
        delete ptr;
    }
Run Code Online (Sandbox Code Playgroud)

Now comes the interesting part, the move constructor:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }
Run Code Online (Sandbox Code Playgroud)

This move constructor does exactly what the auto_ptr copy constructor did, but it can only be supplied with rvalues:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay
Run Code Online (Sandbox Code Playgroud)

The second line fails to compile, because a is an lvalue, but the parameter unique_ptr&& source can only be bound to rvalues. This is exactly what we wanted; dangerous moves should never be implicit. The third line compiles just fine, because make_triangle() is an rvalue. The move constructor will transfer ownership from the temporary to c. Again, this is exactly what we wanted.

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

Move assignment operators

The last missing piece is the move assignment operator. Its job is to release the old resource and acquire the new resource from its argument:

    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;
    }
};
Run Code Online (Sandbox Code Playgroud)

Note how this implementation of the move assignment operator duplicates logic of both the destructor and the move constructor. Are you familiar with the copy-and-swap idiom? It can also be applied to move semantics as the move-and-swap idiom:

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

Now that source is a variable of type unique_ptr, it will be initialized by the move constructor; that is, the argument will be moved into the parameter. The argument is still required to be an rvalue, because the move constructor itself has an rvalue reference parameter. When control flow reaches the closing brace of operator=, source goes out of scope, releasing the old resource automatically.

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.

Moving from lvalues

Sometimes, we want to move from lvalues. That is, sometimes we want the compiler to treat an lvalue as if it were an rvalue, so it can invoke the move constructor, even though it could be potentially unsafe. For this purpose, C++11 offers a standard library function template called std::move inside the header <utility>. This name is a bit unfortunate, because std::move simply casts an lvalue to an rvalue; it does not move anything by itself. It merely enables moving. Maybe it should have been named std::cast_to_rvalue or std::enable_move, but we are stuck with the name by now.

Here is how you explicitly move from an lvalue:

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

Note that after the third line, a no longer owns a triangle. That's okay, because by explicitly writing std::move(a), we made our intentions clear: "Dear constructor, do whatever you want with a in order to initialize c; I don't care about a anymore. Feel free to have your way with a."

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

Xvalues

Note that even though std::move(a) is an rvalue, its evaluation does not create a temporary object. This conundrum forced the committee to introduce a third value category. Something that can be bound to an rvalue reference, even though it is not an rvalue in the traditional sense, is called an xvalue (eXpiring value). The traditional rvalues were renamed to prvalues (Pure rvalues).

Both prvalues and xvalues are rvalues. Xvalues and lvalues are both glvalues (Generalized lvalues). The relationships are easier to grasp with a diagram:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues
Run Code Online (Sandbox Code Playgroud)

Note that only xvalues are really new; the rest is just due to renaming and grouping.

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

Moving out of functions

So far, we have seen movement into local variables, and into function parameters. But moving is also possible in the opposite direction. If a function returns by value, some object at call site (probably a local variable or a temporary, but could be any kind of object) is initialized with the expression after the return statement as an argument to the move constructor:

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

Perhaps surprisingly, automatic objects (local variables that are not declared as static) can also be implicitly moved out of functions:

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

How come the move constructor accepts the lvalue result as an argument? The scope of result is about to end, and it will be destroyed during stack unwinding. Nobody could possibly complain afterwards that result had changed somehow; when control flow is back at the caller, result does not exist anymore! For that reason, C++11 has a special rule that allows returning automatic objects from functions without having to write std::move. In fact, you should never use std::move to move automatic objects out of functions, as this inhibits the "named return value optimization" (NRVO).

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

Note that in both factory functions, the return type is a value, not an rvalue reference. Rvalue references are still references, and as always, you should never return a reference to an automatic object; the caller would end up with a dangling reference if you tricked the compiler into accepting your code, like this:

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

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.

Moving into members

Sooner or later, you are going to write code like this:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};
Run Code Online (Sandbox Code Playgroud)

Basically, the compiler will complain that parameter is an lvalue. If you look at its type, you see an rvalue reference, but an rvalue reference simply means "a reference that is bound to an rvalue"; it does not mean that the reference itself is an rvalue! Indeed, parameter is just an ordinary variable with a name. You can use parameter as often as you like inside the body of the constructor, and it always denotes the same object. Implicitly moving from it would be dangerous, hence the language forbids it.

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

The solution is to manually enable the move:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};
Run Code Online (Sandbox Code Playgroud)

You could argue that parameter is not used anymore after the initialization of member. Why is there no special rule to silently insert std::move just as with return values? Probably because it would be too much burden on the compiler implementors. For example, what if the constructor body was in another translation unit? By contrast, the return value rule simply has to check the symbol tables to determine whether or not the identifier after the return keyword denotes an automatic object.

You can also pass parameter by value. For move-only types like unique_ptr, it seems there is no established idiom yet. Personally, I prefer pass by value, as it causes less clutter in the interface.

Special member functions

C++98 implicitly declares three special member functions on demand, that is, when they are needed somewhere: the copy constructor, the copy assignment operator and the destructor.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor
Run Code Online (Sandbox Code Playgroud)

Rvalue references went through several versions. Since version 3.0, C++11 declares two additional special member functions on demand: the move constructor and the move assignment operator. Note that neither VC10 nor VC11 conform to version 3.0 yet, so you will have to implement them yourself.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator
Run Code Online (Sandbox Code Playgroud)

These two new special member functions are only implicitly declared if none of the special member functions are declared manually. Also, if you declare your own move constructor or move assignment operator, neither the copy constructor nor the copy assignment operator will be declared implicitly.

What do these rules mean in practice?

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.

Note that the copy assignment operator and the move assignment operator can be fused into a single, unified assignment operator, taking its argument by value:

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

This way, the number of special member functions to implement drops from five to four. There is a tradeoff between exception-safety and efficiency here, but I am not an expert on this issue.

Forwarding references (previously known as Universal references)

Consider the following function template:

template<typename T>
void foo(T&&);
Run Code Online (Sandbox Code Playgroud)

You might expect T&& to only bind to rvalues, because at first glance, it looks like an rvalue reference. As it turns out though, T&& also binds to lvalues:

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>&
Run Code Online (Sandbox Code Playgroud)

If the argument is an rvalue of type X, T is deduced to be X, hence T&& means X&&. This is what anyone would expect. But if the argument is an lvalue of type X, due to a special rule, T is deduced to be X&, hence T&& would mean something like X& &&. But since C++ still has no notion of references to references, the type X& && is collapsed into X&. This may sound confusing and useless at first, but reference collapsing is essential for perfect forwarding (which will not be discussed here).

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.

If you want to constrain a function template to rvalues, you can combine SFINAE with type traits:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Run Code Online (Sandbox Code Playgroud)

Implementation of move

Now that you understand reference collapsing, here is how std::move is implemented:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Run Code Online (Sandbox Code Playgroud)

As you can see, move accepts any kind of parameter thanks to the forwarding reference T&&, and it returns an rvalue reference. The std::remove_reference<T>::type meta-function call is necessary because otherwise, for lvalues of type X, the return type would be X& &&, which would collapse into X&. Since t is always an lvalue (remember that a named rvalue reference is an lvalue), but we want to bind t to an rvalue reference, we have to explicitly cast t to the correct return type. The call of a function that returns an rvalue reference is itself an xvalue. Now you know where xvalues come from ;)

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

Note that returning by rvalue reference is fine in this example, because t does not denote an automatic object, but instead an object that was passed in by the caller.

  • ["我的显示器上只有10页"](http://chat.stackoverflow.com/transcript/message/4581272#4581272) (193认同)
  • 移动语义很重要的第三个原因是:异常安全.通常复制操作可能抛出(因为它需要分配资源而分配可能失败),移动操作可能是无法抛出的(因为它可以转移现有资源的所有权而不是分配新资源).拥有不会失败的操作总是很好,在编写提供异常保证的代码时,这一点至关重要. (24认同)
  • @halivingston非常感谢您的反馈,我真的很感激.写一本书的问题是:它比你想象的要多得多.如果你想深入研究C++ 11及更高版本,我建议你购买Scott Meyers的"Effective Modern C++". (12认同)
  • 现在请写一本书...这个答案让我有理由相信,如果你以清醒的方式涵盖C++的其他角落,成千上万的人会理解它. (8认同)
  • 我跟你一起参加'普遍参考',但是接下来就太抽象了.参考崩溃?完美转发?如果类型是模板化的,你是说rvalue引用成为通用引用吗?我希望有一种方法可以解释这一点,以便我知道我是否需要理解它!:) (7认同)
  • @stephematician在expr.prim.general下,C++标准明确指出`字符串文字是左值; 所有其他文字都是prvalues (5认同)
  • 嘿弗雷德,猜猜怎么着?在C++ 17中对l/r值系统进行了重写/重写,并且有一些涉及破坏源的新移动机制. (4认同)
  • 学习这门语言就像挑选哪些东西,你可以学会不知不觉地生活.我不知道一切. (4认同)
  • “ hello world”是右值还是左值(在“隐式转换”部分中)? (2认同)
  • @shayan确保您可以获取其地址,`const char(* p)[12] =&“ hello world”;`是完全有效的C ++。 (2认同)

sbi*_*sbi 76

移动语义基于右值引用.
右值是一个临时对象,它将在表达式的末尾被销毁.在当前的C++中,rvalues只绑定到const引用.C++ 1x将允许const拼写的非右值引用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
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,用旧的编译器的结果f()复制x使用X的拷贝构造.如果你的编译器支持移动语义并且X有一个move-constructor,那么就调用它.由于它的rhs论证是一个rvalue,我们知道它不再需要它,我们可以窃取它的价值.
所以价值移动从无名临时从返回f()x(而的数据x,初始化为空X,移动到暂时的,这将在转让之后被摧毁).


Gre*_*ill 59

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

Matrix multiply(const Matrix &a, const Matrix &b);
Run Code Online (Sandbox Code Playgroud)

当你编写这样的代码时:

Matrix r = multiply(a, b);
Run Code Online (Sandbox Code Playgroud)

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

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

  • @dicroce:一个制作一个空白对象,一个制作副本.如果存储在对象中的数据很大,则副本可能很昂贵.例如,std :: vector. (3认同)
  • 移动构造函数和复制构造函数有何不同? (2认同)
  • @dicroce:它们在语法上有所不同,一个看起来像 Matrix(const Matrix&amp; src)(复制构造函数),另一个看起来像 Matrix(Matrix&amp;&amp; src)(移动构造函数),请查看我的主要答案以获得更好的示例。 (2认同)
  • @Jichao:这是一个名为RVO的优化,有关差异的更多信息,请参阅此问题:http://stackoverflow.com/questions/5031778/what-optimization-does-move-semantics-provide-if-we-already-有-RVO (2认同)

Jam*_*lis 30

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

它非常易于阅读,并且非常适合它们提供的好处.在WG21网站上还有其他关于移动语义的最新和最新的论文,但是这个可能是最直接的,因为它从顶层视图处理事物并且没有深入到粗略的语言细节.


Dav*_*ams 26

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

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

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


Per*_*-lk 23

简单(实用)术语:

复制对象意味着复制其"静态"成员并new为其动态对象调用操作符.对?

class A
{
   int i, *p;

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

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

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

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. 
};
Run Code Online (Sandbox Code Playgroud)

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

void heavyFunction(HeavyType());
Run Code Online (Sandbox Code Playgroud)

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

这导致了"右值"参考的概念.它们仅存在于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; }
};
Run Code Online (Sandbox Code Playgroud)

在这种情况下,当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 */ }
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

void some_function(A&& a)
{
   other_function(a);
}
Run Code Online (Sandbox Code Playgroud)

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

other_function(std::move(a));
Run Code Online (Sandbox Code Playgroud)

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

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

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}
Run Code Online (Sandbox Code Playgroud)

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

 `A& && == A&`
 `A&& && == A&&`
Run Code Online (Sandbox Code Playgroud)

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


Ter*_*fey 19

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


snk*_*kid 13

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

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

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

std::vector<foo> get_foos();
Run Code Online (Sandbox Code Playgroud)

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


Chr*_*s B 8

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

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

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

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

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


And*_* DM 7

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

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

T f(T o) { return o; }
  //^^^ new object constructed
Run Code Online (Sandbox Code Playgroud)

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

T b = f(a);
  //^ new object constructed
Run Code Online (Sandbox Code Playgroud)

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

从返回值创建新对象时,将调用复制构造函数以临时对象的内容复制到新对象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);
}
Run Code Online (Sandbox Code Playgroud)

通过移动语义,现在可以通过简单地移动数据而不是复制来使大部分工作变得不那么令人不愉快.

// 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);
}
Run Code Online (Sandbox Code Playgroud)

移动数据涉及将数据与新对象重新关联.并且根本不会发生任何副本.

这是通过rvalue参考完成的.
一个rvalue参考的工作非常像一个lvalue有一个重要区别参考:
一个右值引用可以移动左值不能.

来自cppreference.com:

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