• C++中的CopyElision


    函数返回对象的Copy Elision处理

    以前C++函数返回一个对象的时候,涉及到对象的拷贝,为了提高效率,加入了copy elision的优化设计。

    假如我们有一个Vector类,表示向量。

    在C++的设计中,函数返回对象的时候其实返回的是对象的一份拷贝,然后用这个拷贝的对象去给外部使用。但从实际使用角度来说,生成的中间拷贝对象明显是不需要的,所以各大编译器在支持C++的时候,想出了一个优化方法,copy elision,顾名思义,拷贝消除,就是消除中间的拷贝过程,直接建立起数据桥梁!

     这些优化策略或称之为NRVO,named return value optimizatioin,或称之为RVO,return value optimation。二者的区别是,前者返回的变量具名(有名字),后者不具名(没名字)。如下面的两个函数,GetValue1返回a,a有名字,GetValue2返回Vector(),没有名字。

    1. Vector GetValue1()
    2. {
    3. Vector a;
    4. return a;
    5. }
    6. Vector GetValue2()
    7. {
    8. return Vector();
    9. }

    不管具名还是不具名,它们都有一个相同的行为:为了返回对象给外部使用,需要拷贝一个中间的临时变量,这一步需要被优化消除掉,return value optimization,简称RVO,有名字的加个named前缀,叫做NRVO。

    这种优化策略,能够带来很大的性能提升,但也有缺陷:如果一个类不支持拷贝(或者移动,C++11带来的新操作,下文直接说拷贝构造,不再赘述移动),就不能被copy elision!

    啥?还有这种要求?

    因为它们的目的是copy elision,所以本质上是对拷贝的处理,优化的前提是代码符合语言标准,而C++17之前,C++认为,这种return Vector()情况,因为涉及到临时变量的拷贝,所以Vector必须提供拷贝构造函数,否则类的设计是有问题的。所以C++17之前,RVO成立的前提是class有拷贝或者移动构造函数才行,否则编译报错!

    而这种return Vector()代码,明眼人一看就知道,其实是可以不需要拷贝构造的嘛,优化之后,直接以Vector里面的()(这里是空)为参数在外部地址进行默认构造就行了,为啥一定要class提供拷贝构造呢?

    于是C++17带来了更新,对于return Vector()这种情况,强制RVO,此时RVO的处理从编译器级别提升到了语言级别,中间没有任何临时对象的产生,因为没有临时对象产生了,当然也就不再需要class提供拷贝(移动)构造接口了!所以不会编译报错。又因为没有临时对象,所以就不存在拷贝,当然也就不存在什么copy elision的说法了!非常棒的改进!这也是C++官方网站“Return value optimization is mandatory and no longer considered as copy elision”的含义。

    这意味着,我们以后看到return Vector()这种代码的时候,可以放心地认为,“返回过程没有临时对象的产生”!(不管项目使用的标准是C++17之前的还是之后的,都没有临时对象产生,C++17之前临时对象被copy elision优化掉了,C++17之后则压根就没什么临时对象)。

    RVO的情况说了,NRVO呢?

    这不太一样,因为NRVO里面一定是有一个中间对象的,就是所谓的named value,如果class没有拷贝构造函数,NRVO就不能工作!

    为什么?为什么RVO可以不需要拷贝构造函数,NRVO就必须要拷贝构造函数?

    RVO的情况,我们可以直接把Vector()这样的数据构造到外部的变量地址里面,比如

    1. Vector GetValue2()
    2. {
    3. return Vector();
    4. }
    5. Vector v = GetValue2();

    最后的return Vector()直接就用Vector()作为参数,在v的地址里构造了对象。所以没有拷贝构造函数也OK!这种情况,只涉及到普通构造函数,不涉及拷贝构造函数。

    但是下面的代码中,

    1. Vector GetValue1()
    2. {
    3. Vector a;
    4. return a;
    5. }
    6. Vector v = GetValue1();

    GetValue1()返回的是a,即使进行copy elision优化,外界所知的也只是a而已,要用a去给v构造,Vector必须要有拷贝构造函数!

    所以NRVO的本质还是copy elision,这里的copy指的是中间copy出的临时对象,不是拷贝构造函数的copy constructor。只不过因为copy elision的存在,我们同样可以为,return a返回的过程中也不会有临时对象(被优化掉了)!

    发展到C++17,RVO和NRVO,虽然只有一字之差,但已经是不同的东西了。

    RVO:不依赖拷贝构造函数的直接返回。

    NRVO:依赖拷贝构造函数的copy elision。

    拷贝构造函数中的Copy Elision处理

    上面讨论了RVO NRVO的前因后果,NRVO只是copy elision的其中一个应用场景。copy elision还有一个典型的应用场景是构造函数的嵌套。

    下面这行代码

    	Vector v = Vector(Vector(Vector()));
    

    我们明显可以看出,多层的拷贝构造是不必要的,所以实际上最后也只调用了一个构造函数。和RVO一样,C++17之前,还叫做copy elision,因为它本质就是对于copy对象的消除,所以依赖拷贝构造函数。

    C++17之后,它本质上已经不是copy elision了,当然也就不再依赖拷贝构造函数,这种情形的进化和RVO是一个道理。

    但为了称呼上的方便,继续叫它copy elision,虽然不准确,但能直观表达出没有中间对象的意图,无伤大雅,知道其中原理就行了!

    让我们看下面这个有趣的例子

    1. class Vector
    2. {
    3. public:
    4. Vector() {cout << " default constructor" << endl;}
    5. Vector(const Vector& Other) = delete;
    6. Vector(Vector&& Other) = delete;
    7. friend const Vector operator + (const Vector& v1, const Vector& v2)
    8. {
    9. ...
    10. }
    11. };
    12. Vector a0;
    13. Vector a1;
    14. Vector a2 (a0 + a1);

    在C++17中,Vector a2a(a0+a1)竟然能通过,要知道Vector的拷贝构造和移动构造都被标记为delete了,为什么这种看似拷贝构造的行为还能通过呢?原因就在于a0+a1返回的是一个临时变量,是一个右值,这在C++17中被认为是要被直接优化掉的,所以就直接在a2的地址里面构造了a0+a1。但下面这行代码却不行

    Vector a2 (a0);

    因为a0不是一个右值,不满足条件,所以调用的是拷贝构造,但拷贝构造又被标记为了delete,所以编译报错。

  • 相关阅读:
    Java线程池-异步任务编排
    20.8 OpenSSL 套接字SSL传输文件
    【Kafka】flinkProducer kafka分区策略及kafka 默认分区策略
    平均分(C++)
    前端补充19
    DOM系列之classList属性
    压测——总结
    【Spring】静态代理
    【无标题】
    GET和POST的区别及使用场景?
  • 原文地址:https://blog.csdn.net/iaibeyond/article/details/126070889