相关博文:
C++新特性探究(十三):右值引用(r-value ref)&&探究
C++新特性探究(十六):move constructor移动构造
C++新特性探究(13.5):右值引用
C++新特性探究(13.6):右值引用再探究

右值引用

  右值引用是C++11 引入的与Lambda 表达式齐名的重要特性之一。它的引入解决了C++ 中大量的历史遗留问题,消除了诸如std::vector、std::string 之类的额外开销,也才使得函数对象容器std::function 成为了可能。

左值、右值的纯右值、将亡值、右值

  要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解。
  左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来说,左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
  右值(rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。
  而C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
  纯右值(prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如10, true;要么是求值;结果相当于字面量或匿名临时对象,例如1+2。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda 表达式都属于纯右值。

需要注意的是,字符串字面量只有在类中才是右值,当其位于普通函数中是左值。例如:


  将亡值(xvalue, expiring value),是C++11 为了引入右值引用而提出的概念(因此在传统C++中,纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

将亡值可能稍有些难以理解,我们来看这样的代码:

  在这样的代码中,就传统的理解而言,函数foo的返回值temp在内部创建然后被赋值给v,然而v获得这个对象时,会将整个temp拷贝一份,然后把temp 销毁,如果这个temp非常大,这将造成大量额外的开销(这也就是传统C++一直被诟病的问题)。在最后一行中,v是左值、foo( )返回的值就是右值(也是纯右值)。但是,v可以被别的变量捕获到,而foo( )产生的那个返回值作为一个临时值,一旦被v复制后,将立即被销毁,无法获取、也不能修改。而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。
  在C++11之后,编译器为我们做了一些工作,此处的左值temp会被进行此隐式右值转换,等价于static_cast<std::vector &&>(temp),进而此处的v会将foo局部返回的值进行移动。也就是后面我们将会提到的移动语义。

右值引用和左值引用

  要拿到一个将亡值,就需要用到右值引用:T&&,其中T是类型。右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。
  C++11 提供了std::move这个方法将左值参数无条件的转换为右值,有了它我们就能够方便的获得一个右值临时对象,例如:

//小问学编程
#include<iostream>
#include<string>
using namespace std;void reference(string& str)
{cout<<"左值"<<endl;
}void reference(string&& str)
{cout<<"右值"<<endl;
}int main()
{string lv1="string";//lv1是一个左值//string&& r1=lv1;//非法,右值引用不能引用左值string&& rv1=std::move(lv1);//合法,move可以将左值转移为右值cout<<rv1<<endl;//stringconst string& lv2=lv1+lv1;//合法,常量左值引用能够延长临时变量的生命周期//lv2+=“Test”;//非法,常量引用无法被改变cout<<lv2<<endl;//string,stringstring&& rv2=lv1+lv2;//合法,右值引用延长临时变量生命周期rv2+="Test";//合法,非常量引用能够修改临时变量cout<<rv2<<endl;//string,string,string,Testreference(rv2);//输出左值//rv2虽然引用了一个右值,但由于它是一个引用,所以rv2依然是一个左值。return 0;
}

  注意,这里有一个很有趣的历史遗留问题,我们先看下面的代码:

  第一个问题,为什么不允许非常量引用绑定到非左值?这是因为这种做法存在逻辑错误:

  由于int& 不能引用double 类型的参数,因此必须产生一个临时值来保存s 的值,从而当increase() 修改这个临时值时,从而调用完成后s 本身并没有被修改。
  第二个问题,为什么常量引用允许绑定到非左值?原因很简单,因为Fortran需要。

移动语义

  传统C++通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。试想,搬家的时候是把家里的东西直接搬到新家去,而不是将所有东西复制一份(重买)再放到新家、再把原来的东西全部扔掉(销毁),这是非常反人类的一件事情。
  传统的C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。右值引用的出现恰好就解决了这两个概念的混淆问题,例如:
在上面的代码中:
  1. 首先会在return_rvalue内部构造两个A对象,于是获得两个构造函数的输出;
  2. 函数返回后,产生一个将亡值,被A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了obj中,而将亡值的指针被设置为nullptr,防止了这块内存区域被销毁。
  从而避免了无意义的拷贝构造,加强了性能。

附上例代码:

//小问学编程
#include <iostream>
class A
{public:int *pointer;A():pointer(new int(1)){std::cout << " 构造" << pointer << std::endl;}A(A& a):pointer(new int(*a.pointer)){std::cout << " 拷贝" << pointer << std::endl;} // 无意义的对象拷贝A(A&& a):pointer(a.pointer){a.pointer = nullptr;std::cout << " 移动" << pointer << std::endl;}~A(){std::cout << " 析构" << pointer << std::endl;delete pointer;}
};
// 防止编译器优化
A return_rvalue(bool test)
{A a,b;if(test) return a; // 等价于static_cast<A&&>(a);else return b; // 等价于static_cast<A&&>(b);
}
int main()
{A obj = return_rvalue(false);std::cout << "obj:" << std::endl;std::cout << obj.pointer << std::endl;std::cout << *obj.pointer << std::endl;return 0;
}

再来看看涉及标准库的例子:

附上例代码:

#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::stringint main()
{std::string str = "Hello world.";std::vector<std::string> v;// 将使用push_back(const T&), 即产生拷贝行为v.push_back(str);// 将输出"str: Hello world."std::cout << "str: " << str << std::endl;// 将使用push_back(const T&&), 不会出现拷贝行为// 而整个字符串会被移动到vector 中,所以有时候std::move 会用来减少拷贝出现的开销// 这步操作后, str 中的值会变为空v.push_back(std::move(str));// 将输出"str: "std::cout << "str: " << str << std::endl;return 0;
}

完美转发

  前面我们提到了,一个声明的右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了问题:

例:
  对于pass(1)来说,虽然传递的是右值,但由于v是一个引用,所以同时也是左值。因此reference(v) 会调用reference(int&),输出『左值』。而对于pass(l) 而言,l是一个左值,为什么会成功传递给pass(T&&) 呢?
  这是基于引用坍缩规则的:在传统C++ 中,我们不能够对一个引用类型继续进行引用,但C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用,既能左引用,又能右引用。但是却遵循如下规则:

  因此,模板函数中使用T&&不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。更准确的讲, 无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。这才使得v作为左值的成功传递。

附上例代码:

//小问学编程
#include <iostream>
using namespace std;void reference(int& v)
{std::cout << " 左值" << std::endl;
}
void reference(int&& v)
{std::cout << " 右值" << std::endl;
}
template <typename T>
void pass(T&& v)
{std::cout << " 普通传参:";reference(v); // 始终调用reference(int&)
}
int main()
{std::cout << " 传递右值:" << std::endl;pass(1); // 1 是右值, 但输出是左值std::cout << " 传递左值:" << std::endl;int l = 1;pass(l); // l 是左值, 输出左值return 0;
}

  完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)。为了解决这个问题,我们应该使用std::forward来进行参数的转发(传递):

  无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发,所以std::move总会接受到一个左值,从而转发调用了reference(int&&)输出右值引用。
  唯独std::forward即没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数。
  std::forward和std::move一样,没有做任何事情,std::move单纯的将左值转化为右值,std::forward也只是单纯的将参数做了一个类型的转换,从现象上来看,std::forward(v)和static_cast<T&&>(v) 是完全一样的。

附上例代码:

//小问学编程
#include <iostream>
#include <utility>
void reference(int& v)
{std::cout << "左值引用" << std::endl;
}
void reference(int&& v)
{std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v)
{std::cout << "             普通传参:";reference(v);std::cout << "       std::move 传参:";reference(std::move(v));std::cout <<"    std::forward 传参:";reference(std::forward<T>(v));std::cout <<"static_cast<T&&> 传参:";reference(static_cast<T&&>(v));
}
int main()
{std::cout << " 传递右值:" << std::endl;pass(1);std::cout << " 传递左值:" << std::endl;int v = 1;pass(v);return 0;
}

  读者可能会好奇,为何一条语句能够针对两种类型的返回对应的值,我们再简单看一看std::forward的具体实现机制,std::forward 包含两个重载:

  在这份实现中,std::remove_reference的功能是消除类型中的引用,而std::is_lvalue_reference用于检查类型推导是否正确,在std::forward的第二个实现中检查了接收到的值确实是一个左值,进而体现了坍缩规则。
  当std::forward接受左值时,_Tp被推导为左值,而所以返回值为左值;而当其接受右值时,_Tp被推导为右值引用,则基于坍缩规则,返回值便成为了&& + &&的右值。可见std::forward的原理在于巧妙的利用了模板类型推导中产生的差异。
  这时我们能回答这样一个问题:为什么在使用循环语句的过程中,auto&&是最安全的方式?因为当auto被推导为不同的左右引用时,与&&的坍缩组合是完美转发。

C++新特性探究(13.6):右值引用再探究相关推荐

  1. C++11新特性——移动语义,右值引用

    移动语义 有一些类的资源是__不可共享__的,这种类型的对象可以被移动但不能被拷贝,如:IO 或 unique_ptr 库容器.string 和 shared_ptr 支持拷贝和移动,IO 和 uni ...

  2. C++11新特性(一)右值引用

    @ 一.C++11简介 在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称.不过由于TC1主要是对C+ ...

  3. C++新特性探究(十三):右值引用(r-value ref)探究

    相关博文: C++新特性探究(十三):右值引用(r-value ref)&&探究 C++新特性探究(十六):move constructor移动构造 C++新特性探究(13.5):右值 ...

  4. VC10中的C++0x特性 Part 2 :右值引用

    http://itlab.idcquan.com/c/vc/200906/785943.html

  5. C++新特性探究(13.5):右值引用

    相关博文: C++新特性探究(十三):右值引用(r-value ref)&&探究 C++新特性探究(十六):move constructor移动构造 C++新特性探究(13.5):右值 ...

  6. (译)C++11中的Move语义和右值引用

    郑重声明:本文是笔者网上翻译原文,部分有做添加说明,所有权归原文作者! 地址:http://www.cprogramming.com/c++11/rvalue-references-and-move- ...

  7. C++/C++11中左值、左值引用、右值、右值引用的使用

    C++的表达式要不然是右值(rvalue),要不然就是左值(lvalue).这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能. 在C++语言中,二者的区别就没 ...

  8. C++右值引用与转移和完美转发

    C++右值引用与转移和完美转发 1.右值引用 1.1右值 lvalue 是 loactor value 的缩写,rvalue 是 read value 的缩写 左值是指存储在内存中.有明确存储地址(可 ...

  9. C++ lambda 捕获模式与右值引用

    lambda 表达式和右值引用是 C++11 的两个非常有用的特性. lambda 表达式实际上会由编译器创建一个 std::function 对象,以值的方式捕获的变量则会由编译器复制一份,在 st ...

最新文章

  1. 解决Myeclipse下Debug出现Source not found以及sql server中导入数据报错
  2. 【学习笔记】ABAP OOD设计模式 - 单例模式
  3. windows服务器双网卡链路聚合_基于windows server 2012的多网卡链路聚合实验设计与......
  4. 什么情况下会用到try-catch
  5. elasticsearch存储空间不足导致索引只读,不能创建
  6. 一个方便的颜色主题组件
  7. LayUI中select下拉框选中触发事件
  8. IDEA maven的安装与配置(超详细)
  9. Codeforces 1194B+1194D
  10. 抖音壁纸小程序怎么做?手把手教你0元拥有自己的壁纸小程序
  11. 超级表格PreA融资记
  12. Python进阶并发基础--线程,全局解释器锁GIL由来,如何更好的利用Python线程,
  13. 数仓开发之DWD层(一)
  14. C-素数回文数的个数
  15. 2019年第十届蓝桥杯C/C++ A组国赛赛后总结(北京旅游总结)
  16. 给定一个字符串计算式,计算结果
  17. 系列一:最全微商城营销36计!
  18. 推荐英语学习之奥巴马演讲视频
  19. 精研物理 格物致知(二)
  20. 时钟壁纸代码python_Python实现系统桌面时钟 | 学步园

热门文章

  1. 基于JAVA+SpringMVC+Mybatis+MYSQL的办公用品销售平台进销存系统
  2. 基于JAVA+SpringMVC+Mybatis+MYSQL的校园二手市场系统
  3. xpath以某个字符开始_XPATH简单使用
  4. CVE-2017-8046(Spring Data Rest RCE)
  5. 机器学习 -- 用户画像
  6. 【送书福利】第一次送书活动(总共10本)
  7. Apache模块管理
  8. SQL语句性能分析常用命令
  9. 程序员编程艺术:第三章续、Top K算法问题的实现
  10. Python之数据分析(算数平均值、加权平均值、最大值与最小值)