右值和左值
C++ 中右值概念是:只能出现在
operator= 右侧的表达式,而左值的概念则是:可能出现在 operator= 左边的的表达式。官方给的概念比较容易理解,但是我们在这里还是要做两点解释。
首先,对于左值官方的定义是可能出现在 operator=左边的表达式,因此我们要注意左值是既可以出现在 operator= 左边,也可以出现在 operator= 的右边,例如:
//在 = 左边的左值
int n1 = 1;
int n2 = 2;
//在 = 右边的左值
n1 = n2;
n2 = n1;
在上面的例子中,n1、n2 无论是在
“=“ 的左边还是右边,它们都是左值。
此外,关于右值的定义官方的说明是只能出现在operator= 的右边,但是在实际的编程中有时候不会那么严格的遵守这条规范,比如:
#include
#include
using namespace std;
int main(){
string s1("Hello ");
string s2("World ");
s1 + s2 = s2; //通过编译,但是 s1 + s2 是一个临时变量
return 0;
}
在上面的例子中,很明显 string 类库的编写者并没有遵循右值的标准定义。
那么,我们该如何判断左值和右值呢?在大部分情况下,官方的定义还是准确的。而当我们判断不准一个表达式是左值还是右值的时候可以使用这样的方法来判断:若可对表达式用 & 符取址,则为左值,否则为右值。比如下面的例子:
#include
#include
using namespace std;
int main(){
string s1("Hello ");
string s2("World ");
int n1 = 1;
int n2 = 2;
s1 + s2 = s2;
cout << &(s1 + s2) << endl;
return 0;
}
上例中,s1 + s2 是一个右值,因此编译器会报:taking address of emporary 的错误。
右值引用
右值引用 C++11 引入新的语法,它是为了配合移动语义来使用的。右值的引用的语法格式如下:T &&。使用两个 && 来表示,以区分左值引用。比如:
int &&p = 5;
引入了右值引用后,C++ 的所属值就分为了三种:左值、将亡值和纯右值,将亡值是新增的与右值引用相关的表达式(包括被移动的对象、T&&函数的返回值、std::move 返回值和转换为 T&& 的类型的转换函数的返回值)。
右值引用就是对右值进行引用的类型,因为右值不具名,所以我们只能通过引用来找到它。同左值引用一样,右值引用在声明的时候必须初始化。
右值引用比较容易理解,但是它会产生引用折叠的问题。
引用折叠
虽然 T&& 叫右值引用,但是当 T&& 和自动类型推导一起使用的是时候(如函数模板的类型自动推导、auto
关键字的),T&&就是一个万能引用(universal
references)。比如:
#include
#include
using namespace std;
template
void f(T&& param){}
int main(){
int x = 10;
f(10); //传入参数右值
f(x); //传入参数为左值
return 0;
}
在上例中,虽然 f() 函数定义时,其参数被声明为右值引用,但是当在具体调用的的时候,依然可以传入左值 x 作为参数。因为此时的 T&& 是一个万能引用。
在这里我们要注意的是,T&& 可以成为万能引用的条件是非常苛刻,有一点的附加条件都不行。来看下面的例子:
template
void f(const T&& param){}
由于 T&& 被
const 所修饰,所以函数的参数 param 就不再是一个万能引用,而是一个右值引用。
由于 T&& 存在未定的引用类型,这时就会出现引用折叠的问题:当T&& 作为一个参数时。T&& 有可能被一个左值引用或者右值引用的参数初始化,这时经过类型推导的 T&& 类型,相比右值引用会发生类型的变化(可能是左值引用,也可能是右值引用),这种变化被称为引用折叠。
引用折叠的规则如下:
1)所有的右值引用叠加到右值引用上仍然是一个右值引用。
#include
#include <type_traits>
using namespace std;
int func(){ return 5;}
int main(){
auto&& f = func();
return 0;
}
在上例中我们要注意到func() 是一个右值,因此 f 与 auto&& 结合会被推导为一个右值引用。
2)所有的其他引用类型之间的叠加都将变成左值引用
#include
#include <type_traits>
using namespace std;
class People{};
int main(){
People p;
People& p1 = p;
auto&& p2 = p1;
return 0;
}
在上面的代码中,p1 是一个左值引用,用它来初始化 p2 的时候,auto 被推断为左值引用。我们可以来看一下编译器进行引用折叠的过程:
1.
首先如果没有引用折叠,应该产生如下的代码:
People& && p2 = p1
但是由于存在引用折叠,所以实际上是这样的:
People& p2 = p1;
最终推导出来的结果就是 p2 是一个左值引用。