右值引用
什么是lvalue, 什么是rvalue?
lvalue: 具有存储性质的对象,即lvalue对象,是指要实际占用内存空间、有内存地址的那些实体对象,例如:变量(variables)、函数、函数指针等。
rvalue:相比较于lvalue就是所谓的没有存储性质的对象, 也就是临时对象。
也可以这样理解:
lvalue: 通过它能够找到内存中存放的变量(location value),位于赋值运算符左,可以赋值。
rvalue:存放在lvalue对应的内存中的东西(register value), 位于赋值运算符左,不可赋值。
对左值和右值的一个最常见的误解是:等号左边的就是左值,等号右边的就是右值。左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象
例子:
- int a = 10;
- int b = 20;
- int *pFlag = &a;
- vector<int> vctTemp;
- vctTemp.push_back(1);
- string str1 = "hello ";
- string str2 = "world";
- const int &m = 1;
请问,a,b, a+b, a++, ++a, pFlag, *pFlag, vctTemp[0], 100, string("hello"), str1, str1+str2, m分别是左值还是右值?、
- a和b都是持久对象(可以对其取地址),是左值;
- a+b是临时对象(不可以对其取地址),是右值;
- a++是先取出持久对象a的一份拷贝,再使持久对象a的值加1,最后返回那份拷贝,而那份拷贝是临时对象(不可以对其取地址),故其是右值;
- ++a则是使持久对象a的值加1,并返回那个持久对象a本身(可以对其取地址),故其是左值;
- pFlag和*pFlag都是持久对象(可以对其取地址),是左值;
- vctTemp[0]调用了重载的[]操作符,而[]操作符返回的是一个int &,为持久对象(可以对其取地址),是左值;
- 100和string("hello")是临时对象(不可以对其取地址),是右值;
- str1是持久对象(可以对其取地址),是左值;
- str1+str2是调用了+操作符,而+操作符返回的是一个string(不可以对其取地址),故其为右值;
- m是一个常量引用,引用到一个右值,但引用本身是一个持久对象(可以对其取地址),为左值。
- //先来看一个例子:
- //stack 分配4字节,并设置值5.
- int a = 5;
变量a的存储空间是 &a (a的lvalue) , 存储空间&a上的值为 5(a的rvalue) .
再看一个例子:
- int* p = NULL;
- p = new int(5);
这里分配了两个内存, 一个是在stack上(p, 地址为&p), 一个是在heap上(*p, 地址为p)
对于p: lvalue为&p(p的地址), rvalue为&p地址上的值
对于*p: lvalue为p(*p的地址),lvalue为p地址上的值
左值引用
左值引用根据其修饰符的不同,可以分为非常量左值引用和常量左值引用。
- int a = 5;
- int& v1 = a;
- const int& v2 = 5;
非常量左值引用
只能绑定到非常量左值,不能绑定到常量左值、非常量右值和常量右值。
- int a = 5;
- sp; const int b = 5;
- int& v1 = a; // 绑定到非常量左值
- int& v2 = 5; // 常量右值, compile error
- int& v3 = b; // 常量左值, compile error
- int& v4 = a + b; // 非常量右值, compile error, a + b为临时对象
如果允许绑定到常量左值和常量右值,则非常量左值引用可以用于修改常量左值和常量右值,这明显违反了其常量的含义。
如果允许绑定到非常量右值,则会导致非常危险的情况出现,因为非常量右值是一个临时对象,非常量左值引用可能会使用一个已经被销毁了的临时对象。
常量左值引用
可以绑定到所有类型的值,包括非常量左值、常量左值、非常量右值和常量右值。
- int a = 5;
- bsp; const int b = 5;
- const int& v1 = a; // 绑定到非常量左值
- const int& v2 = 5; // 常量右值
- const int& v3 = b; // 常量左值
- const int& v4 = a + b; // 非常量右值, a + b为临时对象
可以看出,使用左值引用时,我们无法区分出绑定的是否是非常量右值的情况(无法区分上面的v2,v4)
C++11 增加一个新的非常数引用(reference)类型,称作右值引用(R-value reference),标记为T &&。右值引用所引用的临时对象可以在该临时对象被初始化之后做修改,这是为了允许 move 语义。
右值引用
右值引用根据其修饰符的不同,也可以分为非常量右值引用和常量右值引用。
非常量右值引用
只能绑定到非常量右值,不能绑定到非常量左值、常量左值和常量右值。
- int a = 5;
- const int b = 5;
- int&& v1 = 5;
- int&& v2 = a; //compile error
- int&& v3 = b; //compile error
- int&& v4 = a + b;
如果允许绑定到非常量左值,则可能会错误地窃取一个持久对象的数据,而这是非常危险的;
如果允许绑定到常量左值和常量右值,则非常量右值引用可以用于修改常量左值和常量右值,这明显违反了其常量的含义。
常量右值引用
可以绑定到非常量右值和常量右值,不能绑定到非常量左值和常量左值
- int a = 5;
- const int b = 5;
- const int&& v1 = 5;
- const int&& v2 = a; //compile error
- const int&& v3 = b; //compile error
- const int&& v4 = a + b;
注: 基于安全考虑,具有名字的声明为右值的参数不会被认定为右值:
- bool is_r_value(int &&) { return true; }
- bool is_r_value(const int &) { return false; }
- void test(int && i)
- {
- bool isRValue = is_r_value(i); // i 為具名變數,即使被宣告成右值也不會被認定是右值。false
- isRValue = is_r_value(std::move<int&>(i)); // 使用 std::move<T>() 取得右值。 true
- }
move语义
那么,为什么要对非常量右值进行区分呢,区分出来了又有什么好处呢?这就牵涉到C++中一个著名的性能问题——拷贝临时对象。考虑下面的代码:
- vector<int> getVector()
- {
- vector<int> v;
- v.push_back(1);
- v.push_back(2);
- <pre name="code" class="cpp"> v.push_back(3);</pre><pre name="code" class="cpp"> v.push_back(4);</pre> return v;<br>}
当使用vector<int> v = getVector()进行初始化时,实际上调用了三次构造函数。
尽管有些编译器可以采用RVO(Return Value Optimization)来进行优化,但优化工作只在某些特定条件下才能进行。可以看到,上面很普通的一个函数调用,由于存在临时对象的拷贝,导致了额外的两次拷贝构造函数和析构函数的开销。当然,我们也可以修改函数的形式为void getVector(vector<int> &v),但这并不一定就是我们需要的形式
另外,考虑下面字符串的连接操作:
- string s1("hello");
- string s = s1 + "a" + "b" + "c" + "d" + "e";
在对s进行初始化时,会产生大量的临时对象,并涉及到大量字符串的拷贝操作,这显然会影响程序的效率和性能。怎么解决这个问题呢?如果我们能确定某个值是一个非常量右值(或者是一个以后不会再使用的左值),则我们在进行临时对象的拷贝时,可以不用拷贝实际的数据,而只是“窃取”指向实际数据的指针(类似于STL中的auto_ptr,会转移所有权)。
在 C++11,一个std::vector的 "move 构造函数" 对某个vector的右值引用可以单纯地从右值复制其内部 C-style 数组的指针到新的 vector,然后留下空的右值。这个操作不需要数组的复制,而且空的临时对象的析构也不会摧毁内存。传回vector临时对象的函数不需要显式地传回std::vector<T>&&。如果vector没有 move 构造函数,那么复制构造函数将被调用,以const std::vector<T> &的正常形式。 如果它确实有 move 构造函数,那么就会调用 move 构造函数,这能够免除大幅的内存配置。
C++ 11带来了move语义,可以有效的提高STL的效率,这篇文章写的非常好,可以参考,这里对原文进行翻译,加入我自己的理解
原文:http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html
先看看这个例子:
- #include <iostream>
- using namespace std;
- vector<int> doubleValues (const vector<int>& v)
- {
- vector<int> new_values( v.size() );
- for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr )
- {
- new_values.push_back( 2 * *itr );
- }
- return new_values;
- }
- int main()
- {
- vector<int> v;
- for ( int i = 0; i < 100; i++ )
- {
- v.push_back( i );
- }
- v = doubleValues( v );
- }
理论上来说,在内存中我们完全可以把临时对象的指针偷过来,而不去拷贝。还有就是我们为什么不能move呢?C++03说,我不知道哪个对象是临时的,哪个不是临时的,但是C++11却可以做到。
这就是右值引用和move语义要做的事情:move语义可以使你在使用临时对象时避免拷贝,并且可以安全的使用临时对象里的资源。
左值和右值
C++中,左值是指可以使用其地址的表达式,左值提供一种(半)永久的内存,比如:
int a;
a = 1; //a是一个左值
又如:
- intx;
- int& getRef ()
- {
- returnx;
- }
getRef() = 4;//getRef()这个表达式也是一个左值,因为其提供的返回值全局变量,是有固定地址的。
- intx;
- intgetVal ()
- {
- returnx;
- }
- getVal();
- string getName ()
- {
- return"Alex";
- }
getName();
string nema = getName();
const
string& name = getName();
// ok
string& name = getName();
// NOT ok
C++11中,会让你可以绑定一个mutable右值引用,也就是说,一个值是否临时的,可以使用右值引用来检测,右值引用使用&&语法,可以是const也可以是非const的:
const
string&& name = getName();
// ok
string&& name = getName();
// also ok - praise be!
- printReference (constString& str)
- {
- cout << str;
- }
- printReference (String&& str)
- {
- cout << str;
- }
- string me( "alex");//mutable rvalue-references类型
- printReference( me ); // calls the first printReference function, taking an lvalue reference
现在,右值引用版本的函数就像一个俱乐部的入口,只有临时变量才可以进入。
现在,既然有方法检测出是否临时变量了,有什么用处呢?
- class ArrayWrapper
- {
- public:
- ArrayWrapper (int n)
- : _p_vals( new int[ n ] )
- , _size( n )
- {}
- // copy constructor
- ArrayWrapper (const ArrayWrapper& other)
- : _p_vals( new int[ other._size ] )
- , _size( other._size )
- {
- for ( int i = 0; i < _size; ++i )
- {
- _p_vals[ i ] = other._p_vals[ i ];
- }
- }
- ~ArrayWrapper ()
- {
- delete [] _p_vals;
- }
- private:
- int *_p_vals;
- int _size;
- };
- class ArrayWrapper
- {
- public:
- // default constructor produces a moderately sized array
- ArrayWrapper ()
- : _p_vals( new int[ 64 ] )
- , _size( 64 )
- {}
- ArrayWrapper (int n)
- : _p_vals( new int[ n ] )
- , _size( n )
- {}
- // move constructor
- ArrayWrapper (ArrayWrapper&& other)
- : _p_vals( other._p_vals )
- , _size( other._size )
- {
- other._p_vals = NULL;
- }
- // copy constructor
- ArrayWrapper (const ArrayWrapper& other)
- : _p_vals( new int[ other._size ] )
- , _size( other._size )
- {
- for ( int i = 0; i < _size; ++i )
- {
- _p_vals[ i ] = other._p_vals[ i ];
- }
- }
- ~ArrayWrapper ()
- {
- delete [] _p_vals;
- }
- private:
- int *_p_vals;
- int _size;
- };
可以看出,move构造和copy构造相互重载了,那么调用的时候如何判断调用哪个构造呢?很简单,上边不是已经有了方法吗,如果是临时对象就会自动调用move构造,如果不是,就会调用copy构造。