C++ 11 中的右值引用

右值引用的功能

首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能:

#include <iostream>
    #include <vector>
    using namespace std;

class obj
    {
    public :
        obj() { cout << ">> create obj " << endl; }
        obj(const obj& other) { cout << ">> copy create obj " << endl; }
    };

vector<obj> foo()
    {
        vector<obj> c;
        c.push_back(obj());

cout << "---- exit foo ----" << endl;
        return c;
    }

int main()
    {
        vector<obj> k;
        k = foo();
    }

首先我们编译一下这个函数,运行结果如下:

tianfang > g++ main.cpp
    tianfang > a.out
    >> create obj 
    >> copy create obj 
    ---- exit foo ----
    >> copy create obj 
    tianfang >

可以看到,对obj对象执行了两次构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机:

  1. foo函数第二行,调用push_back的时候,会在vector里建立一个obj的副本
  2. main函数第二行,执行复制函数的时候,会把foo()返回的对象全部复制过来,再次执行一次拷贝构造

由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。

tianfang > g++ -std=c++11 main.cpp
    tianfang > a.out
    >> create obj 
    >> copy create obj 
    ---- exit foo ----
    tianfang >

可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?

在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下:

  1. foo()函数返回一个临时对象(这里用~tmp来标识它)
  2. 执行vector的 '=' 函数,将对象k中的现有成员删除,将~tmp的成员复制到k中来
  3. 删除临时对象~tmp

在C++11的版本中,执行过程如下:

  1. foo()函数返回一个临时对象(这里用~tmp来标识它)
  2. 执行vector的 '=' 函数,将对象k中的成员~tmp的成员互换,此时k中的成员就被替换成了~tmp中的成员。
  3. 删除临时对象~tmp(此时就删除了以前的k中的成员)

关键的过程就是第2步,它不是复制而是交换,从而避免的成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。

但是,这份免费的午餐也不是无条件就可以获取的,带上-std=c++11编译时,如果使用STL代码可以享用这份午餐,但如果使用我们以前的老代码发现还是和以前的功能是一样的,那么,如何让我们以前的代码也能得到这个效率的提升呢?

通过交换减少数据的拷贝

为了演示如何在我们的代码中也获取这个性能提升,首先我先写了一个山寨的vector:

#include <iostream>
    #include <vector>
    using namespace std;

class obj
    {
    public :
        obj() { cout << ">> create obj " << endl; }
        obj(const obj& other) { cout << ">> copy create obj " << endl; }
    };

template <class T>
    class container
    {
    public:
        T* value;

public:
        container() : value(NULL) {};
        ~container() { delete value; }

container(const container& other)
        {
            value = new T(*other.value);
        }

const container& operator = (const container& other)
        {
            delete value;
            value = new T(*other.value);
            return *this;
        }

void push_back(const T& item)
        {
            delete value;
            value = new T(item);
        }
    };

container<obj> foo()
    {
        container<obj> c;
        c.push_back(obj());

cout << "---- exit foo ----" << endl;
        return c;
    }

int main()
    {
        container<obj> k ;
        k = foo();    
    }

这个vector只能容纳一个元素,但并不妨碍我们的演示,其功能和前面的例子是一样的,运行这段代码,结果如下:

tianfang > make
    g++ -std=c++11 main.cpp
    tianfang > a.out
    >> create obj 
    >> copy create obj 
    ---- exit foo ----
    >> copy create obj 
    tianfang >

如前所述,仍然有两次拷贝构造。其实前面已经说过交换实现减少拷贝构造的原理,那么,我们可以通过修改 '=' 函数来手动实现这一过程。

const container& operator = (container& other)
    {
        T* tmp = value;
        value = other.value;
        other.value = tmp;
        return *this;
    }

在VC中运行这段代码,发现运行结果和预期一致,

>> create obj 
    >> copy create obj 
    ---- exit foo ----

但是,gcc中却无法通过编译,原因很简单:gcc期望的赋值函数的参数是const型的,而这里为了交换成员,而不能使用const型。

那么,虽然gcc中不能生效,是否可以说在vc中就可以以这种形式获取性能提升呢?答案是否定的。虽然在这段代码中这么写没有问题,但赋值函数本身是期望复制功能的,而不是交换。例如,修改后下面的运行结果就不对了。

int main()
    {
        container<obj> k, k2;
        k = foo();

//预期结果是复制,但执行了交换
        k2 = k;
    }

gcc的告警是有道理的:如果 '=' 函数实现的是复制功能,虽然效率低点,但保证了功能正确,但如果实现的是交换的功能,则不能保证功能一定正确。只有当 '=' 函数右边的对象为一个临时变量的时候,由于临时变量会马上被删除掉,此时的交换和复制的效果是一样的。其实VC也应该把这个告警加上才合适。

PS:对临时变量定义和来源不清楚的朋友可以参考一下这篇文章。

现在的问题是:我们无法在赋值函数里区分传入的是一个临时对象还是非临时对象,因此只能执行复制操作。为了解决这一问题,c++中引入了一个新的赋值函数的重载形式:

container& operator = (container&& other)

这个赋值函数通常称为移动赋值函数,和老版本的相比,它有两点区别:

  1. 入参不是const型,因此它是可以更改入参的值的,从而实现交换操作
  2. 入参前面有两个&号,这个是C++11引入的新语法,称为右值引用,它的使用方式和普通引用是一样的,唯一的区别是可以指向临时变量。

现在,我们就有两个版本的赋值函数了,C++11在语法级别也做了适应:

  • 如果入参是临时变量,则执行移动赋值函数,如果没有定义移动赋值函数,则执行复制赋值函数(以保证老版本代码能编译通过)
  • 如果入参不是临时变量,则执行普通的复制赋值函数

现在,我们实现一下山寨版的移动赋值函数:

container& operator = (container&& other)
    {
        delete value;
        value = other.value;
        other.value = NULL;
        return *this;

}

运行后结果就和我们期望的那样,避免了成员的第二次的拷贝构造。

和移动赋值函数相应的,也有一个一个移动构造函数,也最好实现以下:

container (container&& other)
    {
        value = other.value;
        other.value = NULL;
    }

我们也可以实现自己的右值引用版的重载函数,这里就不多介绍了。

注意:本文所示的代码只是为了演示和实现右值引用,力求简洁,并没有写得很完善(一个典型的缺失就是在赋值函数中没有判断入参是否是本身),请不要将其应用于项目中。

完善的版本请看MSDN文章:如何编写一个移动构造函数,其相应的对右值引用的介绍文章Rvalue引用声明:&&也非常值得一读。

通过std::move函数显式使用交换

首先看一下这段代码:

class bigobj
    {
    public :
        bigobj() { cout << ">> create obj " << endl; }
        bigobj(const bigobj& other) { cout << ">> copy create obj " << endl; }
        bigobj(bigobj&& other) { cout << ">> move create obj " << endl; }
    };

int main()
    {
        list<bigobj> list;
        for(int i = 0; i < 3; i++)
        {
            bigobj obj;
            list.push_back(obj);
        }
    }

运行的时候就会发现:虽然我们定义了移动构造函数,但是它仍然会执行拷贝构造函数。这是因为编译器并不认为obj是临时变量。关于什么变量才是临时变量,前文已经给了个链接来说明它,简单的说,我们能够看到的命名变量都不是临时变量。

虽然obj对象不是语言级别的临时变量,但是从功能上来看,它就是一个临时变量,是可以使用移动构造函数来消除拷贝带来的性能损失的。为了解决这一问题,C++提供了一个move函数来把obj变量强制转换为右值引用,这样就可以使用移动构造函数了。

for(int i = 0; i < 3; i++)
    {
        bigobj obj;
        list.push_back(std::move(obj));
    }

不过,需要注意的是,和系统识别的临时变量而自动使用右值引用不同,这种强制转换是有一定的风险的,由于在push_back后执行了交换操作,如果再次使用它会出现非预期的结果,只有能确定该变量不会再次被使用才能执行这种转换。

C++ 11 中的右值引用相关推荐

  1. C++11中的右值引用(对比左值引用和常引用)、移动构造函数和引用标识符

    Hello!各位同学们大家好!逗比老师最近说起来还是挺尴尬的,为什么这么说呢?因为以前我对自己的C++水平还是相当自信的,经常以"精通"来自我评价.但是最近发现自己好像对C++11 ...

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

    http://www.cnblogs.com/yanqi0124/p/4723698.html 在C++98中有左值和右值的概念,不过这两个概念对于很多程序员并不关心,因为不知道这两个概念照样可以写出 ...

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

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

  4. C++11标准之右值引用(ravalue reference)

    C++11标准之右值引用(ravalue reference) 1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具 ...

  5. C++11特性《 右值引用-<完美转发>、lambda表达式》

    1.右值引用 1.1移动语义 如果一个类中涉及到资源管理,用户必须显式提供拷贝构造.赋值运算符重载以及析构函数,否则编译器将 会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如 ...

  6. C++ C++11新特性--右值引用

    左值与右值 在C语言中,左值和右值一般有两种区分的方法.可以出现在赋值符号"="的两边的值为左值,只能出现在赋值符号"="的右边的值为右值:还有一种说法是能取地 ...

  7. C++11新特性 右值引用与移动语义

    右值引用作用是可以减少内存拷贝次数,从而优化性能. 首先,什么是右值?右值是一个与左值相区分的概念.左值是:既能出现在等号左边也能出现在等号右边的变量或表达式,比如int a = 5,那么a就是一个左 ...

  8. (原创)C++11改进我们的程序之右值引用

    http://www.cnblogs.com/qicosmos/p/3369940.html 本次主要讲c++11中的右值引用,后面还会讲到右值引用如何结合std::move优化我们的程序. c++1 ...

  9. C++11 移动语义与右值引用

    1.移动语义 C++11 新标准中一个最主要的特性就是提供了移动而非拷贝对象的能力.如此做的好处就是,在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升程序性能.参考如下程序: ...

最新文章

  1. latex如何设置字体并加粗_如何设置微信昵称字体大小加粗变斜???
  2. Makefile完全解析PART5.使用变量
  3. AcDream 1079 郭氏数
  4. 位居全国第一- 丰收节交易会·内蒙古:名特优新农产品数量
  5. python中几种推导式的特殊用法
  6. extern 用法,全局变量与头文件(重复定义)
  7. 架构师是大忽悠吗?阿里技术大牛告诉你真相!
  8. 复杂风控场景下,如何打造一款高效的规则引擎
  9. 【java】java wait 原理 synchronized ReentrantLock 唤醒顺序
  10. ArcGIS AddIN异常:无法注册程序集 未能加载文件或程序集ESRI.ArcGIS.Desktop.Addins
  11. Linux服务器安装JavaWeb环境(三) RocketMQ,Nacos
  12. linux7配置iptables配置转发,Centos7安装iptables及配置
  13. HDU 5586 Sum (预处理 + 动态规划)
  14. 【转】8段数码管引脚图,8段数码管动态显示详解
  15. latex模板中 引入ORCID链接的方法
  16. dell r710重装系统_dell服务器安装系统指南
  17. 程序员最喜欢的15款文本编辑器推荐
  18. 【好文分享】提升早晨工作效率的小tips
  19. 从网络中获取债券收益率数据
  20. 项目开发流程_以房地产项目总的视角,谈谈项目开发流程

热门文章

  1. 考研数学(180°为什么等于π)
  2. mooc哈尔滨c语言作业答案,哈尔滨工业大学C语言2016年MOOC在线测试答案.doc
  3. netpref 使用_使用PerfView监测.NET程序性能(转发)
  4. agv系统介绍_AGV地面控制系统介绍
  5. freebsd java 能用吗_在FreeBSD 4.9下安装JAVA环境
  6. 一页纸项目管理模板_项目管理职场必备读物!这一次全部送给你!
  7. c#养老院老人信息管理系统源码 论文_辽宁吃的好的社区养老院位置,爱心养老院_抚顺市望花区社会养老中心...
  8. java多线程交替打印_java实现多线程交替打印
  9. c语言出圈游戏课设报告,c语言作业 出圈游戏
  10. java 2d划线 刷子_月光软件站 - 编程文档 - Java - Java图形设计中,利用Bresenham算法实现直线线型,线宽的控制(NO 2D GRAPHICS)...