同样转载自编程帮
http://c.biancheng.net/view/7863.html
C++11 move()函数:将左值强制转换为右值
通过学习 《C++11移动构造函数》一节我们知道,C++11 标准中借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数。
注意,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。
move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。
基于 move() 函数特殊的功能,其常用于实现移动语义。
move() 函数的用法也很简单,其语法格式如下:
move( arg )
其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。
【例 1】move() 函数的基础应用。
1. #include <iostream>
2. using namespace std;
4. class movedemo{
5. public:
6. movedemo():num(new int(0)){
7. cout<<"construct!"<<endl;
8. }
9. //拷贝构造函数
10. movedemo(const movedemo &d):num(new int(*d.num)){
11. cout<<"copy construct!"<<endl;
12. }
13. //移动构造函数
14. movedemo(movedemo &&d):num(d.num){
15. d.num = NULL;
16. cout<<"move construct!"<<endl;
17. }
18. public: //这里应该是 private,使用 public 是为了更方便说明问题
19. int *num;
20. };
22. int main(){
23. movedemo demo;
24. cout << "demo2:\n";
25. movedemo demo2 = demo;
26. //cout << *demo2.num << endl; //可以执行
27. cout << "demo3:\n";
28. movedemo demo3 = std::move(demo);
29. //此时 demo.num = NULL,因此下面代码会报运行时错误
30. //cout << *demo.num << endl;
31. return 0;
32. }
程序执行结果为:
construct!
demo2:
copy construct!
demo3:
move construct!
通过观察程序的输出结果,以及对比 demo2 和 demo3 初始化操作不难得知,demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数;而通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数。
注意,调用拷贝构造函数,并不影响 demo 对象,但如果调用移动构造函数,由于函数内部会重置 demo.num 指针的指向为 NULL,所以程序中第 30 行代码会导致程序运行时发生错误。
【例 2】灵活使用 move() 函数。
1. #include <iostream>
2. using namespace std;
4. class first {
5. public:
6. first() :num(new int(0)) {
7. cout << "construct!" << endl;
8. }
9. //移动构造函数
10. first(first &&d) :num(d.num) {
11. d.num = NULL;
12. cout << "first move construct!" << endl;
13. }
14. public: //这里应该是 private,使用 public 是为了更方便说明问题
15. int *num;
16. };
18. class second {
19. public:
20. second() :fir() {}
21. //用 first 类的移动构造函数初始化 fir
22. second(second && sec) :fir(move(sec.fir)) {
23. cout << "second move construct" << endl;
24. }
25. public: //这里也应该是 private,使用 public 是为了更方便说明问题
26. first fir;
27. };
29. int main() {
30. second oth;
31. second oth2 = move(oth);
32. //cout << *oth.fir.num << endl; //程序报运行时错误
33. return 0;
34. }
程序执行结果为:
construct!
first move construct!
second move construct
程序中分别构建了 first 和 second 这 2 个类,其中 second 类中包含一个 first 类对象。如果读者仔细观察不难发现,程序中使用了 2 此 move() 函数:
- 程序第 31 行:由于 oth 为左值,如果想调用移动构造函数为 oth2 初始化,需先利用 move() 函数生成一个 oth 的右值版本;
- 程序第 22 行:oth 对象内部还包含一个 first 类对象,对于 oth.fir 来说,其也是一个左值,所以在初始化 oth.fir 时,还需要再调用一次 move() 函数。
这么理解移动构造
实际把浅拷贝的缺点转变成优点,将指针变量的地址转移到新对象中,同时将旧对象的指针值赋空,新对象就将原对象(指针成员变量)所指的空间占为己有
如果成员变量不是指针呢?
基本思路是赋值,原对象的成员的值如何处理?
勘误:并不是简单的赋值处理,成员可能是内置类型也可能是类类型,所以,第一步操作是先将成员强制右值化
second(second && sec) :fir(move(sec.fir)) {} //强制右值,然后用它赋值
那原对象的成员变量如何处理?
原对象成员变量的处理思路是将它std::move(),意思会调用该对象(成员变量)的移动构造函数,而移动构造又会将用来保存内存地址的指针赋空
右值引用参考知乎这篇技术文章比较全面,把它搬运过来,这篇文章值的多次翻看!!
作者:Tinro
链接:https://www.zhihu.com/question/22111546/answer/30801982
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
右值引用是C++11中最重要的新特性之一,它解决了C++中大量的历史遗留问题,使C++标准库的实现在多种场景下消除了不必要的额外开销(如std::vector, std::string),也使得另外一些标准库(如std::unique_ptr, std::function)成为可能。即使你并不直接使用右值引用,也可以通过标准库,间接从这一新特性中受益。为了更好的理解标准库结合右值引用带来的优化,我们有必要了解一下右值引用的重大意义。
右值引用的意义通常解释为两大作用:移动语义和完美转发。本文主要讨论移动语义。
移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在C++11之前,移动语义的缺失是C++饱受诟病的问题之一。
举个栗子,问题:如何将大象从一台冰箱转移到另一台冰箱?
2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?
“移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。
C++通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。
为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。
右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低(那么析构函数也应该先if(nullptr != ptr))。
右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。
右值引用至少可以解决以下场景中的移动语义缺失问题:
- 按值传入参数
按值传参是最符合人类思维的方式。基本的思路是,如果传入参数是为了将资源交给函数接受者,就应该按值传参。同时,按值传参可以兼容任何的cv-qualified左值、右值,是兼容性最好的方式。
class People {
public:
People(string name) // 按值传入字符串,可接收左值、右值。接收左值时为复制,接收右值时为移动
: name_(move(name)) // 显式移动构造,将传入的字符串移入成员变量
{
}
string name_;
};
People a("Alice"); // 移动构造name
string bn = "Bob";
People b(bn); // 拷贝构造name
构造a时,调用了一次字符串的构造函数和一次字符串的移动构造函数。如果使用const string& name接收参数,那么会有一次构造函数和一次拷贝构造,以及一次non-trivial的析构。尽管看起来很蛋疼,尽管编译器还有优化,但从语义来说按值传入参数是最优的方式。
如果你要在构造函数中接收std::shared_ptr<X>并且存入类的成员(这是非常常见的),那么按值传入更是不二选择。拷贝std::shared_ptr<X>需要线程同步,相比之下移动std::shared_ptr是非常轻松愉快的。
- 按值返回
和接收输入参数一样,返回值按值返回也是最符合人类思维的方式。曾经有无数函数为了返回容器而不得不写成这样
void str_split(const string& s, vector<string>* vec);
// 一个按值语义定义的字符串拆分函数。这里不考虑分隔符,假定分隔符是固定的。
这样要求vec在外部被事先构造,此时尚无从得知vec的大小。即使函数内部有办法预测vec的大小,因为函数并不负责构造vec,很可能仍需要resize。
对这样的函数嵌套调用更是痛苦的事情,谁用谁知道啊。有了移动语义,就可以写成这样
vector<string> str_split(const string& s) {
vector<string> v;
// ...
return v; // v是左值,但优先移动,不支持移动时仍可复制。
}
如果函数按值返回,return语句又直接返回了一个栈上的左值对象(输入参数除外)时,标准要求优先调用移动构造函数,如果不符再调用拷贝构造函数。尽管v是左值,仍然会优先采用移动语义,返回vector<string>从此变得云淡风轻。此外,无论移动或是拷贝,可能的情况下仍然适用编译器优化,但语义不受影响。
对于std::unique_ptr来说,这简直就是福音。
unique_ptr<SomeObj> create_obj(/*...*/) {
unique_ptr<SomeObj> ptr(new SomeObj(/*...*/));
ptr->foo(); // 一些可能的初始化
return ptr;
}
当然还有更简单的形式
unique_ptr<SomeObj> create_obj(/*...*/) {
return unique_ptr<SomeObj>(new SomeObj(/*...*/));
}
在工厂类中,这样的语义是非常常见的。返回unique_ptr能够明确对所构造对象的所有权转移,特别的,这样的工厂类返回值可以被忽略而不会造成内存泄露。上面两种形式分别返回栈上的左值和右值,但都适用移动语义(unique_ptr不支持拷贝)。
- 接收右值表达式
没有移动语义时,以表达式的值(例为函数调用)初始化对象或者给对象赋值是这样的:
vector<string> str_split(const string& s);
vector<string> v = str_split("1,2,3");
// 返回的vector用以拷贝构造对象v。为v申请堆内存,复制数据,然后析构临时对象(释放堆内存)。
vector<string> v2;
v2 = str_split("1,2,3");
// 返回的vector被复制给对象v(拷贝赋值操作符)。需要先清理v2中原有数据,将临时对象中的数据复制给v2,然后析构临时对象。
注:v的拷贝构造调用有可能被优化掉,尽管如此在语义上仍然是有一次拷贝操作。同样的代码,在支持移动语义的世界里就变得更美好了。
vector<string> str_split(const string& s);
vector<string> v = str_split("1,2,3");
// 返回的vector用以移动构造对象v。v直接取走临时对象的堆上内存,无需新申请。之后临时对象成为空壳,不再拥有任何资源,析构时也无需释放堆内存。
vector<string> v2;
v2 = str_split("1,2,3");
// 返回的vector被移动给对象v(移动赋值操作符)。先释放v2原有数据,然后直接从返回值中取走数据,然后返回值被析构。
注:v的移动构造调用有可能被优化掉,尽管如此在语义上仍然是有一次移动操作。
不用多说也知道上面的形式是多么常用和自然。而且这里完全没有任何对右值引用的显式使用,性能提升却默默的实现了。
- 对象存入容器
这个问题和前面的构造函数传参是类似的。不同的是这里是按两种引用分别传参。参见std::vector的push_back函数。
void push_back( const T& value ); // (1)
void push_back( T&& value ); // (2)
不用多说自然是左值调用1右值调用2。如果你要往容器内放入超大对象,那么版本2自然是不2选择。
vector<vector<string>> vv;
vector<string> v = {"123", "456"};
v.push_back("789"); // 临时构造的string类型右值被移动进容器v
vv.push_back(move(v)); //显式将v移动进vv
困扰多年的难言之隐是不是一洗了之了?
- std::vector的增长
又一个隐蔽的优化。当vector的存储容量需要增长时,通常会重新申请一块内存,并把原来的内容一个个复制过去并删除。对,复制并删除,改用移动就够了.
对于像vector<string>这样的容器,如果频繁插入造成存储容量不可避免的增长时,移动语义可以带来悄无声息而且美好的优化。 - std::unique_ptr放入容器
曾经,由于vector增长时会复制对象,像std::unique_ptr这样不可复制的对象是无法放入容器的。但实际上vector并不复制对象,而只是“移动”对象。所以随着移动语义的引入,std::unique_ptr放入std::vector成为理所当然的事情。
容器中存储std::unique_ptr有太多好处。想必每个人都写过这样的代码:
MyObj::MyObj() {
for (...) {
vec.push_back(new T());
}
// ...
}
MyObj::~MyObj() {
for (vector<T*>::iterator iter = vec.begin(); iter != vec.end(); ++iter) {
if (*iter) delete *iter;
}
// ...
}
繁琐暂且不说,异常安全也是大问题。使用vector<unique_ptr<T>>,完全无需显式析构,unqiue_ptr自会打理一切。完全不用写析构函数的感觉,你造吗?
unique_ptr是非常轻量的封装,存储空间等价于裸指针,但安全性强了一个世纪。实际中需要共享所有权的对象(指针)是比较少的,但需要转移所有权是非常常见的情况。auto_ptr的失败就在于其转移所有权的繁琐操作。unique_ptr配合移动语义即可轻松解决所有权传递的问题。
注:如果真的需要共享所有权,那么基于引用计数的shared_ptr是一个好的选择.shared_ptr同样可以移动。由于不需要线程同步,移动shared_ptr比复制更轻量.
- std::thread的传递
thread也是一种典型的不可复制的资源,但可以通过移动来传递所有权。同样std::future std::promise std::packaged_task等等这一票多线程类都是不可复制的,也都可以用移动的方式传递。
完美转发
======
除了移动语义,右值引用还解决了C++03中引用语法无法转发右值的问题,实现了完美转发,才使得std::function能有一个优雅的实现。这部分不再展开了。
总结
======
移动语义绝不是语法糖,而是带来了C++的深刻革新。移动语义不仅仅是针对库作者的,任何一个程序员都有必要去了解它。尽管你可能不会去主动为自己的类实现移动语义,但却时时刻刻都在享受移动语义带来的受益。因此这绝不意味着这是一个可有可无的东西。
注意,测试结果
A&& a1(std::move(A(10)));
//引用的是个将亡右值,std::move()外面的小括号结束后栈中临时对象消亡,a1实际引用的就是那个已消亡的对象
//a1中的数据成员是啥样取决于~A(){}是如何处理数据成员的
A a1(std::move(A(10))); //error
//A(10)生成临时对象调用构造函数,因为已经是右值,因此std::move(A(10))强制转右值无意义
//接着,一个右值对象(临时对象)初始化另一个对象会优先调用移动构造函数,其次才是拷贝构造
A a1(std::move(a));
//左源值对象a仍存续,测试时std::move(a)并没有调用析构函数,左源值被std::move()后仅是值属性受影响,变为右值属性
//而A a1(右值)才会对左源值产生影响,因为会调用移动构造函数,将左源值的数据成员另外赋值(常见就是将指针成员赋nullptr)
得出结论:
- 1.std::move()输入一定要是左值才有意义
- 2.std::move()会对输入的左源值产生影响,具体就是移动构造函数中对左源值的数据成员的重新赋值.所以析构函数中析构对象时要对数据成员判断,判断其是否已被移动走
引用CppPrimer原话:
std::move()移后源仍处于有效,可析构的状态,但是其数据成员已被移动
使用std::move()等于承诺放弃使用之前的左源值
A&& a1(std::move(a)); //不会调用移动构造
A& a1(a);
//上两者等价
A a1(std::move(a));
A a1 = std::move(a);
//以上两者等价,会调用移动构造函数
总之一句话,std::move()唯一作用就是将值属性强制右值话,为能调用移动构造函数作前期准备