第二十四章 C++11特性之右值引用

右值引用,是 C++11 语言核心中最为重要的改进之一。右值引用给 C++ 带来了“Move语义”(“转移语义”),同时解决了模板编程中完美转发的问题(Perfect forwarding)。右值引用使 C++ 对象有能力甄别什么是(可以看作)临时对象,对于临时对象的拷贝可以做某种特别的处理,一般来说主要是直接传递资源的所有权而不是像一般地进行拷贝,这就是所谓的 move 语义了。完美转发则是指在模板编程的时候,各层级函数参数传递时不会丢失参数的“属性”(lvalue/rvalue, const?之类的)。这两个问题都对提高 C++ 程序的效率很有好处,让 C++ 的程序可以更加精确地控制资源和对象的行为,(我觉得,同时在一定程度上也提高的 C++ 程序形式上的“自由度”)。

只要英文基本上过得去,建议阅读下面几个网页的文章,他们都是世界级的专家,对 C++11 的理解比我准确得多:

1. 比如清楚地说明了"why",但是没有说明"how",在Google排好前啊,Scott Meyers也推荐了(下面)

C++ Rvalue References Explained 
http://thbecker.net/articles/rvalue_references/section_01.html

2. Effective C++ 的作者写的

Universal References in C++11—Scott Meyers 
http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

3. VC 的库函数开发组的老师写的,这个我觉得技术细节上说得很清楚。但是很长。把“how”说得很清楚。

Rvalue References: C++0x Features in VC10, Part 2 
http://blogs.msdn.com/b/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx

如果想看简化的中文版,就继续往下,这篇基本上是上面资料3的笔记

一、如何区分左右值

要理解右值引用(还有我们一直在用的“引用”,以后叫左值引用),就要先搞清楚“左值、右值”是什么。首先,左值右值是针对“表达式”而言的,如果一个表达式所表示的内容,在这个表达式语句完了之后还能访问到(也就是有一个实实在在的名称代表它),那么这个表达式就是左值,否则就是右值。我的理解就是,“需要编译器为程序生成临时“匿名”变量来表示的表达式的值,就是右值”。但是人类又不是编译器,用这个方法来判断左右值并不方便。最简单的判断一个表达式是左值还是右值的方法是,“能不能对这个表达式取地址”。比如下面几个:

  1. int x = 0;
  2. x + 1;           // rvalue
  3. x;               // lvalue
  4. ++x;             // lvalue
  5. x++;             // rvalue
  6. int y[10];
  7. y[0];            // lvalue
  8. "literal string" // rvalue

之所以“能不能对这个表达式取地址”可以作为表达式是左值还右值的判断方法呢,因为 C++ 标准说取地址符只能应用在左值上。((C++03 5.3.1/2).)(有例外,参考资料3说 VC 对 C++ 可以开扩展选项,但是一般正直的程序员不会用它!:))。因为右值被看成是“匿名”变量(或者说,是编译器为我们加的变量),是程序中的幽灵变量,程序员不需要知道它的存在,更不应该去处理它,所以如果能对右值取地址,容易出现很多对程序造成危险的行为。

再举个比较常见的例子:

  1. x = a + b + c + d;

其实编译器会生成类似下面伪代码的“东西”

  1. temp1 = a + b;
  2. temp2 = temp1 + c;
  3. x = temp2 + d;

这个例子中的 temp?,就是右值(temp这个名字只有编译器知道,程序是不知道的)。

(原因我猜是两个,一是因为大部分CUP所支持的机器指令都是两操作符的(大多数的运算符也是两个操作数的),另一个是把复杂的式子用类似的方法化简成简单的式子(二叉树形式语法树)并最终翻译成汇编是编译器的成熟算法,词法分析基本上都是在干这个活。(纯猜测,非科班,没学过))

我们之所以会关注右值,主要的原因是,右值有时候会带来不必要的性能开销。还是举类似的例子,如果 x, y, z 是某个类(A)的对象,A 在构造的时候动态地申请资源(比如一块大的内存),在拷贝构造的函数中也一样,先申请一块同样大小的内存,然后把拷贝元对象的内容复制一份到刚申请到的内存中。那么,当我们写下 z = x + y 的时候,重载的操作符函数 + 产生的临时变量 temp,把 x + y 的值计算好放在temp中,(这个temp是不可避免的,因为+不应该改变操作数的值),然后 z 再调用拷贝构造函数,申请一大块内存,把 temp 的值拷贝过来,最后,temp再调用析构函数,把自己的内存释放掉。 其实如果斤斤计较下,就会发现其实 temp 只是一个临时的过渡,反正 z = x + y 这个语句结束之后 temp 就没有意义(也被析构了),那么何不把 temp 的内存换给 z 呢,这样 z 不需要申请请的内存,也不需要拷贝它的值,temp也不需要释放内存了,省下不少开销啊。

是的,我们当然想这么干,不过在 C++11 之前,程序没有办法分辨拷贝的对象是不是一个临时变量(可变的右值),是不是可以安全地从这个对象中把资源偷过来,我们只能写一种“拷贝构造函数”,是的,就是我们一直在写的那种“拷贝用”的构造函数。现在你明白了为什么 C++ 中要增加右值引用了,为的就是让程序可以分辨出一个对象是不是“幽灵对象”,然后再有区别地“对待”传入函数的对象。不过先别急,先看看引用的类型。

二、引用的类型

现在,C++11里的引用类型分为下面几种了:

  • 可变左值引用 :type &
  • 常左值引用:type const &
  • 可变右值引用:type &&
  • 常右值引用:type const &&

这几种引用在初始化的时候,能绑定到什么“值”上呢?遵从两个规则:

  1. 要遵照“常量正确”的原则,也就是非常量的引用(type & 和 type &&)不能绑定到常量上(常量左值,常量右值)
  2. 防止意外修改“幽灵变量”,所以可变左值引用(type &)不能绑定到右值上

总结一下就是下面这个绑定的关系图:

在C++03的时候,我们就已经知道type &和type const & 这两种类型的参数是可以参与函数重载的,现在 C++11 加入了两个新的引用类型,也是可以参与重载的,重载的规则如下:

  1. 不能违反初始化绑定规则
  2. 左值倾向于选择(常)左值引用,右值倾向于选择(常)右值引用(强)
  3. 非常量值倾向于选择非常量引用(弱)

还是有例子会比较容易理解:

引用重载例1

  1. #include <iostream>
  2. #include <string>
  3. #include <iomanip>
  4. using namespace std;
  5. void reference_overload(string & str) {
  6. cout<<setw(15)<<str<<" => "<<"type &        "<<endl;
  7. }
  8. void reference_overload(string && str) {
  9. cout<<setw(15)<<str<<" => "<<"type &&       "<<endl;
  10. }
  11. void reference_overload(string const & str) {
  12. cout<<setw(15)<<str<<" => "<<"type const &  "<<endl;
  13. }
  14. void reference_overload(string const && str) {
  15. cout<<setw(15)<<str<<" => "<<"type cosnt && "<<endl;
  16. }
  17. string const GetConstString() {
  18. return string("const_rvalue");
  19. }
  20. int main() {
  21. string lvalue("lvalue");
  22. string const const_lvalue("const_lvalue");
  23. reference_overload(lvalue);
  24. reference_overload(const_lvalue);
  25. reference_overload(string("rvalues"));
  26. reference_overload(GetConstString());
  27. }

运行的结果如下:

符合我们预期的想像。但在实际中,一般不需要重载这四种,而只需要对:

  • type const &
  • type &&

两种引用类型进行重载就很有用了,只有这两种类型的重载时,会有什么样的匹配发生?

引用重载例2

  1. #include <iostream>
  2. #include <string>
  3. #include <iomanip>
  4. using namespace std;
  5. void reference_overload(string && str) {
  6. cout<<setw(15)<<str<<" => "<<"type &&       "<<endl;
  7. }
  8. void reference_overload(string const & str) {
  9. cout<<setw(15)<<str<<" => "<<"type const &  "<<endl;
  10. }
  11. string const GetConstString() {
  12. return string("const_rvalue");
  13. }
  14. int main() {
  15. string lvalue("lvalue");
  16. string const const_lvalue("const_lvalue");
  17. reference_overload(lvalue);
  18. reference_overload(const_lvalue);
  19. reference_overload(string("rvalues"));
  20. reference_overload(GetConstString());
  21. }

运行结果如下:

从上面的结果看到,C++11 的引用重载规则基础上,实现对 type const & 和 type && 的重载,程序就可以“区分”出,什么变量的值是可以“神不知鬼不觉地偷走的”,而什么变量的值是不可以动的。有了这个办法,类的设计者就可以设计出“move 语义”的拷贝构造函数了。

三、move !

其实后面比较轻松,没有多少要记的东西了,举个简单的例子

  1. A(const A& _right) {
  2. this->p = new int[100];
  3. memcpy(this->p, _right.p, 100);
  4. }
  5. A(A && _right) {
  6. this->p = _right.p;
  7. _right.p = nullptr;
  8. }

1到4行是拷贝构造函数,而5到8行是转移构造函数(move)。拷贝构造函数中,我们从一个(常)左值,或是常右值作为拷贝源来构造一个新对象,原对象是不可以做修改的,所以只能重新 new 一块新的区域,并把数据拷贝一份。但如果拷贝源对象是非常量右值,说明这是一个“无人关心的”变量,我们可以通过指针交换,获取变量所拥有的资源,而“源对象”的针对则指向空,反正这个构造一结束,这个无人关心的变量就析构了。

从上面的例子可以很容易看出,在合适的地方使用这种转义构造,可以很大的提高程序性能。

当然这些是不够的,还有很多地方,我们也想用转移构造,却可能出问题,比如说下面这个例子:

  1. A GetA() {
  2. A temp;
  3. // do sth.
  4. return temp;
  5. }
  6. A x = GetA()

作为程序员,我知道在 return temp 这个语句后,这个temp变量就没有用了,因此我很想能把 temp 这个变量的内容,用转移拷贝构造函数转移给变量x,但是,temp是个左值!转移拷贝构造函数不会被选中!没有办法了吗?如果我们可以把左值“转换”成右值,让编译器调用转移构造函数的话,那就好了。这是一非常“普遍的需求”,除了上面这个例子之外,我们还会遇到很多类似的情况,比如,我想用“赋值运算符”来实现拷贝构造函数的时候:

  1. A(const A& _right) {
  2. this->p = new int[100];
  3. memcpy(this->p, _right.p, 100);
  4. }
  5. A(A && _right) {
  6. this->p = nullptr
  7. *this = _right; // Unexpected thing happens
  8. }
  9. A& operator = (A && _right) {
  10. this->p = _right.p;
  11. _right.p = nullptr;
  12. return *this;
  13. }
  14. A& operator = (A const & _right) {
  15. this->p = new int[100];
  16. memcpy(this->p, _right.p, 100);
  17. return *this;
  18. }

第7行,我们期待它能调用第9行的“转移赋值运算符”,但实际上会调用第15行的“普通赋值运算符”,

因为在C++中

  1. 具名的左值是左值
  2. 不具名的左值引用是左值
  3. 具名的右值引用是左值
  4. 不具名的右值引用是右值

所以上面这个例子,最终并不能实现我们想要的效果。

怎么办呢?C++11提供了这样的办法:std::move,现在程序会是这样:

  1. A GetA() {
  2. A temp;
  3. // do sth.
  4. return std::move(temp);
  5. }
  6. A x = GetA()

好了,现在转移构造函数被调用了,像魔法对吧? std::move 这个名字取得不好,其实它本身没有“move”任何东西,它只是把“不是“非常量右值”转换成“非常量右值””。但这是怎么做到的呢?其实并不难:

  1. template <typename T> struct RemoveReference {
  2. typedef T type;
  3. };
  4. template <typename T> struct RemoveReference<T&> {
  5. typedef T type;
  6. };
  7. template <typename T> struct RemoveReference<T&&> {
  8. typedef T type;
  9. };
  10. template <typename T> typename RemoveReference<T>::type&& move(T&& t) {
  11. return t;
  12. }

有了这个 move 之后,就有办法把左值,右值,左值引用,右值引用都转换成右值引用了。

好了,到这里为止,基本上对于左值右值,以及它们的引用,还有转移语义,都应该清楚了。

move 语义和右值引用,确实使 C++ 更加“复杂”了,付出这个代价当然会有回报,STL 的性能得到了很大的提升,想想 vector<string>  v; 这么一个对象,每当 v 的内存区域需要扩大或是缩小的时候,在没有 move 的时候,每个一存储在 v 中的 string 都需要重新拷贝一次,但现在不用了,它们只需要交换指针。而这一切你只需要换一个编译器就可以得到,完全不需要修改以前的程序。 当然,以后当我们再写会动态申请资源的类的时候(需要深拷贝的类(实现 the rule three)),如果实现上这个类的转移拷贝构造和赋值的函数的话,我们也可以在使用 STL 和其它一些函数的时候得这种性能上的好处。而且,它也使得“传值”在C++中变得不那么可怕了。你可以不再为了“性能”而不得不对程序的形式作出妥协,比如使用引用参数而不是返回值来得到函数的 output 等等。

作为一个“不怎么写模板和库”的程序员,我觉得理解了到这里为止的内容,就已经够了。而且这些内容并不容易消化。

完美转发,就留给下次有余力的时候再学习了~

时间: 2024-12-10 19:21:54

第二十四章 C++11特性之右值引用的相关文章

C++11特性之右值引用

title: 右值引用与移动语义 date: 2019-2-24 15:06:34 tags: 学习 categories: 日常 --- 什么是右值?在C++中,一种被广泛认可的说法是,不能取地址,没有名字的就是右值,通常位于等号右边,相反,位于等号左边的,能取地址,有名字的被称为左值. a = b + c 例如上式中,a就是个左值,b+c则是右值. C++11又将右值分为纯右值和将亡值.纯右值包括:不跟对象关联的字面值,一些运算表达式(如1+3).将亡值是跟右值引用相关的表达式,比如右值引用

C#图解教程 第二十四章 反射和特性

反射和特性元数据和反射Type 类获取Type对象什么是特性应用特性预定义的保留的特性Obsolete(废弃)特性Conditional特性调用者信息特性DebuggerStepThrough 特性其他预定义特性有关应用特性的更多内容多个特性其他类型的目标全局特性自定义特性声明自定义特性使用特性的构造函数指定构造函数使用构造函数构造函数中的位置参数和命名参数限制特性的使用自定义特性的最佳实践访问特性使用IsDefined方法使用GetCustomAttributes方法 Note 类的元数据包含

Gradle 1.12用户指南翻译——第二十四章. Groovy 插件

其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Github上的地址: https://github.com/msdx/gradledoc/tree/1.12. 直接浏览双语版的文档请访问: http://gradledoc.qiniudn.com/1.12/userguide/userguide.html. 另外,Android 手机用户可通过我写的一个程序浏览文档,带缓存功能的,兼容

第二十四章

希言自然.飘风不终朝,骤雨不终日.孰为此?天地,天地而弗能久,又况于人乎?故从事而道者同于道,德者同于德,失者同于失.同于德者,道亦德之.同于失者,道亦失之. 第二十四章1 为何盛世的领导者很少有丰功伟绩? 各位朋友大家好,今天我们接着来聊<道德经>.前边大家的留言我都看了,写的感想我也看了,我真的没想到大家感想写的这么好.而且这个<道德经>给大家带来这么多的变化.这么多的提升,让我特别开心,真的非常感动.我自己在讲<道德经>的过程中,说实话我自己也在不断地提升.也在学

【WPF学习】第二十四章 基于范围的控件

原文:[WPF学习]第二十四章 基于范围的控件 WPF提供了三个使用范围概念的控件.这些控件使用在特定最小值和最大值之间的数值.这些控件--ScrollBar.ProgressBar以及Slider--都继承自RangeBase类(该类又继承自Control类).尽管它们使用相同的抽象概念(范围),但工作方式却又很大的区别. 下表显示了RangeBase类定义的属性: 表 RangeBase类的属性 通常不比直接使用ScrollBar控件.更高级的ScrollViewer控件(封装了两个Scro

C++11新特性:右值引用和转移构造函数

问题背景 [cpp] view plaincopy #include <iostream> using namespace std; vector<int> doubleValues (const vector<int>& v) { vector<int> new_values( v.size() ); for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end

C++ 11 中的右值引用

C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include <vector>    using namespace std; class obj    {    public :        obj() { cout << ">> create obj " << endl; }        obj(c

[转载] C++11中的右值引用

C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导规则 特殊模板参数推导规则 解决完美转发问题 引用 在C++98中有左值和右值的概念,不过这两个概念对于很多程序员并不关心,因为不知道这两个概念照样可以写出好程序.在C++11中对右值的概念进行了增强,我个人理解这部分内容是C++11引入的特性中最难以理解的了.该特性的引入至少可以解决C++98中的

C++11标准之右值引用(rvalue reference)

1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了Copy Elision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下Copy Elision.RVO,对此不感兴趣的可以直接跳过: (1) Copy Elision Copy Elision技术是为了防止某些不必要的临时对象产生和拷贝,例如: struct A { A(int)