C++ 11 右值引用和移动语义的实现

什么是左值,什么是右值?
左值就是程序能获得其地址的表示数据的表达式,包括变量,const常量,解除引用的指针。
相反,右值就是不能应用地址运算符&的表示数据的表达式,包括字面常量,x+y,非引用的返回值。

什么是左值引用,什么是右值引用?
【C++ 11 右值引用和移动语义的实现】我们常说的C++的引用,大部分时候指的就是左值引用,符号是&,
比如 int a=10; int &b=a; 其中,b就是a的引用,可以理解为别名。
右值引用符号是&&,
比如 int &&a = 10; 其中,a就是10的右值引用。&10是非法的,但是&a却是合法的。

移动语义和右值引用的关系
移动语义对降低C++构造和析构的开销有重要的意义,减少了传值、返回值过程中的资源拷贝。
C++移动语义的实现,正是基于右值引用。

传值和返回值的代码开销在哪里?
请看下面这段代码;

#include using std::cin; using std::cout; using std::endl; struct Person { Person(const char* p) { cout << "constructor" << endl; } Person(const Person& p) { cout << "copy constructor" << endl; } const Person& operator=(const Person& p) { cout << "operator=" << endl; return *this; } ~Person() { cout << "destructor" << endl; } }; Person getAlice() { Person p("alice"); // 对象创建。调用构造函数,一次 new 操作 return p; // 返回值创建。调用拷贝构造函数,一次 new 操作 // p 析构。一次 delete 操作 }int main() { cout << "______________________" << endl; Person a = getAlice(); // 对象创建。调用拷贝构造函数,一次 new 操作 // 返回值析构,一次 delete 操作 // 当前步骤合共 3次构造,2次析构 cout << "______________________" << endl; a = getAlice(); // 对象创建。调用拷贝构造函数,一次 new 操作 // 返回值析构,一次 delete 操作 // 当前步骤合共 3次构造,2次析构 cout << "______________________" << endl; return 0; // a 析构。一次 delete 操作 }

在不考虑NVRO(返回值优化)的情况下,上面这段代码的预期过程如注释,总共6次构造,5次析构。
当然了,编译器会进行NVRO(返回值优化),减少构造和析构次数。
不同编译器的NVRO结果是不一样的:
在Visual Studio 2015上面编译运行结果是:
C++ 11 右值引用和移动语义的实现
文章图片

Person a = getAlice(),这一步,getAlice里面p的析构和返回值的构造被优化掉了,相当于a直接用了getAlice()的对象;
a=getAlice(),这一步,没有NVRO优化。

g++(8.2.0)优化程度比VS高。
C++ 11 右值引用和移动语义的实现
文章图片

Person a = getAlice(),这一步,getAlice() 里面p的析构,返回值的构造和析构,a的拷贝构造都被优化掉了;
a=getAlice(),这一步,NVRO优化程度比赋初值操作的低,
getAlice() 里面p的析构和返回值的构造被优化掉了,相当于a直接用了getAlice里面的对象;

上面的代码还能优化吗?
可以。
通过移动语义,可以把拷贝构造函数改写成移动构造函数;或者就是另外写一个移动构造函数,实现重载。参考[4]
使用std::move相当于显式使用移动语义。std::move()实际上是static_cast()的简单封装。

用右值引用实现移动语义,从而优化拷贝构造函数
参见以下代码和注释
// 基于左值引用的拷贝构造函数 //(参数p设置const属性,不允许直接取用参数p的指针成员,这是为了拷贝构造函数既能接受左值参数,也能接受右值参数) //(不设置const属性也行,但是就不能用右值(getAlice的返回值)进行拷贝构造得到新的对象了。) const Person& operator=(const Person& p) { cout << "operator=" << endl; delete[] name; int len = strlen(p.name) + 1; name = new char[len]; memcpy(name, p.name, len); //左值引用的拷贝构造,会有一次申请内存和数据拷贝 return *this; } // 基于右值引用的拷贝构造函数 //(不需要const了,那么就可以直接取用参数p的指针成员,且可以在取用后将p的指针成员置为nullptr,这样该块内存就不会被析构了) const Person& operator=(Person&& p) { cout << "operator=" << endl; delete[] name; name = p.name; //直接取用 p.name = nullptr; //置空使得系统无法将该块内存析构掉 //相对比左值引用,右值引用的拷贝构造可以实现更加高效:少了一次内存申请和拷贝。 return *this; }


【参考】
[1]《C++ Primer Plus》,18.1.9,右值引用一节
[2] https://harttle.land/2015/10/11/cpp11-rvalue.html 这篇文章讲的比较易于理解
[3] 如何评价 C++11 的右值引用(Rvalue reference)特性? - Tinro的回答 - 知乎 https://www.zhihu.com/question/22111546/answer/30801982
[4] https://www.cnblogs.com/dongdongweiwu/p/4743661.html

    推荐阅读