具名返回值优化(NRV)
具名返回值优化(NRV)
最近在看《深度探索C++对象模型》,在第2.3节中提到了具名返回值优化(Named Return Value optimization, NRV),如下:
#include <iostream> class Test{ public: explicit Test(int i) : m_i(i) { std::cout << "Test::Test(int)" << std::endl; } Test(const Test& rhs) : m_i(rhs.m_i) { std::cout << "Test::opeartor=(const Test&)" << std::endl; } ~Test() { std::cout << "Test::~Test()" << std::endl; } private: int m_i; }; Test getTest() { Test t(1); return t; } int main() { auto test = getTest(); }
在代码33行中,main()
函数使用getTest()
函数的返回值进行初始化,而getTest()
函数的返回值为类Test
,按理来说应该会返回一个临时对象tmp
,然后在33行中调用test
的拷贝构造函数,然后tmp
对象析构掉。而在getTest()
函数中,也有一个从t
到临时对象tmp
的拷贝构造和t的析构。即:
getTest()
中t
构造函数- 临时对象
tmp
拷贝构造 getTest()
中t
析构函数main()
函数中test
对象拷贝构造- 临时对象
tmp
析构 main()
函数中test
对象析构。
即期望的输出应该是类似于:
Test::Test(int) Test::opeartor=(const Test&) Test::~Test() Test::opeartor=(const Test&) Test::~Test() Test::~Test()
但在实际情况中,g++(g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0)的输出确是:
Test::Test(int) Test::~Test()
事实上,如果一个函数中所有return
语句的返回值都是同一个具名对象(如getTest()
中return
语句返回的都是对象t
),那么编译器会省略掉其拷贝构造函数,即变成如下代码:
void getTest(Test* pTest) { ::new (pTest) Test(1); // placement new, 即在pTest处调用构造函数 } int main() { char tmp[sizeof(Test)]; // 只分配内存,不调用构造函数 getTest(reinterpret_cast<Test*>(tmp)); Test& t = *(reinterpret_cast<Test*>(tmp)); }
在这种情况下,只会调用一次构造函数,一次析构函数,而没有拷贝构造函数的调用。
而g++支持禁用掉返回值优化,只需要添加编译参数-fno-elide-constructors
即可,得到的结果如前所示,调用了多次拷贝构造和析构函数。
书中一处描述现在可能已不适用
在《深度探索C++对象模型》中提到类需要显式定义的拷贝构造函数时才会触发NRV,但在测试中g++即使类不显式定义拷贝构造函数也会触发NRV。
class TestWithoutCopyCtor { public: TestWithoutCopyCtor(int i) : m_i(i) { } private: int m_i; }; class TestWithCopyCtor { public: TestWithCopyCtor(int i) :m_i(i) {} TestWithCopyCtor(const TestWithCopyCtor& rhs) :m_i(rhs.m_i) {} private: int m_i; }; TestWithCopyCtor getTestWith() { TestWithCopyCtor t(1); return t; } TestWithoutCopyCtor getTestWithout() { TestWithoutCopyCtor t(2); return t; } int main() { auto t1 = getTestWith(); auto t2 = getTestWithout(); }
在gcc.godbolt.org上使用其中x86-64 gcc 12.2,不添加任何编译参数,getTestWith()和getTestWithout()函数汇编如下:
getTestWith(): push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov esi, 1 mov rdi, rax call TestWithCopyCtor::TestWithCopyCtor(int) [complete object constructor] nop mov rax, QWORD PTR [rbp-8] leave ret getTestWithout(): push rbp mov rbp, rsp sub rsp, 16 lea rax, [rbp-4] mov esi, 2 mov rdi, rax call TestWithoutCopyCtor::TestWithoutCopyCtor(int) [complete object constructor] mov eax, DWORD PTR [rbp-4] leave ret
可以看到无论有没有显式定义的拷贝构造函数,都会触发NRV。
在网上找到了这样一段话:
如果客户没有显示提供拷贝构造函数,那么cfront认为客户对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,没必要再对其实施NRV优化;但 如果客户显式提供了拷贝构造函数,这说明客户由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。
返回值优化(RVO)
在wiki上可找到Copy elision(拷贝省略)词条,其中介绍了返回值优化(Return Value Optimization, RVO),上面的NRV就是RVO的一个例子。其余情况如(示例来自wiki):
- 函数直接返回一个匿名对象
#include <iostream> struct C { C() = default; C(const C&) { std::cout << "A copy was made.\n"; } }; C f() { return C(); } int main() { std::cout << "Hello World!\n"; C obj = f(); }
- 还有一个拷贝省略的例子:生成一个匿名临时对象,用来初始化一个对象(
c2
)
#include <iostream> int n = 0; struct C { explicit C(int) {} C(const C&) { ++n; } // the copy constructor has a visible side effect }; // it modifies an object with static storage duration int main() { C c1(42); // direct-initialization, calls C::C(int) C c2 = C(42); // copy-initialization, calls C::C(const C&) std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码