再谈 Copy Elision, RVO, NRVO

一直在各种文章中看到RVO 和 NRVO 以及 Copy Elision,但是一直是只知道大概意思,却没有一个真正的定义,最近又再次被这个问题困惑,仔细理解了cppreference上的相关内容(没有去读标准原文),下面做一些总结。

Copy Elision 拷贝消除

就是一种拷贝优化技术的总称,RVO 和 NRVO 都属于 Copy Elision

但是从C++11开始,返回语句中编译器在无法进行拷贝消除时,对左值的强制调用move constructor的行为不属于此概念范畴,这是在拷贝消除满足条件却未发生作用时,编译器的另一种优化行为

拷贝消除的几种常见情形

在这里为了清晰起见,我把拷贝消除的发生分为三种情形:

C++17之前:

1、使用临时对象初始化时:

在对对象进行初始化时,当表达式是一个同一类型(忽略CV限定符)的无名临时对象时,举例如下:

我们先建立一个类A便于测试

class A
{
public:
    A()
    {
        cout << "constructor" << endl;
    }
    ~A()
    {
        cout << "destructor" << endl;
    }
    A(const A& other)
    {
        cout << "copy constructor" << endl;
    }
    A& operator=(const A& other)
    {
        cout << "copy assignment" << endl;
        return *this;
    }
    A(A&& other)
    {
        cout << "move constructor" << endl;
    }
    A& operator=(A&& other)
    {
        cout << "move assignment" << endl;
        return *this;
    }
};

  执行如下语句:

A a = A();

此时只会调用A的Constructor一次,因为发生了拷贝消除。

如果在gcc中通过-fno-elide-constructors 选项关闭拷贝消除优化后,可以发现该语句分别调用Constructor --> Copy/Move Constructor  --> Destructor 

constructor
move constructor
destructor

2、在返回语句中返回临时对象时(RVO):

严格来说,该种情况和第一种情况是相同的,因为相当于使用返回临时对象初始化本地临时对象,但是由于这种情况就是常说的RVO(Return Value Optimization,返回值优化),所以单独列出来,示例如下:

A F()
{
    return A();
}

A a = F();

此时如果允许拷贝消除,则只会调用A的Constructor一次,因为拷贝消除之间是可以发生链式作用的。其过程应该为:

1、通过返回语句的返回临时对象初始化本地临时对象(触发本条款中的拷贝消除条件,其实也同时满足条款1中的条件,所以说条款2严格来说是属于条款1)

2、通过本地临时对象初始化对象a(触发条款1中的消除条件)

同样的,当我们使用-fno-elide-constructors 选项后,所有拷贝消除都不会发生,此时调用情况如下:

constructor        //生成返回语句中的临时对象
move constructor   //返回临时对象拷贝到本地临时对象
destructor
move constructor   //本地临时对象拷贝到对象a
destructor
destructor

当然,上述两种优化并不是强制要求编译器必须实现的,但是目前我也不知道哪个编译器没有实现这些特性,可能早期版本的部分编译器更有可能

C++17之后:

由于C++17中,纯右值和临时对象的定义发生了本质上的改变,简单来说,此时可以把纯右值理解为一个符号,不会再轻易生成实体,所以编译器被强制要求执行上述两种行为,由于此时已经无关优化,因此这两种行为也不再属于拷贝消除的优化行为。

因此在C++17以上标准中,即使使用-fno-elide-constructors 选项,也不会再有任何效果

所有C++版本适用:

3、在返回语句中返回具有自动存储期的对象时(NRVO):

具体来说:在返回语句中,表达式是一个拥有自动存储期的 non-volatile 对象,并且它不能是该函数的参数或 catch 语句的参数,如果其类型(忽略cv限定符)与函数返回值类型一致。

此种情况也被称作 NRVO(Named Return Value Optimization,命名返回值优化)

示例如下:

A NF()
{
    A na;
    return na;
}

A a = NF();

cout << __cplusplus << endl;

此时na是一个在栈上构造的具有自动存储期的对象,当使用其做为返回值时,触发NRVO优化,除此之外,同时发生了和2中情形相同的链式消除,因此只会调用一次Constructor

同样,如果使用-fno-elide-constructors 选项后,所有拷贝消除都不会发生,在C++11下调用情况如下:

constructor      //生成na
move constructor //使用na初始化本地临时对象
destructor
move constructor //使用本地临时对象初始化a
destructor
201103
destructor

在C++17下开启-fno-elide-constructors 选项后,结果如下

constructor      //生成na
move constructor //使用na初始化a
destructor
201703
destructor

由此可见,NRVO是所有版本都适用的一种拷贝消除优化情形,当然NRVO也不是强制要求实现的,在msvc的debug模式中就默认没有使用NRVO

除此之外,NRVO的应用条件也受到限制,在有多个return语句返回不同对象的情形中,NRVO不会发生作用,示例如下:

A NF1()
{
    A a;
    A b;
    return a;
    return a;
}
A a1 = NF1();

A NF2()
{
    A a;
    A b;
    return a;
    return b;
}
A a2 = NF2();

cout << __cplusplus << endl;

在C++17默认开启NRVO情形下,结果如下:

constructor
constructor
destructor

constructor
constructor
move constructor
destructor
destructor

201703
destructor
destructor

 很显然,在第二种情形下,NRVO没有发生作用,即使返回值是编译器可以确定的

拷贝消除的其他情形

除了上述几种常见情形,还有些情况可能涉及拷贝消除发生,包括在异常处理中以及c++20的协程语义下,具体可以参考cppreference相关文章:

https://en.cppreference.com/w/cpp/language/copy_elision

总结

拷贝消除情形:

1、使用临时对象初始化时

2、临时对象做为返回值时,又称作RVO

3、具有自动存储期对象做为返回值时,又称作NRVO

其中1,2不适用于C++17及以上版本,3适用于所有版本

posted on 2022-01-29 10:36  二十一级厨子  阅读(434)  评论(0编辑  收藏  举报

导航