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.

顺序 选择 循环 总结

posted @ 2020-01-26 21:03  kaizenly  阅读(1478)  评论(0编辑  收藏  举报
打赏