C++11最广泛的特性是移动语义,移动语义的基础在于区分左值表达式和右值表达式。一般来说,一个对象是右值意味着可以对其实施移动语义,而左值不可以。右值对应的是函数返回的临时对象,而左值对应的是可指涉的对象,指涉途径有:标识符、指针、左值引用等。
常见的疑问有:
(1)什么是左值和右值?
(2)什么是左值表达式、右值表达式?
(3)引用有哪些?什么又是左值引用和右值引用?
1、一种简明的说法是:
在赋值运算法左边的就是左值,右边的就是右值;
表达式求值结果是左值,就是左值表达式,表达式求值结果是右值,就是右值表达式;
- 这种简明的说法不严谨不准确,以下给出详细说明:
表达式是由运算符或运算对象构成的计算式,字面值(literal)和变量(variable)是最简单的表达式,函数返回值也是表达式,而表达式是可以求值的,对表达式求值将得到一个结果(result),这个结果/值/量有两个自带的属性:型别和值类别。类型大家都很容易理解,int、char、reference...,但是值类别可能容易被忽视,而这最容易被忽视和不知的点:值类型正是C++11后划分对象/值/量的根据和基础。
C++11标准中规定复杂,对于值/表达式的解释大概划分如下:
具体说明如下表:
Expression / value | 表达式 / 值 |
Lvalue | [左值] |
rvalue | [右值] |
glvalue | [广义左值/泛左值] |
xvalue | [将忘值] |
prvalue | 纯右值 |
(1)Prvalue纯右值【纯纯的沙雕】
定义 | 无标识符、不可取地址的表达式/对象,占内存但立即释放 (也叫临时对象) |
纯右值[属于右值] | |
字面值(字串字面值除外) | 1,‘w’,nullptr,true... |
返回值为非引用的函数调用 | str.substr(1, 2), str1 + str2, or it++ |
后置自增自减表达式 | a++, a– |
算术表达式 | a + b |
逻辑表达式 | a && b |
比较表达式 | a > b |
取地址表达式 | &a |
Lambda表达式 | ... |
(2)Lvalue左值【左盟主】
定义 | 有标识符、可取地址的表达式/对象 |
左值[属于广义左值] | |
字串字面值 | “hello world” ... |
变量、函数、数据成员名字 | int a, fun(), str.m_name |
前置自增自减表达式 | ++a, --a |
返回左值引用的表达式 | x = 1、cout << ’ ’,... |
注意:
在函数调用时,左值可以绑定到左值引用的参数,如 T&。
一个常量只能绑定到常左值引用,如 const T&。
左值和右值都是针对表达式而言的(也就是说二者不存在严格的区别):
左值是指表达式结束后依然存在的持久对象
右值是指表达式结束时就不再存在的临时对象
C/C++规定:对于对象的引用必须是左值(常量引用除外)
const引用能够绑定到临时对象,
并将临时对象的生命周期由”创建临时对象的完整表达式”提升
至”绑定到的const引用超出作用域” non-const 引用没有这个功能。
示例:
const int& a = 101; //对{常引用可以作用于右值}
int& b = 101; //错{左值引用不可引用右值}
int a;
int &b = a; //对{左值引用可引用左值}
a = 10;
printf(“b = %d\n”,b);
此时b = 10,b是a的引用,就是说b和a的地址相同,对a做改变b也跟着变化。
- Xvalue将亡值【右值引用引出】
定义 | 通过右值引用符号“&&” , 返回右值引用的函数调用表达式, 转换为右值引用的转换函数的调用表达 |
将亡值[右值引用/广义左值] | |
T&& 函数返回值 | T&& fun(); |
移动语义返回值 | std::move、tsatic_cast<X&&>(x) |
具有右值引用型别的左值 |
在C++11中,用左值去初始化对象或为其赋值,会调用拷贝构造函数或拷贝赋值运算符函数来拷贝资源,用右值(纯右值和将亡值)初始化和赋值时,调用移动构造函数或移动赋值运算符来移动资源,这样会避免拷贝,提高性能效率,右值完成移动后会马上销毁(析构)。
这样的右值存在的使命就是移动后走向死亡,所以叫做“将亡值”。
- 五种值类型辨析
辨析内容 | 左值 | 右值 |
自增自减(看符号) | ++i | i-- |
解引用和取地址(解左取右) | *p | &a |
常见表达式 | x = 1、cout << ’ ’ | a+b、a&&b、a==b |
字面值(字串为左值) | “ddd” | 1、3.14、‘r’ |
具名和不具名 | 具名的右值引用 | 不具名的右值引用 |
void foo(int&& t) t |
(5)值类别与型别的辨析
值类别 | 型别 |
左值、纯右值、将亡值 | 内建类型、基本类型(**指针、**引用...)、自定义类型 |
表达式的型别与他是左值和右值没有关系[一个是型别,一个是值类别],很多的移动构造函数内部对入参取地址完全没问题,尽管该参数的型别属于右值引用。
基于此,我们可以得知,任何形参都是左值。
当一个对象被用作右值的时候,用的是对象的值(内容);
当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
大概的意思就是说左值就有内存地址的,存活的生命周期较长的,
而右值一般是无法获取到内存地址的,生命周期是短暂的。还是以以上的代码为例子,
- 引用型别可分为四类
(1)左值引用:即绑定到左值的引用,必须绑定左值
int i = 1;
int &ref = i; //这里ref是绑定了i这个左值的引用
int &ref2 = 3; // 3 是右值,无法绑定
(2)右值引用
右值引用的作用是给开发者提供一个想要可以绑定临时变量的作用,可以通过右值引用符号“&&” 来实现
int &&i=1;//右值引用可直接引用右值,想要给 “ 1 ”一个固定地址,并给予一个变量名i
i = 3;//可以通过i再赋值
右值引用b延长了函数getX返回值的生命周期。延长临时对象生命周期并不是这里右值引用的最终目标,其真实目标应该是减少对象复制,提升运行性能(关闭RVO优化)。
右值---移动语义
移动构造
对于拷贝构造函数而言形参是一个左值引用,而不能是某些函数返回的临时对象,而且在拷贝构造函数中往往进行的是深复制,即一般不会破坏实参对象。而移动构造函数恰恰相反,它接受的是一个右值,其核心思想是通过转移实参对象的数据以达成构造目标对象的目的,也就是移动构造函数会修改实参对象,一般来说调用了移动构造函数之后,实参对象的相关变量资源就会被转移,原本实参的变量就会被置空,也就是实参就不能再使用了, 因此与其叫做移动构造函数不如叫做窃取构造函数更加的贴切。
那么在什么情况会发生移动构造的调用呢?比如在C++11的STL容器中,会根据具体情况自动调用移动构造函数,比如以下例子:
注意:和拷贝构造函数对于拷贝赋值运算符一样,移动构造函数也对于这一个移动赋值运算符,因为在移动语义中一般会置空实参的相关变量,所以需要注意在移动赋值运算符避免自己赋值给自己的情况,这样会给自己赋值的同时置空自己,在做无用功。
(3)Const常量引用,本质上也是左值引用的一种,但区别有二
一是无法通过这个引用改变引用地址的值【只读】,
二是它可以间接绑定右值(实际上是绑定了一种左值)【绑定右值】
int i = 1;
const int &ref = 1;
ref = 10; //错误,无法再赋值 ,因为ref是一个常量引用
const int &ref2 = 100;//可引用一个右值,本质上是100转换为一个变量,
// 再将ref2引用到这个变量上
总结:
左值引用只能引用左值,右值引用只能引用右值,
而const引用就可以同时引用左值和右值
(通过临时变量间接引用右值,实际上是左值引用)
(4)万能引用
令人眼花的是这个将亡值,它既可以代表一个左值,又可以代表一个右值,这是怎么的一回事呢?所谓的万能引用就是既可以引用左值,也可以引用右值的引用,
一般是在模板中的未知右值引用类型,需要根据规则进行推导型别的引用。
只要发生了类型推导就会是万能引用,在T&&和auto&&的初始化过程中都会发生类型的推导所以它们是万能引用。在这个推导过程中,初始化的源对象如果是一个左值,则目标对象会推导出左值引用;反之如果源对象是一个右值,则会推导出右值引用。
通过类型推导细分万能引用,关于万能引用主要涉及到引用折叠规则(类型推导)、完美转发两个方面。
完美转发
上面介绍了万能引用,它的一个重要用途就是进行完美转发,所谓完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数,不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
在C++11使用标准库中的std::forward函数就可以试下完美转发: