具名返回值优化(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
}
标签:int,具名,mov,Test,返回值,拷贝,NRV,构造函数
From: https://www.cnblogs.com/lycpp/p/16822425.html