C++ 11 中的右值引用

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 = (containerother)
    {
        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 bigobjother) { 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后执行了交换操作,如果再次使用它会出现非预期的结果,只有能确定该变量不会再次被使用才能执行这种转换。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/509630.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LinkedHashSet类

List接口: 允许元素重复,记录先后添加顺序. Set接口: 不允许元素重复,不记录先后添加顺序. 需求: 不允许元素重复,但是需要保证先后添加的顺序. LinkedHashSet:底层才有哈希表和链表算法. 哈希表:来保证唯一性,.此时就是HashSet,在哈希表中元素没有先后顺序. 链表: 来记录…

数据结构实验之串一:KMP简单应用

题目描述 给定两个字符串string1和string2&#xff0c;判断string2是否为string1的子串。输入 输入包含多组数据&#xff0c;每组测试数据包含两行&#xff0c;第一行代表string1(长度小于1000000)&#xff0c;第二行代表string2&#xff08;长度小于1000000&#xff09;&#…

Canal Mysql binlog 同步至 ElasticSearch 详细介绍

文章目录数据同步ElasticSearch单表基本配置适配器映射文件详细介绍&#xff08;单表、多表映射介绍&#xff09;单表映射索引示例sql单表映射索引示例sql带函数或运算操作多表映射(一对一, 多对一)索引示例sql多表映射(一对多)索引示例sql其它类型的sql示例注意事项本文详细介…

基于C++11的线程池

背景 在传统的收到任务即创建线程的情况下&#xff0c;我们每收到一个任务&#xff0c;就创建一个线程&#xff0c;执行任务&#xff0c;销毁线程&#xff0c; 我们把这三个过程所用的时间分别记做T1,T2,T3 任务本身所用的时间仅占T2/(T1T2T3),这在任务本身所用时间很短的情况下…

集合的工具类

集合操作的工具类: 1):Arrays类: 2):Collections类. Arrays类: 在Collection接口中有一个方法叫toArray把集合转换为Object数组. 把集合转换为数组: Object[] arr 集合对象.toArray(); 数组也可以转换为集合(List集合): public static List asList(T… a) 等价于public …

Docker入门到精通开发指南(一文搞懂)

文章目录安装官方安装文档具体安装步骤1.卸载之前的版本(如果之前未用过忽略该步骤)2.安装相关依赖3.设置docker镜像4.安装docker安装latest版本指定版本安装5.启动docker6.查看docker版本7.运行一个docker hello world8.卸载docker9.配置阿里云镜像加速地址docker常用命令dock…

数据结构实验之串二:字符串匹配

题目描述 给定两个字符串string1和string2&#xff0c;判断string2是否为string1的子串。 输入 输入包含多组数据&#xff0c;每组测试数据包含两行&#xff0c;第一行代表string1&#xff0c;第二行代表string2&#xff0c;string1和string2中保证不出现空格。(string1和strin…

Unicode和Utf-8

很久很久以前&#xff0c;有一群人&#xff0c;他们决定用8个可以开合的晶体管来组合成不同的状态&#xff0c;以表示世界上的万物。他们看到8个开关状态是好的&#xff0c;于是他们把这称为”字节“。再后来&#xff0c;他们又做了一些可以处理这些字节的机器&#xff0c;机器…

如何向Maven中央仓库提交自己的Jar包(发布自己的Jar包到中央仓库)

文章目录注册账号GPG 安装安装生成密钥上传公钥Maven配置上传到Maven仓库修改项目的配置&#xff0c;填写基本信息执行编译命令登录网站配置发布项目中应用遇到的问题解决方法本文将介绍如何将自己的jar包发布至公共的中央仓库&#xff0c;通过maven方式进行引用 注册账号 注册…

数据结构实验之串三:KMP应用

题目描述 有n个小朋友&#xff0c;每个小朋友手里有一些糖块&#xff0c;现在这些小朋友排成一排&#xff0c;编号是由1到n。现在给出m个数&#xff0c;能不能唯一的确定一对值l和r(l < r)&#xff0c;使得这m个数刚好是第l个小朋友到第r个小朋友手里的糖块数&#xff1f;输…

C++读写锁

读写锁实际是一种特殊的自旋锁&#xff0c;它把对共享资源的访问者划分成读者和写者&#xff0c;读者只对共享资源进行读访问&#xff0c;写者则需要对共享资源进行写操作。 读写锁实际是一种特殊的自旋锁&#xff0c;它把对共享资源的访问者划分成读者和写者&#xff0c;读者只…

List和Set以及Map的选用

选用哪一种容器取决于每一种容器的存储特点以及当前业务的需求: List: 单一元素集合. 允许元素重复/记录元素的添加顺序. Set:单一元素集合. 不允许元素重复/不记录元素的添加顺序. 既要不重复,又要保证先后顺序:LinkedHashSet. Map: 双元素集合. 如果存储数据的时候,还得…

FastDFS文件服务器安装

文章目录环境准备使用的系统软件磁盘目录安装libfastcommon安装FastDFS安装fastdfs-nginx-module安装nginx单机部署tracker配置storage配置client测试配置nginx访问分布式部署tracker配置storage配置client测试配置nginx访问启动防火墙trackerstoragenginx检测集群说明配置文件…

学密码学一定得学程序

题目描述 曾经&#xff0c;ZYJ同学非常喜欢密码学。有一天&#xff0c;他发现了一个很长很长的字符串S1。他很好奇那代表着什么&#xff0c;于是神奇的WL给了他另一个字符串S2。但是很不幸的是&#xff0c;WL忘记跟他说是什么意思了。这个时候&#xff0c;ZYJ不得不求助与伟大的…

boost::shared_mutex

前一篇介绍的条件变量可以进行进程间通信&#xff0c;用来实现生产者/消费者模型。今天介绍的共享互斥量用来实现缓冲区读写模型&#xff0c;与生产者/消费者模型不同的地方是&#xff0c;消费者消费产品后&#xff0c;被消费的产品就不存在了&#xff0c;所以消费者线程也要互…

Map集合类

映射的数学解释: 设A、B是两个非空集合&#xff0c;如果存在一个法则f&#xff0c;使得对A中的每个元素a&#xff0c;按法则f&#xff0c;在B中有唯一确定的元素b与之对应&#xff0c;则称f为从A到B的映射&#xff0c;记作f&#xff1a;A→B。 映射关系(两个集合):A集合和B集…

github 国内加速镜像

GitHub 镜像访问 这里提供两个最常用的镜像地址&#xff08;别登录账号&#xff09;&#xff1a; https://github.com.cnpmjs.org https://hub.fastgit.org 也就是说上面的镜像就是一个克隆版的Github&#xff0c;你可以访问上面的镜像网站&#xff0c;网站的内容跟Github是完…

数据结构实验之二叉树二:遍历二叉树

题目描述 已知二叉树的一个按先序遍历输入的字符序列&#xff0c;如abc,,de,g,,f,,, (其中,表示空结点)。请建立二叉树并按中序和后序的方式遍历该二叉树。 输入 连续输入多组数据&#xff0c;每组数据输入一个长度小于50个字符的字符串。 输出 每组输入数据对应输出2行&#x…

Socket select模型

Windows socket select模型开发。 套接字select模型是一种比较常用的IO模型。利用该模型可以使Windows socket应用程序可以同时管理多个套接字。 使用select模型&#xff0c;可以使当执行操作的套接字满足可读可写条件时&#xff0c;给应用程序发送通知。收到这个通知后&#x…