C++17下有无自定义拷贝构造函数时NRVO失效原因咨询
场景复现
我编写了如下演示代码:
#include <iostream> using namespace std; class Test { public: Test(); ~Test(); Test(const Test&); }; Test::Test(const Test& rhs) { cout << "do copy constructor function" << endl; } Test::Test() { cout << "do constructor function" << endl; } Test::~Test() { cout << "do deconstructor function" << endl; } Test fun1() { Test tmp; return tmp; // NRVO } int main() { Test t1 = fun1(); return 0; }
在VS2017 Release模式下运行上述代码,C标准设置为C17,运行结果符合预期:拷贝构造函数未被调用,NRVO正常生效。
但当注释掉自定义的拷贝构造函数后再次运行,代码如下:
#include <iostream> using namespace std; class Test { public: Test(); ~Test(); }; Test::Test() { cout << "do constructor function" << endl; } Test::~Test() { cout << "do deconstructor function" << endl; } Test fun1() { Test tmp; return tmp; // NRVO } int main() { Test t1 = fun1(); return 0; }
运行结果显示NRVO似乎失效:构造函数仅执行1次,但析构函数执行了2次。
原因解析
这个现象不是NRVO失效,是VS2017对可平凡复制类型的返回值做的特殊ABI优化导致的,具体逻辑如下:
- 当手动定义拷贝构造函数时,
Test属于非可平凡复制类型,VS2017会正常应用NRVO优化:直接在main函数栈帧为t1预留的内存地址上构造对象,整个过程不会产生fun1内的局部临时对象,也不会调用拷贝构造,最终构造、析构各执行1次,和观测结果一致。 - 当注释掉自定义拷贝构造函数后,编译器会自动生成隐式的可平凡复制拷贝构造函数。对于这类不需要执行任何额外逻辑的平凡类型,VS2017会选择效率更高的返回路径:不会做NRVO的地址透传,而是先在
fun1的栈帧内构造局部对象tmp,函数返回时直接通过CPU内存拷贝指令把tmp的内存数据逐字节复制到main栈帧中t1所在的内存位置。
这个逐字节拷贝的过程不会执行任何构造函数逻辑,所以全程只会观测到1次构造函数调用,也就是fun1内tmp的构造。 - 析构执行2次的原因非常直接:这个流程中实际存在两个独立生命周期的
Test对象:一个是fun1栈帧内的tmp,在fun1返回前会执行一次析构;另一个是main栈帧内通过内存拷贝得到的t1,在main函数结束前会执行第二次析构。两个对象都会调用你自定义的析构函数,所以最终会输出2次析构日志。
补充说明:这个实现是VS2017(v141工具集)的特定行为,不是C++标准的强制要求。对可平凡复制类型来说,直接内存拷贝的开销比NRVO的地址传递逻辑更低,编译器是出于性能考量选择了这个返回路径,不属于优化bug。
内容的提问来源于stack exchange,提问作者f1msch
相关产品推荐
相关产品推荐

