C++11 左值、右值与右值引用
【1】左值与右值
在C语言中,常常会提起左值(lvalue)、右值(rvalue)这样的称呼。
而在编译程序时,编译器有时也会在报出的错误信息中包含左值、右值的说法。
不过左值、右值通常不是通过一个严谨的定义而为人所知的。
事实上,之所以只知道一些关于左值、右值的判断而很少听到其真正的定义的一个原因就是很难归纳。而且即使归纳了,仍需要大量的解释。
(1)左右值的判别方法一
大多数时候,左右值的一个最为典型的判别方法:
在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”。比如:
a = b + c;
在这个赋值表达式中,a就是一个左值,而b+c则是一个右值。
这种识别左值、右值的方法在C++中依然有效。
(2)左右值判别方法二
不过C++中还有一个被广泛认同的说法,那就是:
可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。
那么这个加法赋值表达式中,&a是允许的操作,但&(b + c)这样的操作则不会通过编译。
因此a是一个左值,(b+c)是一个右值。这些判别方法通常都非常有效。
(3)左右值判别方法三
更为细致地,在C++11中,右值是由两个概念构成的,一个是纯右值(prvalue,Pure Rvalue),另一个则是将亡值(xvalue,eXpiring Value)。
其中纯右值就是C++98标准中右值的概念,指的是用于辨识临时变量和一些不跟对象关联的值。
比如:返回类型为非引用的函数返回的临时变量值就是一个纯右值。
一些运算表达式,比如1+3产生的临时变量值,也是纯右值。
而不跟对象关联的字面量值,比如:2、‘c’、true,也是纯右值。
此外,类型转换函数的返回值、lambda表达式等,也都是右值。
而将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如:
[1] 返回右值引用T&&的函数返回值
[2] std::move的返回值
[3] 转换为T&&的类型转换函数的返回值。
而剩余的,可以标识函数、对象的值都属于左值。
所以,在C++11的程序中,所有的值必属于左值、将亡值、纯右值三者之一。
【2】左值引用与右值引用
在C++11中,右值引用(rvalue reference)就是对一个右值进行引用的类型。
为了区别于之前C++98中的引用类型,我们称C++98中的引用为“左值引用”(lvalue reference)。
右值引用和左值引用都是属于引用类型,无论是声明一个左值引用还是右值引用,都必须立即进行初始化。
其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。
左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。
通常情况下,我们只能是从右值表达式获得其引用。比如:
T && a = ReturnRvalue();
这个表达式中,假设ReturnRvalue返回一个右值,我们就声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。
ReturnRvalue函数返回的右值在表达式语句结束后,其生命也就终结了(通常我们也称其具有表达式生命期)。
而通过右值引用的声明,该右值又“重获新生”,其生命期将与右值引用类型变量a的生命期一样。
即就是,只要a还“活着”,该右值临时量将会一直“存活”下去。所以,相比于以下语句的声明方式:
T b = ReturnRvalue();
刚才的右值引用变量声明,就会少一次对象的析构及一次对象的构造。
因为a是右值引用,直接绑定了ReturnRvalue()返回的临时量,而b只是由临时值构造而成的,而临时量在表达式结束后会被析构掉,因应就会多一次析构和构造的开销。
关于右值引用,可以参考下例加深理解:
1 #include <iostream> 2 3 void f(int& i) { std::cout << "call left value ref :: " << i << "\n"; } 4 void f(int&& i) { std::cout << "call right value ref :: " << i << "\n"; } 5 6 int main() 7 { 8 int i = 2020; 9 f(i); // call left value ref :: 2020 10 f(1010); // call right value ref :: 1010 11 f(std::move(i)); // call right value ref :: 2020 12 13 system("pause"); 14 return 0; 15 } 16 17 /* 18 call left value ref :: 2020 19 call right value ref :: 1010 20 call right value ref :: 2020 21 请按任意键继续. . . 22 */
【3】常量左值引用 与 常量右值引用
(1)常量左值引用
通常情况下,右值引用是不能够绑定到任何的左值的。比如下面的表达式就是无法通过编译的:
int c; int && d = c;
那么,在C++98标准中就已经出现的左值引用是否可以绑定到右值(由右值进行初始化)呢?比如:
T & e = ReturnRvalue(); const T & f = ReturnRvalue();
这样的语句是否能够通过编译呢?答案是:e的初始化会导致编译时错误,而f则不会。出现这样的状况的原因:
常量左值引用在C++98标准中开始就是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。
而且,在使用右值对其初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延长。
不过相比于右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只读的。
相对地,非常量左值只能接受非常量左值对其进行初始化。
(2)常量右值引用
为了语义的完整,C++11中还存在着常量右值引用。比如通过以下代码声明一个常量右值引用:
const T && crvalueref = ReturnRvalue();
但是,一来右值引用主要就是为了移动语义,而移动语义需要右值是可以被修改的,那么常量右值引用在移动语义中就没有用武之处;
二来如果要引用右值且让右值不可以更改,常量左值引用往往就足够了。
因此,在现在的尴尬情况下,还没有看到常量右值引用有何用处。
(3)引用类型可引用值的类型表
下面列出了在C++11中各种引用类型可以引用的值的类型:
值得注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。
【4】如何判断?
有的时候,我们可能不知道一个类型是否是引用类型,以及是左值引用还是右值引用(这在模板中比较常见)。
标准库在<type_traits>头文件中提供了3个模板类:is_rvalue_reference、is_lvalue_reference、is_reference,可供我们进行判断。比如:
1 #include <iostream> 2 #include <type_traits> 3 using namespace std; 4 5 int main() 6 { 7 cout << is_reference<int>::value << endl; // 0 8 cout << is_rvalue_reference<string &&>::value << endl; // 1 9 cout << is_lvalue_reference<string &> ::value << endl; // 1 10 cout << is_rvalue_reference<const string &&>::value << endl; // 1 11 cout << is_lvalue_reference<const string &> ::value << endl; // 1 12 }
我们通过模板类的成员value就可以打印出stirng && 是否是一个右值引用了。
配合C++11中的类型推导操作符decltype,我们甚至还可以对变量的类型进行判断。
good good study, day day up.
顺序 选择 循环 总结