读书频道 > 网站 > 网页设计 > 深入理解C++11:C++ 11新特性解析与应用
3.3.3 左值、右值与右值引用
13-06-18    奋斗的小年轻
收藏    我要投稿   
国内首本全面深入解读C++11新标准的专著,由C++标准委员会代表和IBM XL编译器中国开发团队共同撰写。不仅详细阐述了C++11标准的设计原则,而且系统地讲解了C++11新标准中的所有新语言特性、新标准库特性、对原有...立即去当当网订购

在C语言中,我们常常会提起左值(lvalue)、右值(rvalue)这样的称呼。而在编译程序时,编译器有时也会在报出的错误信息中会包含左值、右值的说法。不过左值、右值通常不是通过一个严谨的定义而为人所知的,大多数时候左右值的定义与其判别方法是一体的。一个最为典型的判别方法就是,在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”。比如:

a = b + c;

在这个赋值表达式中,a就是一个左值,而b + c则是一个右值。这种识别左值、右值的方法在C++中依然有效。不过C++中还有一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。那么这个加法赋值表达式中,&a是允许的操作,但&(b + c)这样的操作则不会通过编译。因此a是一个左值,(b + c)是一个右值。

这些判别方法通常都非常有效。更为细致地,在C++11中,右值是由两个概念构成的,一个是将亡值(xvalue,eXpiring Value),另一个则是纯右值(prvalue,Pure Rvalue)。

其中纯右值就是C++98标准中右值的概念,讲的是用于辨识临时变量和一些不跟对象关联的值。比如非引用返回的函数返回的临时变量值(我们在前面多次提到了)就是一个纯右值。一些运算表达式,比如1 + 3产生的临时变量值,也是纯右值。而不跟对象关联的字面量值,比如:2、‘c’、true,也是纯右值。此外,类型转换函数的返回值、lambda表达式(见7.3节)等,也都是右值。

而将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值(稍后解释),或者转换为T&&的类型转换函数的返回值(稍后解释)。而剩余的,可以标识函数、对象的值都属于左值。在C++11的程序中,所有的值必属于左值、将亡值、纯右值三者之一。

注意 事实上,之所以我们只知道一些关于左值、右值的判断而很少听到其真正的定义的一个原因就是—很难归纳。而且即使归纳了,也需要大量的解释。

在C++11中,右值引用就是对一个右值进行引用的类型。事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。通常情况下,我们只能是从右值表达式获得其引用。比如:
T && a = ReturnRvalue();

这个表达式中,假设ReturnRvalue返回一个右值,我们就声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。

为了区别于C++98中的引用类型,我们称C++98中的引用为“左值引用”(lvalue reference)。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

在上面的例子中,ReturnRvalue函数返回的右值在表达式语句结束后,其生命也就终结了(通常我们也称其具有表达式生命期),而通过右值引用的声明,该右值又“重获新生”,其生命期将与右值引用类型变量a的生命期一样。只要a还“活着”,该右值临时量将会一直“存活”下去。

所以相比于以下语句的声明方式:
T b = ReturnRvalue();

我们刚才的右值引用变量声明,就会少一次对象的析构及一次对象的构造。因为a是右值引用,直接绑定了ReturnRvalue()返回的临时量,而b只是由临时值构造而成的,而临时量在表达式结束后会析构因应就会多一次析构和构造的开销。

不过值得指出的是,能够声明右值引用a的前提是ReturnRvalue返回的是一个右值。通常情况下,右值引用是不能够绑定到任何的左值的。比如下面的表达式就是无法通过编译的。
int c;
int && d = c;

相对地,在C++98标准中就已经出现的左值引用是否可以绑定到右值(由右值进行初始化)呢?比如:
T & e = ReturnRvalue();
const T & f = ReturnRvalue();

这样的语句是否能够通过编译呢?这里的答案是:e的初始化会导致编译时错误,而f则不会。

出现这样的状况的原因是,在常量左值引用在C++98标准中开始就是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。而且在使用右值对其初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延长。不过相比于右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。

既然常量左值引用在C++98中就已经出现,读者可能会努力地搜索记忆,想找出在C++中使用常量左值绑定右值的情况。不过可能一切并不如愿。这是因为,在C++11之前,左值、右值对于程序员来说,一直呈透明状态。不知道什么是左值、右值,并不影响写出正确的C++代码。引用的是左值和右值通常也并不重要。不过事实上,在C++98通过左值引用来绑定一个右值的情况并不少见,比如:
const bool & judgement = true;

就是一个使用常量左值引用来绑定右值的例子。不过与如下声明相比较看起来似乎差别不大。
const bool judgement = true;

可能很多程序员都没有注意到其中的差别(从语法上讲,前者直接使用了右值并为其“续命”,而后者的右值在表达式结束后就销毁了)。

事实上,即使在C++98中,我们也常可以使用常量左值引用来减少临时对象的开销,如代码清单3-20所示。

代码清单3-20
#include <iostream>
using namespace std;

struct Copyable {
    Copyable() {}
    Copyable(const Copyable &o) {
        cout << "Copied" << endl;
    }
};

Copyable ReturnRvalue() { return Copyable(); }
void AcceptVal(Copyable) {}
void AcceptRef(const Copyable & ) {}

int main() {
    cout << "Pass by value: " << endl;
    AcceptVal(ReturnRvalue()); // 临时值被拷贝传入
    cout << "Pass by reference: " << endl;
    AcceptRef(ReturnRvalue()); // 临时值被作为引用传递
}
// 编译选项:g++ 3-3-5.cpp -fno-elide-constructors

在代码清单3-20中,我们声明了结构体Copyable,该结构体的唯一的作用就是在被拷贝构造的时候打印一句话:Copied。而两个函数,AcceptVal使用了值传递参数,而AcceptRef使用了引用传递。在以ReturnRvalue返回的右值为参数的时候,AcceptRef就可以直接使用产生的临时值(并延长其生命期),而AcceptVal则不能直接使用临时对象。

编译运行代码清单3-20,可以得到以下结果:
Pass by value:
Copied
Copied
Pass by reference:
Copied

可以看到,由于使用了左值引用,临时对象被直接作为函数的参数,而不需要从中拷贝一次。读者可以自行分析一下输出结果,这里就不赘述了。而在C++11中,同样地,如果在代码清单3-20中以右值引用为参数声明如下函数:
void AcceptRvalueRef(Copyable && ) {}

也同样可以减少临时变量拷贝的开销。进一步地,还可以在AcceptRvalueRef中修改该临时值(这个时候临时值由于被右值引用参数所引用,已经获得了函数时间的生命期)。不过修改一个临时值的意义通常不大,除非像3.3.2节一样使用移动语义。
就本例而言,如果我们这样实现函数:
void AcceptRvalueRef(Copyable && s) {
   Copyable news = std::move(s);
}

这里std::move的作用是强制一个左值成为右值(看起来很奇怪?这个我们会在下面一节中解释)。该函数就是使用右值来初始化Copyable变量news。当然,如同我们在上小节提到的,使用移动语义的前提是Copyable还需要添加一个以右值引用为参数的移动构造函数,比如:
Copyable(Copyable &&o) { /* 实现移动语义 */ }

这样一来,如果Copyable类的临时对象(即ReturnRvalue返回的临时值)中包含一些大块内存的指针,news就可以如同代码清单3-19一样将临时值中的内存“窃”为己用,从而从这个以右值引用参数的AcceptRvalueRef函数中获得最大的收益。事实上,右值引用的来由从来就跟移动语义紧紧相关。这是右值存在的一个最大的价值(另外一个价值是用于转发,我们会在后面的小节中看到)。

对于本例而言,很有趣的是,读者也可以思考一下:如果我们不声明移动构造函数,而只声明一个常量左值的构造函数会发生什么?如同我们刚才提到的,常量左值引用是个“万能”的引用类型,无论左值还是右值,常量还是非常量,一概能够绑定。那么如果Copyable没有移动构造函数,下列语句:
Copyable news = std::move(s);

将调用以常量左值引用为参数的拷贝构造函数。这是一种非常安全的设计—移动不成,至少还可以执行拷贝。因此,通常情况下,程序员会为声明了移动构造函数的类声明一个常量左值为参数的拷贝构造函数,以保证在移动构造不成时,可以使用拷贝构造(不过,我们也会在之后看到一些特殊用途的反例)。

为了语义的完整,C++11中还存在着常量右值引用,比如我们通过以下代码声明一个常量右值引用。
const T && crvalueref = ReturnRvalue();

但是,一来右值引用主要就是为了移动语义,而移动语义需要右值是可以被修改的,那么常量右值引用在移动语义中就没有用武之处;二来如果要引用右值且让右值不可以更改,常量左值引用往往就足够了。因此在现在的情况下,我们还没有看到常量右值引用有何用处。

表3-1中,我们列出了在C++11中各种引用类型可以引用的值的类型。值得注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。

表3-1 C++11中引用类型及其可以引用的值类型
引用类型 可以引用的值类型 注  记
 非常量左值 常量左值 非常量右值 常量右值 
非常量左值引用 Y N N N 无
常量左值引用 Y Y Y Y 全能类型,可用于拷贝语义
非常量右值引用 N N Y N 用于移动语义、完美转发
常量右值引用 N N Y Y 暂无用途

有的时候,我们可能不知道一个类型是否是引用类型,以及是左值引用还是右值引用(这在模板中比较常见)。标准库在<type_traits>头文件中提供了3个模板类:is_rvalue_reference、is_lvalue_reference、is_reference,可供我们进行判断。比如:
cout << is_rvalue_reference<string &&>::value;

我们通过模板类的成员value就可以打印出stirng &&是否是一个右值引用了。配合第4章中的类型推导操作符decltype,我们甚至还可以对变量的类型进行判断。当读者搞不清楚引用类型的时候,不妨使用这样的小工具实验一下。

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.3 功能
下一篇:1.5 小结
相关文章
图文推荐
JavaScript网页动画设
1.9 响应式
1.8 登陆页式
1.7 主题式
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站