拷贝消除(Copy elision)
拷贝消除(Copy elision)是一种编译器优化技术,消除了对象不必要的对象复制。
在使用C++语言时,返回值必须是 int
、bool
、枚举或指针等轻量级原生(lightweight primitive)数据类型。如果确实需要返回大型数据,使用引用或指针作为输出参数返回,而不是通过return
语句返回。
纯右值参数复制构造
同类型纯右值复制构造,复制省略,直接在最新对象位置上进行构造,即使在复制构造函数中存在副作用,也不会进行调用。
RVO (Return value optimization RVO)
返回值优化RVO是一种编译器优化技术,在接收对象的位置构造返回值,避免从函数返回时创建临时对象。到了C++17标准保证了函数返回临时对象不会被复制,而不再是依赖于编译器优化.
编译器遇到可以RVO优化的时候,会在调用函数前先为返回值分配好地址,后调用函数,在分配好的地址上初始化构造返回值。
RVO相对于不同版本编译器和C++标准的区别
1、C++17 标准保证返回临时对象不会发生拷贝
C++17 引入了强制复制省略技术。对于临时对象,无论是通过值传递参数,还是通过值返回时,都不会调用拷贝构造函数。
例如,当函数返回一个临时对象时,编译器会强制进行返回值优化(RVO),直接在调用端的栈帧空间构造对象(在调用之前就先预留了返回值的地址空间),避免了拷贝和移动操作。但是,具名临时变量的返回在 C++17 中不支持强制复制省略
2、C++17 标准无论移动构造和移动赋值是否被删除都会进行 RVO 优化
在 C++17 之前,虽然编译器会尝试进行 RVO 优化,但如果类中禁止编译器默认生成拷贝构造和移动构造函数,代码将不会被编译通过,因为编译器需要检查 copy/move 构造函数是否可用,即使实际执行中可能不需要调用它们。
而在 C++17 标准下,对于临时对象(prvalue)作为返回值的情况,会强制进行 RVO 优化,此时无论移动构造和移动赋值是否被删除,通常都能正常编译并实现优化。不过,若返回的是具名变量,仍可能需要考虑移动构造函数等是否存在。
3、 禁止编译进行 RVO 优化
在 C++17 之前,GCC 默认会开启具名返回值优化等 RVO 优化。通过添加-fno-elide-constructors
编译参数,可以禁止编译器进行复制省略相关的优化,使得代码在编译时按照标准检查拷贝构造函数和移动构造函数等是否可用,若不存在则会编译报错。而在 C++17 及以后,即使使用了-fno-elide-constructors
,对于符合条件的临时对象返回,编译器依然会强制进行 RVO 优化。
NRVO(Name Return value optimization)
NRVO原理
NRVO与RVO类似,但适用于返回函数内部已命名的局部变量。编译器优化这个过程,允许在调用者的栈帧上直接构造局部变量,避免了将局部变量拷贝到返回值的过程,但是NRVO并不能保证每次都会进行优化,在有一些情况不会发生,不同编译器情况也不太一样,依赖于编译器实现。
MyType return_name_value() {
MyType x; //返回值具有名
return x;
}
NRVO不生效的情况
1、运行时依赖
根据不同的条件分支,返回不同的变量。在不同的编译器下,实现的优化策略也是不一样的,能否对两个分支都进行NRVO优化,依赖于编译器的实现,这里会涉及到一部分操作系统反汇编的知识,我也还需要再去复习巩固。
例如:
#include <cstdio>
#include <atomic>
struct MyType {
char buffer[100000];
};
MyType return_name_value(bool test) {
if (test) {
MyType x;
x.buffer[0] = '\0';
return x;
} else {
MyType y;
return y;
}
}
int main()
{
auto x = return_name_value(true);
return 0;
}
2、返回函数参数
返回函数参数不会进行NRVO优化,原因是函数参数的控制权和生命周期在函数内部,随着函数的结束而结束
3、返回值是全局变量也不会优化
因为全局变量的生命周期随着整个程序的,因此即使像RVO那样,预留返回值的内存空间,返回时依旧需要对全局变量进行拷贝
4、若局部对象可能适用于返回值优化,不能对其做std::move或者std::forward
std::move()
将返回值转换成对象的引用,但是RVO的条件强调的是返回值,在关于RVO标准中:当RVO的前提条件允许时,要么发生复制省略,要么std::move隐式地实施于返回的局部对象上.因此对于局部对象可用于RVO优化的,不必添加move操作.