什么是移动语义?
我刚听完关于C ++ 0x的关于Scott Meyers的软件工程无线电播客采访 。 大部分新function对我来说都是有意义的,现在我对C ++ 0x兴奋不已,除了一个。 我仍然没有得到移动语义 …他们究竟是什么?
我发现使用示例代码理解移动语义是最容易的。 让我们从一个非常简单的string类开始,它只保存一个指向堆分配的内存块的指针:
#include <cstring> #include <algorithm> class string { char* data; public: string(const char* p) { size_t size = strlen(p) + 1; data = new char[size]; memcpy(data, p, size); }
既然我们自己selectpipe理记忆,就要遵循三条规则 。 我将推迟写作业操作符,现在只执行析构函数和复制构造函数:
~string() { delete[] data; } string(const string& that) { size_t size = strlen(that.data) + 1; data = new char[size]; memcpy(data, that.data, size); }
复制构造函数定义了复制string对象的含义。 参数const string& that
绑定到stringtypes的所有expression式,允许您在以下示例中创build副本:
string a(x); // Line 1 string b(x + y); // Line 2 string c(some_function_returning_a_string()); // Line 3
现在来移动语义的关键洞察力。 请注意,只有在我们复制的第一行中x
是真正必要的深度副本,因为我们可能稍后要检查x
并且如果x
以某种方式改变,将会感到非常惊讶。 你有没有注意到我刚刚说了三次(如果包含这个句子,是四次)并且每次都表示完全一样的对象 ? 我们称之为x
“左值”等expression式。
第2行和第3行的参数不是左值,而是右值,因为底层的string对象没有名字,所以客户端在以后的时间点再也不能检查它们了。 右值表示在下一个分号处被破坏的临时对象(更确切地说:在词汇上包含右值的全expression式的末尾)。 这一点很重要,因为在b
和c
的初始化过程中,我们可以对源string做任何我们想做的事情,而客户端无法区别 !
C ++ 0x引入了一个叫做“右值引用”的新机制,它允许我们通过函数重载来检测右值参数。 我们所要做的就是编写一个带有右值引用参数的构造函数。 在构造函数里面,只要我们把它放在一个有效的状态,我们就可以做任何我们想要的东西 :
string(string&& that) // string&& is an rvalue reference to a string { data = that.data; that.data = nullptr; }
我们在这里做了什么? 我们不是复制堆数据,而是复制指针,然后将原始指针设置为null。 实际上,我们“偷走了”原来属于源string的数据。 再一次,关键的洞察是,客户在任何情况下都不能检测到源已被修改。 由于我们在这里没有真正做一个副本,所以我们称这个构造函数为“移动构造函数”。 它的工作是将资源从一个对象移到另一个对象而不是复制它们。
恭喜,您现在了解移动语义的基础知识! 让我们继续执行赋值操作符。 如果你对复制和交换习惯不熟悉,学习它并回来,因为这是一个非常好的与exception安全相关的C ++习惯用法。
string& operator=(string that) { std::swap(data, that.data); return *this; } };
呃,就是这样? “右值参考在哪里?” 你可能会问。 “我们这里不需要它!” 是我的回答:)
请注意,我们通过值传递参数,所以必须像其他任何string对象一样被初始化。 到底如何进行初始化? 在C ++ 98的古代日子里,答案将是“通过复制构造函数”。 在C ++ 0x中,编译器根据赋值运算符的参数是左值还是右值,在复制构造函数和移动构造函数之间进行select。
所以如果你说a = b
, 拷贝构造函数会初始化that
(因为expression式b
是一个左值),赋值操作符用新创build的深拷贝交换内容。 这就是复制和交换成语的定义 – 制作副本,将副本交换内容,然后通过离开范围摆脱副本。 这里没有新东西。
但是如果你说a = x + y
, 移动构造函数将初始化that
(因为expression式x + y
是一个右值),所以不涉及深度拷贝,只有有效的移动。 that
论点仍然是一个独立的对象,但是它的构build是微不足道的,因为堆数据不需要被复制,只是移动。 没有必要复制它,因为x + y
是一个右值,同样,从rval表示的string对象移动也是可以的。
总而言之,拷贝构造函数进行深度拷贝,因为源码必须保持不变。 另一方面,移动构造函数只能复制指针,然后将源中的指针设置为null。 以这种方式“消除”源对象是可以的,因为客户端没有办法再次检查对象。
我希望这个例子得到了主要的观点。 右值引用和移动语义有很多,我故意省略它来保持简单。 如果你想了解更多的细节,请看我的补充答案 。
我的第一个回答是移动语义的一个非常简单的介绍,许多细节被保留下来以保持简单。 然而,移动语义还有很多,我认为是时候第二个答案来填补空白。 第一个答案已经很老了,简单地用一个完全不同的文字replace它是不对的。 我认为它作为第一个介绍仍然很好。 但如果你想深入挖掘,请阅读:)
Stephan T. Lavavej花时间提供了宝贵的意见。 谢谢,斯蒂芬!
介绍
移动语义允许一个对象在某些条件下获得一些其他对象的外部资源的所有权。 这在两个方面是重要的:
-
把昂贵的副本变成廉价的移动。 看到我的第一个答案为例。 请注意,如果对象不pipe理至less一个外部资源(直接或通过其成员对象间接pipe理),移动语义将不会提供超过复制语义的任何优势。 在这种情况下,复制对象和移动对象意味着完全相同的事情:
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 // ... };
-
实施安全的“仅移动”types; 也就是说,复制没有意义的types,但移动。 示例包括锁,文件句柄和具有唯一所有权语义的智能指针。 注意:这个答案讨论了
std::auto_ptr
,一个被废弃的C ++ 98标准库模板,在C ++ 11中被replace为std::unique_ptr
。 中级C ++程序员可能至less对std::auto_ptr
有些熟悉,而且由于显示了“移动语义”,这似乎是讨论C ++ 11中移动语义的一个很好的起点。 因人而异。
什么是移动?
C ++ 98标准库提供了一个具有唯一所有权语义的智能指针,称为std::auto_ptr<T>
。 如果你不熟悉auto_ptr
,它的目的是保证一个dynamic分配的对象总是被释放,即使是在例外的情况下:
{ 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
不复制三angular形,而是将三angular形的所有权从a
转移到b
。 我们也说“ a
被移到 b
”或者“三angular从a
移到 b
”。 这可能听起来很混乱,因为三angular形本身总是停留在内存中的相同位置。
移动对象意味着将其pipe理的某个资源的所有权转移给另一个对象。
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();
然而,其中一个调用未定义的行为,而另一个则不行。 那么expression式a
和make_triangle()
之间有什么区别呢? 他们不是同一types的吗? 事实上他们是,但他们有不同的价值类别 。
价值类别
显然,表示一个auto_ptr
variables的expression式a
和表示一个函数调用的函数make_triangle()
之间必定存在某种深刻的差别,该函数按值返回一个auto_ptr
,从而在每次被调用时创build一个新的临时的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
请注意,字母l
和r
在作业的左侧和右侧具有历史渊源。 这在C ++中不再是正确的,因为左值不能出现在赋值的左边(比如数组或者没有赋值操作符的用户自定义types),并且有右值(types的所有右值与一个赋值运算符)。
类types的右值是一个评估expression式创build一个临时对象的expression式。 一般情况下,同一范围内的其他expression式不能表示同一个临时对象。
右值引用
我们现在明白从左值移动是有潜在危险的,但是从右值移动是无害的。 如果C ++有语言支持来区别左值参数和右值参数,那么我们可以完全禁止左值移位,或者至less使得左值在显式调用位置移动,这样我们就不会意外移动了。
C ++ 11的这个问题的答案是右值引用 。 右值引用是一种新的引用,只绑定到右值,语法是X&&
。 好的旧参考X&
现在被称为左值参考 。 (请注意, X&&
不是对引用的引用,在C ++中没有这种东西。)
如果我们把const
放入混合中,我们已经有了四种不同的引用。 X
可以绑定什么types的expression式?
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&&
也绑定到不同typesY
所有值类别,前提是存在从Y
到X
的隐式转换。 在这种情况下,创build一个typesX
的临时值,并将右值引用绑定到该临时值:
void some_function(std::string&& r); some_function("hello world");
在上面的例子中, "hello world"
是一个types为const char[12]
的右值。 由于存在从const char[12]
到const char*
到std::string
的隐式转换,所以创build了一个types为std::string
的临时表,并将r
绑定到该临时表。 这是rvalues(expression式)和临时对象(objects)之间的区别有点模糊的情况之一。
移动构造函数
带有X&&
参数的函数的一个有用示例是移动构造函数 X::X(X&& source)
。 其目的是将来自源的受pipe资源的所有权转移到当前对象中。
在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
。 再次,这正是我们想要的。
移动构造器将托pipe资源的所有权转移到当前对象中。
移动赋值运算符
最后一个缺失的部分是移动赋值操作符。 它的工作是释放旧资源,并从其论点中获得新的资源:
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; } };
现在这个source
是一个unique_ptr
types的variables,它将被移动构造函数初始化; 也就是说,参数将被移入参数中。 该参数仍然需要是一个右值,因为移动构造函数本身有一个右值引用参数。 当控制stream程到达operator=
大括号时, source
超出范围,自动释放旧的资源。
移动赋值操作符将托pipe资源的所有权转移到当前对象中,释放旧资源。 移动交换习语简化了实现。
从左值移动
有时候,我们想从左值移动。 也就是说,有时我们希望编译器将左值视为右值,因此它可以调用移动构造函数,尽pipe它可能是不安全的。 为此,C ++ 11在头文件<utility>
提供了一个名为std::move
的标准库函数模板。 这个名字有点不幸,因为std::move
只是将左值转换为右值; 它本身不会移动任何东西。 它只是使移动。 也许它应该被命名为std::cast_to_rvalue
或者std::enable_move
,但是我们现在被这个名字卡住了。
以下是你如何从一个左值显式移动:
unique_ptr<Shape> a(new Triangle); unique_ptr<Shape> b(a); // still an error unique_ptr<Shape> c(std::move(a)); // okay
请注意,在第三行之后,不再拥有一个三angular形。 没关系,因为通过明确地写std::move(a)
,我们明确地expression了我们的意图:“亲爱的构造函数,为了初始化c
做任何你想要的;我不再关心a
了。用你的方式。“
std::move(some_lvalue)
将左值转换为右值,从而启用后续的移动。
Xvalues
请注意,即使std::move(a)
是一个右值,它的求值也不会创build一个临时对象。 这个难题迫使委员会引入第三个价值类别。 可以被绑定到右值引用的东西,即使它不是传统意义上的右值,被称为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”的精神取代所有的“右值”。
走出function
到目前为止,我们已经看到移动到局部variables和function参数。 但移动也可能在相反的方向。 如果一个函数按值返回,那么在调用站点(可能是一个局部variables或一个临时的,但可以是任何types的对象)的某个对象被作为移动构造函数的参数的return
语句之后的expression式初始化:
unique_ptr<Shape> make_triangle() { return unique_ptr<Shape>(new Triangle); } \-----------------------------/ | | temporary is moved into c | v unique_ptr<Shape> c(make_triangle());
也许令人惊讶的是,自动对象(未声明为static
局部variables)也可以隐式地移出函数:
unique_ptr<Shape> make_square() { unique_ptr<Shape> result(new Square); return result; // note the missing std::move }
移动构造函数如何接受左值result
作为参数? result
的范围即将结束,并且在堆栈展开期间将被破坏。 事后没有人可能会抱怨result
已经改变了, 当控制stream返回到调用者时, result
不再存在! 出于这个原因,C ++ 11有一个特殊的规则,允许从函数返回自动对象,而不必写std::move
。 事实上,你绝对不应该使用std::move
将自动对象移出函数,因为这会禁止“命名返回值优化”(NRVO)。
切勿使用
std::move
将自动对象移出函数。
请注意,在两个工厂函数中,返回types是一个值,而不是右值引用。 右值引用仍然是引用,并且一如既往,您不应该返回对自动对象的引用; 如果你欺骗编译器接受你的代码,调用者最终会得到一个悬而未决的引用:
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
是一个左值。 如果你看它的types,你会看到一个右值引用,但右值引用只是意味着“一个绑定到右值的引用”。 这并不意味着参考本身是一个右值! 事实上, parameter
只是一个名字的普通variables。 你可以在构造函数体内使用parameter
,它总是表示同一个对象。 隐含地从它移走将是危险的,因此语言禁止它。
一个命名的右值引用是一个左值,就像任何其他variables一样。
解决scheme是手动启用移动:
class Foo { unique_ptr<Shape> member; public: Foo(unique_ptr<Shape>&& parameter) : member(std::move(parameter)) // note the std::move {} };
你可能会认为parameter
在member
初始化后不再使用。 为什么没有像返回值一样静静插入std::move
特殊规则? 可能是因为编译器的实现者负担过重。 例如,如果构造函数体在另一个翻译单元中呢? 相比之下,返回值规则只需检查符号表以确定return
关键字之后的标识符是否表示自动对象。
您也可以通过值传递parameter
。 对于像unique_ptr
这样的移动types,似乎还没有成熟的习惯用法呢。 就我个人而言,我更喜欢按价值传递,因为它在界面上造成更less的混乱。
特殊会员function
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
如果没有手动声明特殊成员函数,这两个新的特殊成员函数只能被隐式声明。 此外,如果您声明自己的移动构造函数或移动赋值运算符,则复制构造函数和复制赋值运算符都不会隐式声明。
这些规则在实践中意味着什么?
如果你写一个没有非托pipe资源的类,就不需要自己声明任何五个特殊的成员函数,你将得到正确的复制语义并且免费移动语义。 否则,你将不得不自己实现特殊的成员函数。 当然,如果你的类没有从移动语义中获益,就不需要执行特殊的移动操作。
请注意,可以将复制赋值运算符和移动赋值运算符合并为一个统一的赋值运算符,并按值赋值:
X& X::operator=(X source) // unified assignment operator { swap(source); // see my first answer for an explanation return *this; }
这样一来,实现特殊成员函数的数量从五个减less到四个。 在这里exception安全和效率之间有一个权衡,但我不是这个问题的专家。
转发引用( 以前称为通用引用 )
考虑下面的函数模板:
template<typename T> void foo(T&&);
你可能会希望T&&
只绑定到右值,因为乍一看,它看起来像一个右值引用。 事实certificate, 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>&
如果参数是typesX
的右值,则T
被推断为X
,因此T&&
意味着X&&
。 这是任何人都可以期待的。 但是,如果参数是X
types的左值,由于特殊的规则, T
被推断为X&
,因此T&&
意味着类似X& &&
东西。 但是由于C ++仍然没有对引用的引用,所以X& &&
types被折叠为X&
。 这可能听起来令人困惑和无用,但参考折叠对于完美转发来说是必不可less的(这里不讨论)。
T &&不是一个右值引用,而是一个转发引用。 它也绑定到左值,在这种情况下
T
和T&&
都是左值引用。
如果你想限制一个函数模板为右值,你可以将SFINAE和types特征结合起来:
#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
接受任何types的参数感谢转发引用T&&
,它返回一个右值引用。 std::remove_reference<T>::type
元函数调用是必须的,因为否则,对于X
types的左值,返回types将是X& &&
,这会崩溃到X&
。 由于t
总是一个左值(记住一个命名的右值引用是一个左值),但是我们想把t
绑定到右值引用,所以我们必须明确地将t
给正确的返回types。 返回一个右值引用的函数本身就是一个xvalue。 现在你知道xvalues来自哪里;)
返回一个右值引用(如
std::move
的函数的调用是一个xvalue。
请注意,在这个例子中,通过右值引用返回罚款,因为t
不表示一个自动对象,而是一个被调用者传入的对象。
移动语义基于右值引用 。
一个右值是一个临时对象,它将在expression式的末尾被销毁。 在当前的C ++中,右值只能绑定到const
引用。 C ++ 1x将允许非常量右值引用,拼写T&&
,这是引用右值对象。
由于右值将在expression式结尾死亡,因此可以窃取其数据 。 而不是将其复制到另一个对象中,而是将其数据移入其中。
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
在上面的代码中,对于旧的编译器, f()
的结果使用X
的拷贝构造函数复制到x
。 如果你的编译器支持移动语义,并且X
有一个移动构造器,那就调用它。 由于它的论点是一个右值 ,我们知道它不再需要,我们可以窃取它的价值。
所以这个值从f()
返回的未命名的临时值移动到x
(而x
的数据,初始化为一个空的X
,被移动到临时的,这将被分配后被销毁)。
假设你有一个返回实质对象的函数:
Matrix multiply(const Matrix &a, const Matrix &b);
当你写这样的代码:
Matrix r = multiply(a, b);
那么普通的C ++编译器会为multiply()
的结果创build一个临时对象,调用copy构造函数初始化r
,然后破坏临时返回值。 在C ++ 0x中移动语义允许调用“移动构造函数”通过复制其内容来初始化r
,然后丢弃临时值而不必破坏它。
如果(如上面的Matrix
例子)那样,这是特别重要的,被复制的对象在堆上分配额外的内存来存储它的内部表示。 复制构造函数将不得不完整复制内部表示,或者使用引用计数和写时复制语义。 移动构造函数将单独留下堆内存,只需将指针复制到Matrix
对象内。
If you are really interested in a good, in-depth explanation of move semantics, I'd highly recommend reading the original paper on them, "A Proposal to Add Move Semantics Support to the C++ Language."
It's very accessible and easy to read and it makes an excellent case for the benefits that they offer. There are other more recent and up to date papers about move semantics available on the WG21 website , but this one is probably the most straightforward since it approaches things from a top-level view and doesn't get very much into the gritty language details.
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.
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(ai), p(new int(*ap)) {} ~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(ai), p(ap) { ap = 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(ai), p(new int(*ap)) {} // Movement (&& means "rvalue reference to") A(A&& a) : i(ai), p(ap) { ap = 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.
To illustrate the need for move semantics , let's consider this example without move semantics:
Here's a function that takes an object of type T
and returns an object of the same type T
:
T f(T o) { return o; } //^^^ new object constructed
The above function uses call by value which means that when this function is called an object must be constructed to be used by the function.
Because the function also returns by value , another new object is constructed for the return value:
T b = f(a); //^ new object constructed
Two new objects have been constructed, one of which is a temporary object that's only used for the duration of the function.
When the new object is created from the return value, the copy constructor is called to copy the contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.
Now, let's consider what a copy constructor does.
It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much time and memory usage
// 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); }
With move semantics it's now possible to make most of this work less unpleasant by simply moving the data rather than copying.
// 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); }
Moving the data involves re-associating the data with the new object. And no copy takes place at all.
This is accomplished with an rvalue
reference.
An rvalue
reference works pretty much like an lvalue
reference with one important difference:
an rvalue reference can be moved and an lvalue cannot.
From 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&&).
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 languages which do not allow the creation of local objects (that is, objects on the stack) these types of problems do not occur as all objects are allocated on the heap and are always accessed by reference.