C++的表达式化分为三种,左值(lvalue),纯右值(prvaule),亡值(xvalue),一般而言将纯右值与亡值合一起称为右值。通常左值,右值是是针对表达式的。
C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。在介绍右值引用类型之前先要了解什么是左值和右值:
通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。
- int a = 520;
- int b = 1314;
- a = b;
一般情况下,位于 = 前的表达式为左值,位于 = 后边的表达式为右值。也就是说例子中的 a, b 为左值,520,1314 为右值。a=b 是一种特殊情况,在这个表达式中 a, b 都是左值,因为变量 b 是可以被取地址的,不能视为右值。
- "Hello Word"在等号右边,字符串字面量,是左值
- const char* pStr = "Hello Word";
C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):
int value = 520;
在上面的语句中,value 是左值,520 是字面量也就是右值。其中 value 可以被引用,但是 520 就不行了,因为字面量都是右值。
注意:
++i是左值,i++是右值。
前者,对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i;而对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果是对i加1前i的一份拷贝,所以它是不具名的。假设自增前i的值是6,那么,++i得到的结果是7,这个7有个名字,就是i;而i++得到的结果是6,这个6是i加1前的一个副本,它没有名字,i不是它的名字,i的值此时也是7。可见,++i和i++都达到了使i加1的目的,但两个表达式的结果不同。
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,(延长生命周期)其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
关于右值引用的使用,参考代码如下:
- #include
- using namespace std;
-
- int&& value = 520;
- class Test
- {
- public:
- Test()
- {
- cout << "construct: my name is jerry" << endl;
- }
- Test(const Test& a)
- {
- cout << "copy construct: my name is tom" << endl;
- }
- };
-
- Test getObj()
- {
- return Test();
- }
-
- int main()
- {
- int a1;
- int &&a2 = a1; // error
- Test& t = getObj(); // error
- Test && t = getObj();
- const Test& t = getObj();
- return 0;
- }
在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
再来修改一下上面的实例代码:
- #include
- using namespace std;
-
- class Test
- {
- public:
- Test() : m_num(new int(100))
- {
- cout << "construct: my name is jerry" << endl;
- }
-
- Test(const Test& a) : m_num(new int(*a.m_num))
- {
- cout << "copy construct: my name is tom" << endl;
- }
-
- ~Test()
- {
- delete m_num;
- }
-
- int* m_num;
- };
-
- Test getObj()
- {
- Test t;
- return t;
- }
-
- int main()
- {
- Test t = getObj();
- cout << "t.m_num: " << *t.m_num << endl;
- return 0;
- };
测试代码执行的结果为:
- construct: my name is jerry
- copy construct: my name is tom
- t.m_num: 100
通过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 t,在 getObj() 函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高 C++ 应用程序的性能。
- #include
- using namespace std;
-
- class Test
- {
- public:
- Test() : m_num(new int(100))
- {
- cout << "construct: my name is jerry" << endl;
- }
-
- Test(const Test& a) : m_num(new int(*a.m_num))
- {
- cout << "copy construct: my name is tom" << endl;
- }
-
- // 添加移动构造函数
- Test(Test&& a) : m_num(a.m_num)
- {
- a.m_num = nullptr;
- cout << "move construct: my name is sunny" << endl;
- }
-
- ~Test()
- {
- delete m_num;
- cout << "destruct Test class ..." << endl;
- }
-
- int* m_num;
- };
-
- Test getObj()
- {
- Test t;
- return t;
- }
-
- int main()
- {
- Test t = getObj();
- cout << "t.m_num: " << *t.m_num << endl;
- return 0;
- };
测试代码执行的结果如下:
- construct: my name is jerry
- move construct: my name is sunny
- destruct Test class ...
- t.m_num: 100
- destruct Test class ...
通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。
如果不使用移动构造,在执行 Test t = getObj() 的时候也是进行了浅拷贝,但是当临时对象被析构的时候,类成员指针 int* m_num; 指向的内存也就被析构了,对象 t 也就无法访问这块内存地址了。
在测试程序中 getObj() 的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果 = 右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。
对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。
在 C++ 中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为 T&&,如果是自动类型推导需要指定为 auto &&,在这两种场景下 && 被称作未定的引用类型。另外还有一点需要额外注意 const T&& 表示一个右值引用,不是未定引用类型。
先来看第一个例子,在函数模板中使用 &&:
- template<typename T>
- void f(T&& param);
- void f1(const T&& param);
- f(10);
- int x = 10;
- f(x);
- f1(x);
在上面的例子中函数模板进行了自动类型推导,需要通过传入的实参来确定参数 param 的实际类型。
再来看第二个例子:
- int main()
- {
- int x = 520, y = 1314;
- auto&& v1 = x;
- auto&& v2 = 250;
- decltype(x)&& v3 = y; // error
- cout << "v1: " << v1 << ", v2: " << v2 << endl;
- return 0;
- };
由于上述代码中存在 T&& 或者 auto&& 这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在 C++11 中引用折叠的规则如下:
- 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
- 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型
- int&& a1 = 5;
- auto&& bb = a1;
- auto&& bb1 = 5;
-
- int a2 = 5;
- int &a3 = a2;
- auto&& cc = a3;
- auto&& cc1 = a2;
-
- const int& s1 = 100;
- const int&& s2 = 100;
- auto&& dd = s1;
- auto&& ee = s2;
-
- const auto&& x = 5;
-
再看最后一个例子,代码如下:
- #include
- using namespace std;
-
- void printValue(int &i)
- {
- cout << "l-value: " << i << endl;
- }
-
- void printValue(int &&i)
- {
- cout << "r-value: " << i << endl;
- }
-
- void forward(int &&k)
- {
- printValue(k);
- }
-
- int main()
- {
- int i = 520;
- printValue(i);
- printValue(1314);
- forward(250);
-
- return 0;
- };
测试代码输出的结果如下:
- l-value: 520
- r-value: 1314
- l-value: 250
根据测试代码可以得知,编译器会根据传入的参数的类型(左值还是右值)调用对应的重置函数(printValue),函数 forward () 接收的是一个右值,但是在这个函数中调用函数 printValue () 时,参数 k 变成了一个命名对象,编译器会将其当做左值来处理。
最后总结一下关于 && 的使用: