右值引用
右值引用最简单的作用:可以避免无谓的复制,提高了程序性能(在移动构造函数中有体现)。
什么是右值
最基本的解释:
左值可以取地址、位于等号左边;
右值没法取地址,位于等号右边。(或者函数的返回值等)
例如:
struct A { A(int a = 0) { a_ = a; } int a_; }; A a = A();
- 其中a可以通过 & 取地址,位于等号左边,所以a是左值。
- A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。
左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。
左值引用和右值引用
引用:引用本质是别名,可以通过引用来修改变量的值,传参时传引用可以避免拷贝。
左值引用
左值引用:能指向左值,不能指向右值的就是左值引用
int a = 5; int &ref_a = a; // 左值引用指向左值,编译通过 int &ref_a = 5; // 左值引用指向了右值,会编译失败
代码中第三行,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但是有特例const。
const左值引用
const int &ref_a = 5; // 编译通过
const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const & 作为函数参数的原因之一,这可以按照固定搭配记住。例如:
void push_back (const value_type& val); ... vec.push_back(5);
如果函数参数没有const , vec.push_back(5) 这样的代码就无法编译通过。
右值引用
右值引用:右值引用专可以指向右值,不能指向左值
右值引用的标志是&& :
int &&ref_a_right = 5; // ok int a = 5; int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值 ref_a_right = 6; // 右值引用的用途:可以修改右值
std::move函数
右值引用可以使用std::move可以指向左值
#include <iostream> #include <memory> using namespace std; int main() { int a = 5; // a是个左值 int &ref_a_left = a; // 左值引用指向左值 int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向 cout << ref_a_right << endl; // 打印结果:5 cout << a; // 打印结果:还是5 return 0; } //代码编译时使用C++11新特性 //g++ main.cpp -o main -std=c++11
std::move把一个变量a里的内容移动到另一个变量ref_a_right了吗?
不是!在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。
std::move函数:
- std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。
- 其实现等同于一个类型转换:static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx) 不会有性能提升。
右值引用的含义
右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:
int &&ref_a = 5; ref_a = 6; //等同于以下代码: int temp = 5; int &&ref_a = std::move(temp); ref_a = 6; // 此时temp也等于6 //两个变量的地址是相同的;&temp,&ref_a是一样的
左值引用、右值引用的本身
被声明出来的左、右值引用都是左值。
因为被声明出的左右值引用是有地址的,都是左值。如下:
// 函数的形参是个右值引用 void change(int&& right_value) { right_value = 8; } int main() { int a = 5; // a是个左值 int &ref_a_left = a; // ref_a_left是个左值引用 int &&ref_a_right = std::move(a); // ref_a_right是个右值引用 change(a); // 编译不过,a是左值,change参数要求右值 change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值 change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值 change(std::move(a)); // 编译通过 change(std::move(ref_a_right)); // 编译通过 change(std::move(ref_a_left)); // 编译通过 change(5); // 当然可以直接接右值,编译通过 cout << &a << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right; // 打印这三个左值的地址,都是一样的 }
如上代码所示int &&ref_a_right = std::move(a); 是一个右值引用,但是ref_a_right是一个左值。
结论:
从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
右值引用避免深拷贝
深拷贝可以避免重复析构的问题,可参考帖子深拷贝与浅拷贝定义以及案例说明。但是深拷贝也带来了性能的消耗,这里可以通过移动构造函数避免额外的内存消耗。
#include <iostream> using namespace std; class A { public: A() :m_ptr(new int(0)) { cout << "constructor A" << endl; } A(const A& a) :m_ptr(new int(*a.m_ptr)) { cout << "copy constructor A" << endl; } // 移动构造函数,可以浅拷贝 A(A&& a) :m_ptr(a.m_ptr) { a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr cout << "move constructor A" << endl; } ~A(){ cout << "destructor A, m_ptr:" << m_ptr << endl; if(m_ptr) delete m_ptr; } private: int* m_ptr; }; // 为了避免返回值优化,此函数故意这样写 A Get(bool flag) { A a; A b; cout << "ready return" << endl; if (flag) return a; else return b; } int main() { { A a = Get(false); // 正确运行 } cout << "main finish" << endl; return 0; }
运行结果:
建议对比深拷贝与浅拷贝定义以及案例说明中的代码。
- 可以看到A Get(bool flag),返回参数为A对象,函数中我们返回的a,b是临时变量,所以这里自动会使用移动构造函数通过右值引用接收临时变量。
- 上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。
- 从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。
- 这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。
- 移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。
结论:
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高
C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
(move)移动语义
move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持(也就是需要移动构造函数)。
// 用c++11的右值引用来定义这两个函数 //MyString类的移动构造函数 MyString(MyString&& str) { std::cout << "Move Constructor is called! source: " << str.m_data << std::endl; m_len = str.m_len; m_data = str.m_data; //避免了不必要的拷贝 str.m_len = 0; str.m_data = NULL; } ... MyString c = std::move(a); // Move Constructor is called! 将左值转为右值
有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。
forward 完美转发
forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
如下面这个简单的例子:
int &&a = 10; int &&b = a; //错误
这里a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。
这里就可以使用std::forward()完美转发,就会按照参数原来的类型转发;
int &&a = 10; int &&b = std::forward<int>(a);
这样就是正确的,这里有点像move移动语义的意思,forward是有把左值转为右值的功能,但尽量要以完美转发的角度去理解forward。
上面的是一个简单的完美转发的例子,但完美转发最常见还是处理在模板类中会存在一种“属性变换”现象。
#include <iostream> using namespace std; template <class T> void Print(T &t) { cout << "L" << t << endl; } template <class T> void Print(T &&t) { cout << "R" << t << endl; } template <class T> void func(T &&t) { Print(t); Print(std::move(t)); Print(std::forward<T>(t)); } int main() { cout << "-- func(1)" << endl; func(1); int x = 10; int y = 20; cout << "-- func(x)" << endl; func(x); // x本身是左值 cout << "-- func(std::forward<int>(y))" << endl; func(std::forward<int>(y)); //这里将输入参数转为右值传入了 return 0; }
运行结果:
解释一下func(1)结果 :
- 由于1是右值,所以未定的引用类型T&&v被一个右值初始化后变成了一个右值引用,但是在func()函数体内部,调用Print(v) 时,v又变成了一个左值(因为在std::forward里它已经变成了一个具名的变量,所以它是一个左值),因此,示例测试结果第一个Print被调用,打印出“L1";
- 调用Print(std::move(v))是将v变成一个右值(v本身也是右值),因此输出”R1";
- 调用Print(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型,会调用void Print(T&&t)函数打印“R1”.