C++11出现的右值相关语法可谓是很多C++程序员难以理解的新特性,不少人知其然而不知其所以然,面试被问到时大概就只知道可以减少开销,但是为什么减少开销、减少了多少开销、什么时候用...这些问题也不一定知道,于是我写下了这篇夹带自己理解的博文,希望它对你有所帮助。

浅拷贝、深拷贝


在介绍右值引用等概念之前,可以先来认识下浅拷贝(shallow copy)和深拷贝(deep copy)。

这里举个例子:

  1. class Vector{
  2. int num;
  3. int* a;
  4. public:
  5. void ShallowCopy(Vector& v);
  6. void DeepCopy(Vector& v);
  7. };
  • 浅拷贝:按位拷贝对象,创建的新对象有着原始对象属性值的一份精确拷贝(但不包括指针指向的内存)。
  1. //浅拷贝
  2. void Vector::ShallowCopy(Vector& v){
  3. this.num = v.num;
  4. this.a = v.a;
  5. }
  • 深拷贝:拷贝所有的属性(包括属性指向的动态分配的内存)。换句话说,当对象和它所引用的对象一起拷贝时即发生深拷贝。
  1. //深拷贝
  2. void Vector::DeepCopy(Vector& v){
  3. this.num = v.num;
  4. this.a = new int[num];
  5. for(int i=0;i<num;++i){a[i]=v.a[i]}
  6. }

可以看到,深拷贝的开销往往比浅拷贝大(除非没有指向动态分配内存的属性),所以我们就倾向尽可能使用浅拷贝。

但是浅拷贝的有一个问题:当有指向动态分配内存的属性时,会造成多个对象共用这块动态分配内存,从而可能导致冲突。一个可行的办法是:每次做浅拷贝后,必须保证原始对象不再访问这块内存(即转移所有权),这样就保证这块内存永远只被一个对象使用。

那有什么对象在被拷贝后可以保证不再访问这块内存呢?相信大家心里都有答案:临时对象。

左值、右值


为了让编译器识别出临时对象,从而好做浅拷贝优化,于是C++引入了左值(lvalue)、右值(rvalue)的概念。

  • 左值:表达式结束后依然存在的持久对象。
  • 右值:表达式结束后就不再存在的临时对象。

之所以取名左值右值,是因为在等式左边的值往往是持久存在的左值类型,在等式右边的表达式值往往是临时对象。

  1. a = ++b;
  2. a = b+c*2;
  3. a = func();

更直观的理解是:有变量名、可以取地址的对象都是左值,没有变量名、不可以取地址的都是右值。(因为有无变量名意味着这个对象是否在下一行代码时依然存在)

右值引用类型


有了左值、右值的概念,我们就很清楚认识到右值都是些短暂存在的临时对象。

于是,C++11 为了匹配这些左右值类型,引入了右值引用类型 &&

右值引用类型负责匹配右值,左值引用则负责匹配左值。

因此刚刚的浅拷贝、深拷贝例子,我们可以无需显式调用浅拷贝或深拷贝函数,而是调用重载函数:

  1. //左值引用形参=>匹配左值
  2. void Vector::Copy(Vector& v){
  3. this.num = v.num;
  4. this.a = new int[num];
  5. for(int i=0;i<num;++i){a[i]=v.a[i]}
  6. }
  7. //右值引用形参=>匹配右值
  8. void Vector::Copy(Vector&& temp){
  9. this.num = temp.num;
  10. this.a = temp.a;
  11. }

当然,最标准还是编写成各种构造函数(拷贝构造、移动构造、赋值构造、移动赋值构造):

移动的意思是转移所有权。由于右值 大部分 都是临时的值,临时值释放后也就不再持有属性的所有权,因此这相当于转移所有权的行为。

  1. //拷贝构造函数:这意味着深拷贝
  2. Vector::Vector(Vector& v){
  3. this.num = v.num;
  4. this.a = new int[num];
  5. for(int i=0;i<num;++i){a[i]=v.a[i]}
  6. }
  7. //移动构造函数:这意味着浅拷贝
  8. Vector::Vector(Vector&& temp){
  9. this.num = temp.num;
  10. this.a = temp.a;
  11. temp.a = nullptr; //实际上Vector一般都会在析构函数来释放指向的内存,所以需赋值空地址避免释放
  12. }

虽然从优雅的实现深、浅拷贝这个目的开始出发,C++11的移动语义可以不止用于浅拷贝,得益于转移所有权的特性,我们还可以做其它事情,例如在右值所占有的空间临时存放一些东西。

强转右值 std::move


除了上面说的临时值,有些左值其实也很适合转移所有权:

  1. void func(){
  2. Vector result;
  3. //...DoSomehing with result
  4. if(xxx){ans = result;} //现在我希望把结果提取到外部的变量a上。
  5. return;
  6. }

可以看到result赋值给ans后就不再被使用,我们期望它调用的是移动赋值构造函数。

但是result是一个有变量名的左值类型,因此ans = result 调用的是赋值构造函数而非移动赋值构造函数。

为了将某些左值当成右值使用,C++11 提供了 std::move 函数以用于将某些左值转成右值,以匹配右值引用类型。

这也是移动语义的由来:无论是临时值还是被强转的左值,只要遵守转移所有权的保证,都可以使用移动语义。

  1. void func(){
  2. Vector result;
  3. //...DoSomehing with result
  4. if(xxx){ans = std::move(result);} //调用的是移动赋值构造函数
  5. return;
  6. }

重新审视右值引用


右值引用类型和右值的关系

有了上面的知识后,我们来重新审视一下右值引用类型。

先看看如下代码:

  1. void test(Vector& o) {std::cout << "为左值。" << std::endl;}
  2. void test(Vector&& temp) {std::cout << "为右值。" << std::endl;}
  3. int main(){
  4. Vector a;
  5. Vector&& b = Vector();
  6. //请分别回答:a、std::move(a)、b 分别是左值还是右值?
  7. test(a);
  8. test(std::move(a));
  9. test(b);
  10. }

答:a是左值,std::move(a)是右值,但b却是左值。

在这里b虽然是 Vector&& 类型,但却因为有变量名(即可持久存在),被编译器认为是左值。

  1. //即使函数返还值是临时值,但返还类型是左值引用类型,因此被认为是持久存在的左值。
  2. Vector& func1();
  3. //函数返还值为右值引用类型=>是短暂存在的右值。
  4. Vector&& func2();
  5. //函数返还值为正常类型=>是短暂存在的右值。
  6. Vector func3();

结论:右值引用类型只是用于匹配右值,而并非表示一个右值。因此,尽量不要声明右值引用类型的变量,而只在函数形参使用它以匹配右值。

实际上C++ std::move的实现原理就是的强转右值引用类型并返还之,由于函数返还值类型是临时值,且返还的还是右值引用类型(非左值引用类型),因此该返还值会被判断为右值。

函数参数传递

  1. void func1(Vector v) {return;}
  2. void func2(Vector && v) {return;}
  3. int main() {
  4. Vector a;
  5. Vector &b = a;
  6. Vector c;
  7. Vector d;
  8. //请回答:不开优化的版本下,调用以下函数分别有多少Copy Consturct、Move Construct的开销?
  9. func1(a);
  10. func1(b);
  11. func1(std::move(c));
  12. func2(std::move(d));
  13. }

实际上在不开优化的版本下,如果实参为右值,调用func1的开销只比func2多了一次移动构造函数和析构函数。

实参传递给形参,即形参会根据实参来构造。其结果是调用了移动构造函数;函数结束时则释放形参。

倘若说对象的移动构造函数开销较低(例如内部仅一个指针属性),那么使用无引用类型的形参函数是更优雅的选择,而且还能接受左值引用类型或无引用的实参(尽管这两种实参都会导致一次Copy Consturct)。

那我们在写一般函数形参的时候,有必要每个函数都提供关于&&形参的重载版本吗?

回答:一般来说是没必要的。对象的移动构造(赋值)函数开销不大时,我们可以只提供非引用类型和左值引用类型(避免Copy Construct)的重载版本,而不必编写右值引用类型的重载版本。

函数返还值传递

  1. Vector func1() {
  2. Vector a;
  3. return a;
  4. }
  5. Vector func2() {
  6. Vector a;
  7. return std::move(a);
  8. }
  9. Vector&& func3() {
  10. Vector a;
  11. return std::move(a);
  12. }
  13. int main() {
  14. //请回答:不开优化的版本下,执行以下3行代码分别有多少Copy Consturct、Move Construct的开销?
  15. Vector test1 = func1();
  16. Vector test2 = func2();
  17. Vector test3 = func3();
  18. }

同样的道理,执行这3行代码实际上都没有任何Copy Construct的开销(这其中也有NRV技术的功劳),都是只有一次Move Construct的开销。

此外一提,func3是危险的。因为局部变量释放后,函数返还值仍持有它的右值引用。

因此,这里也不建议函数返还右值引用类型,同前面传递参数类似的,移动构造开销不大的时候,直接返还非引用类型就足够了(在某些特殊场合有特别作用,例如std::move的实现)。

结论:我们应该把编写右值引用类型相关的任务放在对象的构造、赋值函数上,而非一般函数。从源头上出发,你就会发现在编写其它代码时就会自然而然享受到了移动构造、移动赋值的优化效果。

万能引用


接下来的内容都是属于模板的部分了:万能引用、引用折叠、完美转发。这部分更加难以理解,不编写模板代码的话可以绕道了。

万能引用(Universal Reference):

  • 发生类型推导(例如模板、auto)的时候,使用T&&类型表示为万能引用,否则表示右值引用。
  • 万能引用类型的形参既能匹配任意引用类型的左值、右值。

也就是说编写模板函数时,只提供万能引用形参一个版本就可以匹配左值、右值,不必编写多个重载版本。

  1. template<class T>
  2. void func(T&& t){
  3. return;
  4. }
  5. int main() {
  6. Vector a,b;
  7. func(a); //OK
  8. func(std::move(b)); //OK
  9. }

此外需要注意的是,使用万能引用参数的函数是最贪婪的函数,容易让需要隐式转换的实参匹配到不希望的转发引用函数。例如下面代码:

  1. template<class T>
  2. void f(T&& value);
  3. void f(int a);
  4. //当调用f(long类型的参数)或者f(short类型的参数),则不会匹配int版本而是匹配到万能引用的版本

引用折叠


使用万能引用遇到的第一个问题是推导类型会出现不正确的引用类型:例如当模板参数T为Vector&或Vector&&,模板函数形参为T&&时,展开后变成Vector& &&或者Vector&& &&。

  1. template<class T>
  2. void func(T&& t){
  3. return;
  4. }
  5. int main(){
  6. func(Vector()); //模板参数T被推导为Vector&&
  7. }

但显然C++中是不允许对引用再进行引用的,于是为了让模板参数正确传递引用性质,C++定义了一套用于推导类型的引用折叠(Reference Collapse)规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。

引用折叠 & &&
& & &
&& & &&

Example1:

  1. func(Vector());

模板函数func的T被推导为Vector&&,形参object为T&&即展开后为Vector&& &&。由于折叠规则的存在,形参object最终被折叠推导为Vector&&类型。

Example2:

  1. func(a);

模板函数func的T在这里被推导为Vector&,形参object为T&&即展开后为Vector& &&。由于折叠规则的存在,形参object最终被推导为Vector&类型。

完美转发 std::forward<T>


当我们使用了万能引用时,即使可以同时匹配左值、右值,但需要转发参数给其他函数时,会丢失引用性质(形参是个左值,从而无法判断到底匹配的是个左值还是右值)。

  1. //当然我们也可以写成如下重载代码,但是这已经违背了使用万能引用的初衷(仅编写一个模板函数就可以匹配左值、右值)
  2. template<class T>
  3. void func(T& t){
  4. doSomething(t);
  5. }
  6. template<class T>
  7. void func(T&& t){
  8. doSomething(std::move(t));
  9. }

完美转发(Perfect Forwarding):C++11提供了完美转发函数 std:forward<T> 。它可以在模板函数内给另一个函数传递参数时,将参数类型保持原本状态传入(如果形参推导出是右值引用则作为右值传入,如果是左值引用则作为左值传入)。

于是现在我们可以这样做了:

  1. template<class T>
  2. void func(T&& object){
  3. doSomething(std::forward<T>(object));
  4. }

不借助std::forward<T>间接传入参数的话,无论object是左值引用类型,还是右值引用类型,都会被视为左值。

std::forward<T>()的实现主要就一句return static_cast<T&&>(形参),实际上也是利用了折叠规则。从而接受右值引用类型时,将右值引用类型的值返还(返还值为右值)。接受左值引用类型时,将左值引用类型的值返还(返还值为左值)。

而std::move<T>()的实现还需要先移除形参的所有引用性质得到无引用性质的类型(假设为T2),然后再return static_cast<T2&&>(形参),从而保证不会发生引用折叠,而是直接作为右值引用类型的值返还(返还值为右值)。

透彻理解C++11新特性:右值引用、std::move、std::forward的更多相关文章

  1. C++ 新特性-右值引用

    作为最重要的一项语言特性,右值引用(rvalue references)被引入到 C++0x中.我们可以通过操作符“&&”来声明一个右值引用,原先在C++中使用“&”操作符声明 ...

  2. 【转】C++11 标准新特性: 右值引用与转移语义

    VS2013出来了,对于C++来说,最大的改变莫过于对于C++11新特性的支持,在网上搜了一下C++11的介绍,发现这篇文章非常不错,分享给大家同时自己作为存档. 原文地址:http://www.ib ...

  3. C++11 标准新特性: 右值引用与转移语义

    文章出处:https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/ 新特性的目的 右值引用 (Rvalue Referene) ...

  4. C++11中的右值引用及move语义编程

    C++0x中加入了右值引用,和move函数.右值引用出现之前我们只能用const引用来关联临时对象(右值)(造孽的VS可以用非const引用关联临时对象,请忽略VS),所以我们不能修临时对象的内容,右 ...

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

    C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导 ...

  6. C++11中的右值引用

    原文出处:http://kuring.me/post/cpp11_right_reference May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移 ...

  7. C++ 11 中的右值引用

    C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include &l ...

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

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了Copy Elision.RVO(包 ...

  9. 右值引用、move与move constructor

    http://blog.chinaunix.net/uid-20726254-id-3486721.htm 这个绝对是新增的top特性,篇幅非常多.看着就有点费劲,总结更费劲. 原来的标准当中,参数与 ...

随机推荐

  1. 关于js在一个固定的盒子里面拖拽的问题(包含临界值)

    回武汉打卡第三天,武汉加油,逆战必胜!今天我们一起分享一下js拖拽的问题. 当然实现拖拽方法是有很多的,下面简单讲一种方法,大致思路如下: 首先需要用到的事件主要有  onmousedown,onmo ...

  2. Java并发基础04. 线程技术之死锁问题

    我们知道,使用 synchronized 关键字可以有效的解决线程同步问题,但是如果不恰当的使用 synchronized 关键字的话也会出问题,即我们所说的死锁.死锁是这样一种情形:多个线程同时被阻 ...

  3. Gang Of Four的23中设计模式

    Gang Of Four的23中设计模式 标签(空格分隔): 设计模式 1. 根据目的来进行划分 根据目的进行划分可以分为创建型模式, 结构型模式和行为模式三种. 1.1 创建型模式 怎样创建对象, ...

  4. PTA数据结构与算法题目集(中文) 7-31

    PTA数据结构与算法题目集(中文)  7-31 7-31 笛卡尔树 (25 分)   笛卡尔树是一种特殊的二叉树,其结点包含两个关键字K1和K2.首先笛卡尔树是关于K1的二叉搜索树,即结点左子树的所有 ...

  5. Redis底层结构概述

    可以使用 object encoding <key> 查看使用的具体数据结构 原图链接

  6. Java 判断日期的方法

    //str:传入的日期 eg:"2018-07-23" function IsDate(str) { arr = str.split("-"); if(arr. ...

  7. 这个案例写出来,还怕跟面试官扯不明白 OAuth2 登录流程?

    昨天和小伙伴们介绍了 OAuth2 的基本概念,在讲解 Spring Cloud Security OAuth2 之前,我还是先来通过实际代码来和小伙伴们把 OAuth2 中的各个授权模式走一遍,今天 ...

  8. Linux服务器架设篇,DNS服务器(一),基础知识

    一.端口 DNS监听端口 注意: DNS通常是以UDP协议来进行数据传输协议的,但是若没有办法查询到完整的信息是.DNS的daemon是named,它会启动TCP和UDP的53端口,所以启用DSN服务 ...

  9. Tcl编程第三天,数学运算

    1.tcl语言没有自己的数学计算,如果想要使用数学公式,必须得用C语言的库.使用方法如下. #!/usr/bin/tclsh set value [expr 8/5] puts $value set ...

  10. SQL基础系列(2)-内置函数--转载w3school

    1.    日期函数 Mssql: SELECT GETDATE() 返回当前日期和时间 SELECT DATEPART(yyyy,OrderDate) AS OrderYear, DATEPART( ...