这篇主要介绍返回值优化及移动构造(赋值)函数,后者是C++11中的知识点。本来是不准备写的,因为这些都比较基础且编译器为我们进行了一定的优化,但还是想记录下代码层面所发生的事情,且为最近相关C++11新知识点的学习进行简单总结。
在进行下面之前,需要了解什么是构造函数,默认构造函数,拷贝构造函数,赋值运算符,析构函数,以及什么情况下编译器会为类合成在编译器看来有用的构造(和其他几个)函数,这些知识可以参考《深度探索C++对象模型》和《(More)Effective C++》。
以最基本的类开始分析:
5 class CBase {
6 public:
7 CBase(const uint32_t m = 0) : m_count(m) {
8 std::cout << "CBase ctor" << std::endl;
9 }
10
11 CBase(const CBase& lhs) {
12 std::cout << "CBase copy ctor" << std::endl;
13 }
14
15 ~CBase() {
16 std::cout << "CBase dtor" << std::endl;
17 }
18 private:
19 uint32_t m_count;
20 };
21
22 CBase GetObjWithNoMove() {
23 return CBase();
24 }
25
26 int main(int argc, char** argv) {
27 CBase obj = GetObjWithNoMove();
28 return 0;
29 }
版本为7.3.0,直接g++ 编译,不加任何选项,输出为:
CBase ctor
CBase dtor
不考虑析构的代码,其他反汇编如下:
138 __Z16GetObjWithNoMovev:
139 100000d34: 55 pushq %rbp
140 100000d35: 48 89 e5 movq %rsp, %rbp
141 100000d38: 48 83 ec 10 subq $16, %rsp
142 100000d3c: 48 89 7d f8 movq %rdi, -8(%rbp)
143 100000d40: 48 8b 45 f8 movq -8(%rbp), %rax
144 100000d44: be 00 00 00 00 movl $0, %esi
145 100000d49: 48 89 c7 movq %rax, %rdi
146 100000d4c: e8 9b 00 00 00 callq 155
151 _main:
152 100000d57: 55 pushq %rbp
153 100000d58: 48 89 e5 movq %rsp, %rbp
154 100000d5b: 53 pushq %rbx
155 100000d5c: 48 83 ec 28 subq $40, %rsp
156 100000d60: 89 7d dc movl %edi, -36(%rbp)
157 100000d63: 48 89 75 d0 movq %rsi, -48(%rbp)
158 100000d67: 48 8d 45 ec leaq -20(%rbp), %rax
159 100000d6b: 48 89 c7 movq %rax, %rdi
160 100000d6e: e8 c1 ff ff ff callq -63 <__Z16GetObjWithNoMovev>
结合源码分析,汇编从152〜159似乎是在准备什么参数,然后调用GetObjWithNoMove
,但是从源码并没有传实参,但这里为何会这样呢?就debug看一下这几行代码是什么玩意。
启动的时候,就直接定位在=> 0x0000000100000d67 <+16>: lea -0x14(%rbp),%rax
把地址mov到rax中,查看下obj地址和rbp-0x20的值为:
(gdb) p obj
$4 = {m_count = 0}
(gdb) p &obj
$5 = (CBase *) 0x7ffeefbffa8c
(gdb) i r rbp
rbp 0x7ffeefbffaa0 0x7ffeefbffaa0
(gdb) x/w 0x7ffeefbffa8c
0x7ffeefbffa8c: 0x00000000
我们new一个对象的时候,先是分配一段足够的空间,再调用构造函数,再返回地址,当然后两步可以顺序对调,这在多线程中会有些问题,不讨论。
这里先分配一段空间,然后作为参数调用函数GetObjWithNoMove
,再里面进行了构造函数的调用:
0x0000000100000d3c <+8>: mov %rdi,-0x8(%rbp)
=> 0x0000000100000d40 <+12>: mov -0x8(%rbp),%rax
0x0000000100000d44 <+16>: mov $0x0,%esi
0x0000000100000d49 <+21>: mov %rax,%rdi
0x0000000100000d4c <+24>: callq 0x100000dec
(gdb) si
CBase::CBase (this=0x100000000, m=32766) at move.cpp:7
7 CBase(const uint32_t m = 0) : m_count(m) {
(gdb) disassemble
Dump of assembler code for function CBase::CBase(unsigned int):
=> 0x0000000100000cb4 <+0>: push %rbp
0x0000000100000cb5 <+1>: mov %rsp,%rbp
0x0000000100000cb8 <+4>: sub $0x10,%rsp
0x0000000100000cbc <+8>: mov %rdi,-0x8(%rbp)
0x0000000100000cc0 <+12>: mov %esi,-0xc(%rbp)
0x0000000100000cc3 <+15>: mov -0x8(%rbp),%rax
0x0000000100000cc7 <+19>: mov -0xc(%rbp),%edx
0x0000000100000cca <+22>: mov %edx,(%rax)
是编译器为我们进行了优化?那原来的过程是怎么样的呢?重新编译并加选项-fno-elide-constructors
并重新执行结果如下:
CBase ctor
CBase copy ctor
CBase dtor
CBase copy ctor
CBase dtor
CBase dtor
这里解释一下,然后再进行反汇编说明。在调用GetObjWithNoMove
过程中,先进行了一次构造CBase()
,此时打印第一行,然后调用拷贝构造作为返回值,打印第二行,执行完此条语句后,便进行析构,即打印第三行。GetObjWithNoMove
结束返回一个临时对象,并以拷贝构造obj,即第四行,执行完后析构,最后析构obj。在这个过程中生成了两个临时对象,真正有用的是最后一次,这里没有涉及到复杂的对象构造和深拷贝。
以下是部分汇编代码,进行了旁注:
Dump of assembler code for function main(int, char**):
0x0000000100000cb5 <+0>: push %rbp
0x0000000100000cb6 <+1>: mov %rsp,%rbp
0x0000000100000cb9 <+4>: push %rbx
0x0000000100000cba <+5>: sub $0x28,%rsp
0x0000000100000cbe <+9>: mov %edi,-0x24(%rbp)
0x0000000100000cc1 <+12>: mov %rsi,-0x30(%rbp)
0x0000000100000cc5 <+16>: lea -0x14(%rbp),%rax
=> 0x0000000100000cc9 <+20>: mov %rax,%rdi
0x0000000100000ccc <+23>: callq 0x100000c50 <GetObjWithNoMove()>
0x0000000100000cd1 <+28>: lea -0x14(%rbp),%rdx
0x0000000100000cd5 <+32>: lea -0x18(%rbp),%rax
0x0000000100000cd9 <+36>: mov %rdx,%rsi
0x0000000100000cdc <+39>: mov %rax,%rdi
0x0000000100000cdf <+42>: callq 0x100000d8a //拷贝构造
0x0000000100000ce4 <+47>: lea -0x14(%rbp),%rax
0x0000000100000ce8 <+51>: mov %rax,%rdi
0x0000000100000ceb <+54>: callq 0x100000d96 //析构函数
0x0000000100000cf0 <+59>: mov $0x0,%ebx
0x0000000100000cf5 <+64>: lea -0x18(%rbp),%rax
0x0000000100000cf9 <+68>: mov %rax,%rdi
0x0000000100000cfc <+71>: callq 0x100000d96 //析构函数
Dump of assembler code for function GetObjWithNoMove():
=> 0x0000000100000c50 <+0>: push %rbp
0x0000000100000c51 <+1>: mov %rsp,%rbp
0x0000000100000c54 <+4>: push %rbx
0x0000000100000c55 <+5>: sub $0x28,%rsp
0x0000000100000c59 <+9>: mov %rdi,-0x28(%rbp)
0x0000000100000c5d <+13>: lea -0x14(%rbp),%rax
0x0000000100000c61 <+17>: mov $0x0,%esi
0x0000000100000c66 <+22>: mov %rax,%rdi
0x0000000100000c69 <+25>: callq 0x100000d90 //构造函数
0x0000000100000c6e <+30>: lea -0x14(%rbp),%rdx
0x0000000100000c72 <+34>: mov -0x28(%rbp),%rax
0x0000000100000c76 <+38>: mov %rdx,%rsi
0x0000000100000c79 <+41>: mov %rax,%rdi
0x0000000100000c7c <+44>: callq 0x100000d8a //拷贝构造
0x0000000100000c81 <+49>: nop
0x0000000100000c82 <+50>: lea -0x14(%rbp),%rax
0x0000000100000c86 <+54>: mov %rax,%rdi
0x0000000100000c89 <+57>: callq 0x100000d96 //析构函数
为了减少其中的构造成本,以下是编译器为这种代码优化的实现伪代码,即返回值优化,这个函数会被编译器转化为GetObjWithNoMove
:
void GetObjWithNoMove(CBase& obj) {
obj.CBase::CBase();
//do something...
}
CBase obj;
GetObjWithNoMove(obj);
如果原来是这样的形式:
CBase GetObjWithNoMove() {
CBase tmp;
//do something...
return tmp;
}
还是会被优化成上面的形式,不过网上和书上有提及那种NRVO优化,用拷贝构造的形式,可能跟写法有关系:
void GetObjWithNoMove(CBase& obj) {
tmp.CBase::CBase();
obj.CBase::CBase(tmp); //copy ctor
tmp.CBase::~CBase();
return;
}
不过一般不会像上面这么写代码,再说编译器也为我们进行了优化。
具体可参考《深度探索C++对象模型》第二章的构造函数语意学。
不过这书比较老,最新版的C++11加了许多新的特性,比如移动构造,移动赋值。这些不是很难理解,我个人在工作中用C++11的机会不多,平时都只是从网上和书上,开源项目中学习到的使用方法。
以上涉及到万能引用,右值引用,和完美转发,这些可以看参考资料。
包括最新的emplace_back之类的接口直接在内存地址上构造而不是使用push_back之类的接口,后者可能生成临时对象等性能方面的原因。
当然使用emplace之类的需要自定义类带有构造函数,不然会报如下的错误:
/usr/local/Cellar/gcc@7/7.3.0/include/c++/7.3.0/ext/new_allocator.h:136:4: error: no matching function for call to 'CString::CString(int)'
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
move.cpp:16:5: note: candidate: CString::CString(const CString&)
CString(const CString& str) {
move.cpp:16:5: note: no known conversion for argument 1 from 'int' to 'const CString&'
还有些情况下不能使用emplace之类的,具体原因可能要在实践中去发现,比如参考中的会涉及,但是看的云里雾里的。
emplace_back
部分源码如下:
template<class _Objty,
class... _Types>
static void construct(_Alloc&, _Objty * const _Ptr, _Types&&... _Args)
{ // construct _Objty(_Types...) at _Ptr
::new (const_cast<void *>(static_cast<const volatile void *>(_Ptr)))
_Objty(_STD forward<_Types>(_Args)...);
}
还是要多学习,和运用。
这篇没啥干货,后面会备忘C++11中的一些实现源码。
参考资料:
《深度探索C++对象模型》
《Effective Modern C++》
C++雾中风景9:emplace_back与可变长模板
网友评论