再谈 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适用于所有版本