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); } |
复制构造函数定义了复制字符串对象的含义。参数
1 2 3 | string a(x); // Line 1 string b(x + y); // Line 2 string c(some_function_returning_a_string()); // Line 3 |
现在有了对移动语义的关键洞察。请注意,只有在我们复制
第2行和第3行中的参数不是lvalue,而是rvalue,因为底层字符串对象没有名称,所以客户机无法在以后的时间点再次检查它们。rvalues表示临时对象,这些对象在下一个分号处被破坏(更精确地说:在词法上包含rvalue的完整表达式的末尾)。这一点很重要,因为在初始化
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; } }; |
嗯,就是这样吗?"右值引用在哪里?"你可能会问。"我们这里不需要它!"我的回答是:好的。
注意,我们通过值传递参数
因此,如果您说
但是如果您说
总之,复制构造函数进行深度复制,因为源必须保持不变。另一方面,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)。如果您对
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 |
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 | | | | | +---+ | | +---+ | +---------+ +---------+ |
请注意,使用
To move an object means to transfer ownership of some resource it manages to another object.
Ok.
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 } |
危险无害的行动
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 |
但江户十一〔四〕并不总是危险的。工厂功能是
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(); |
然而,其中一个调用未定义的行为,而另一个则不调用。那么,
显然,表示
从诸如
1 2 | auto_ptr<Shape> c(make_triangle()); ^ the moved-from temporary dies right here |
注意,字母
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引用。右值引用是一种只绑定到右值的新引用,语法为
如果我们将
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 |
实际上,你可以忘记
An rvalue reference
X&& is a new kind of reference that only binds to rvalues.Ok.
隐式转换
右值引用经历了几个版本。自2.1版以来,如果存在从
1 2 3 | void some_function(std::string&& r); some_function("hello world"); |
在上面的示例中,
带有
在C++ 11中,EDCOX1×26×Ed已被EDCOX1×27所取代,它利用了RValk引用。我将开发和讨论一个简化版的
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构造函数与
1 2 3 | unique_ptr<Shape> a(new Triangle); unique_ptr<Shape> b(a); // error unique_ptr<Shape> c(make_triangle()); // okay |
第二行编译失败,因为
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; } }; |
既然
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中。这个名字有点不幸,因为
下面是从左值显式移动的方式:好的。
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 |
注意,在第三行之后,
std::move(some_lvalue) casts an lvalue to an rvalue, thus enabling a subsequent move.Ok.
X值
注意,即使
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.
退出功能
到目前为止,我们已经看到了局部变量和函数参数的移动。但也可以朝相反的方向移动。如果函数按值返回,则调用站点的某个对象(可能是局部变量或临时对象,但可以是任何类型的对象)将用
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()); |
也许令人惊讶的是,自动对象(未声明为
1 2 3 4 5 | unique_ptr<Shape> make_square() { unique_ptr<Shape> result(new Square); return result; // note the missing std::move } |
为什么move构造函数接受左值
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 {} }; |
基本上,编译器会抱怨
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 {} }; |
你可以说,在
您还可以按值传递
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&&); |
您可能期望
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>& |
如果参数是
T&& is not an rvalue reference, but a forwarding reference. It also binds to lvalues, in which case
T andT&& 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&&); |
搬迁的实施
现在您了解了参考崩溃,下面是如何实现
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); } |
如您所见,
The call of a function that returns an rvalue reference, such as
std::move , is an xvalue.Ok.
注意,在本例中,通过右值引用返回是可以的,因为
移动语义基于右值引用。右值是一个临时对象,它将在表达式的末尾被销毁。在当前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 |
在上述代码中,使用旧编译器时,使用
假设您有一个函数返回一个实体对象:
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,通过复制其内容,然后丢弃临时值而不必破坏它。
如果(像上面的
如果你真的对移动语义有一个好的、深入的解释,我强烈推荐阅读他们的原始论文,"一个提议将移动语义支持添加到C++语言中。"
这是非常容易接近和易于阅读,它是一个很好的理由,他们提供的好处。在wg21网站上还有其他关于移动语义的最新和最新的文章,但是这篇文章可能是最直接的,因为它从一个顶层的角度来处理事情,并且没有太多关于粗糙语言细节的内容。
移动语义是指当不再有人需要源值时,转移资源而不是复制它们。
在C++ 03中,对象经常被复制,只在任何代码再次使用该值之前被销毁或分配。例如,当您从一个函数返回值时,除非rvo打乱了您要返回的值,否则将复制到调用方的堆栈帧,然后它将超出范围并被销毁。这只是许多例子中的一个:当源对象是临时对象时,请参见传递值,例如仅重新排列项的
当这种复制/销毁对很昂贵时,通常是因为对象拥有一些重量级资源。例如,
简单(实际)地说:好的。
复制一个对象意味着复制它的"静态"成员,并为它的动态对象调用
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"是一个可分配的实体(
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; } }; |
在这种情况下,当应该"复制"
必须记住,"静态"对象总是被复制的。无法"移动"静态对象(堆栈中的对象而不是堆中的对象)。因此,当对象没有动态成员(直接或间接)时,"移动"/"复制"的区别是不相关的。好的。
如果您的对象很复杂,并且析构函数具有其他副作用,例如调用库的函数、调用其他全局函数或其他全局函数,则最好用标志来表示移动:好的。
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 */ } }; |
因此,您的代码较短(不需要为每个动态成员执行
另一个典型的问题是:
什么是完美的转发?重要的是要知道"右值引用"是对"调用者范围"中命名对象的引用。但在实际范围中,右值引用是对象的名称,因此它充当命名对象。如果您将一个右值引用传递给另一个函数,那么您将传递一个命名的对象,因此,对象不会像时间对象一样被接收。好的。
1 2 3 4 | void some_function(A&& a) { other_function(a); } |
对象
1 | other_function(std::move(a)); |
通过这条线,
这是完美的转发吗?不是,但我们很亲近。完美转发只对使用模板有用,其目的是说:如果我需要将对象传递给另一个函数,我需要,如果我接收到一个命名对象,该对象将作为命名对象传递,如果不传递,我希望像传递未命名对象一样传递它:好的。
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&&` |
因此,如果
这类似于复制语义,但不必复制所有的数据就可以从被"移动"的对象中窃取数据。
你知道复制语义是什么意思吧?这意味着您有可复制的类型,对于用户定义的类型,您可以定义它,或者购买显式地编写复制构造函数和分配运算符,或者编译器隐式地生成它们。这可以复制一份。
move semantics基本上是一个用户定义的类型,其构造函数接受一个r值引用(使用&;&;(yes two ampersands)的新引用类型),它是非常量,这称为move构造函数,赋值运算符也是如此。因此,move构造函数做什么呢?它不是从源参数复制内存,而是从源参数"移动"内存到目标。
你想什么时候做?例如,假设您创建了一个临时的std::vector,然后从函数say返回它:
1 | std::vector<foo> get_foos(); |
当函数返回时,您将从复制构造函数中得到开销,如果(并且它将在C++ +0x)STD::vector有一个移动构造函数而不是复制它,它只需设置指针和"移动"动态分配的内存到新实例。这有点像所有权转移语义和std::auto-ptr。
为了说明移动语义的需求,让我们考虑一下这个没有移动语义的示例:
这是一个函数,它接受一个
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); } |
移动数据涉及到将数据与新对象重新关联。而且根本没有复制品。
这是通过一个
来自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构造函数允许程序员重写这个默认行为,而不是通过将返回给调用者的对象指向与本地对象关联的堆数据来"重用"与本地对象关联的堆数据。因此不需要复制。
在不允许创建本地对象(即堆栈上的对象)的语言中,这些类型的问题不会在所有对象都分配到堆上并且总是通过引用访问时发生。