在我写的《现代C++语言核心特性解析》中有一个小节是讲解的返回值优化,在这篇文章中,我将对这部分内容进行一点补充,将更多细节展示出来。
首先还是来看看书中的这段代码:

#include <iostream>
class X {
    public:
    X() { std::cout << "X ctor" << std::endl; }
    X(const X& x) { std::cout << "X copy ctor" << std::endl; }
    ~X() { std::cout << "X dtor" << std::endl; }
};
X make_x() {
    X x1;
    return x1;
}
int main() {
    X x2 = make_x();
}

这段代码在开启和关闭拷贝消除的运行情况是不同的,不过书中只使用了两种情况讨论,但是实际上我漏掉了C++17关闭拷贝消除的情况,以下是正确的对比表格:

拷贝消除 C++14 关闭拷贝消除 C++17 关闭拷贝消除
X ctor X ctor X ctor
X dtor X copy ctor X copy ctor
X dtor X dtor
X copy ctor X dtor
X dtor
X dtor

可以看到C++17和C++14的行为是不同的。开启拷贝消除的很明显,优化让构造直接发生在main函数中:

make_x(): # @make_x()
  push rbx
  mov rbx, rdi
  call X::X() [base object constructor]
  mov rax, rbx
  pop rbx
  ret
main: # @main
  push rbx
  sub rsp, 16
  lea rdi, [rsp + 8]
  call make_x()

C++14的行为也很明确,和书中介绍了一样,发生了三次构造:

make_x(): # @make_x()
  push r14
  push rbx
  push rax
  mov rbx, rdi
  mov r14, rsp
  mov rdi, r14
  call X::X() [base object constructor]
  mov rdi, rbx
  mov rsi, r14
  call X::X(X const&) [base object constructor]
  mov rdi, rsp
  call X::~X() [base object destructor]
  mov rax, rbx
  add rsp, 8
  pop rbx
  pop r14
  ret
  
main: # @main
  push rbx
  sub rsp, 16
  mov rbx, rsp
  mov rdi, rbx
  call make_x()
  lea rdi, [rsp + 8]
  mov rsi, rbx
  call X::X(X const&) [base object constructor]
  mov rdi, rsp
  call X::~X() [base object destructor]
  lea rdi, [rsp + 8]
  call X::~X() [base object destructor]
  xor eax, eax
  add rsp, 16
  pop rbx
  ret

但是C++17的行为相对就比较奇怪了,关闭拷贝消除但并没有完全关闭:

make_x(): # @make_x()
  push r14
  push rbx
  push rax
  mov rbx, rdi
  mov r14, rsp
  mov rdi, r14
  call X::X() [base object constructor]
  mov rdi, rbx
  mov rsi, r14
  call X::X(X const&) [base object constructor]
  mov rdi, rsp
  call X::~X() [base object destructor]
  mov rax, rbx
  add rsp, 8
  pop rbx
  pop r14
  ret

main: # @main
  push rbx
  sub rsp, 16
  lea rbx, [rsp + 8]
  mov rdi, rbx
  call make_x()
  mov rdi, rbx
  call X::~X() [base object destructor]
  xor eax, eax
  add rsp, 16
  pop rbx
  ret

只有两次构造,x1拷贝到临时对象,临时对象拷贝到x2的过程合并成了一次,也就是x1直接拷贝到了x2,这是为什么呢?
其实是因为C++17对临时对象进行了特殊规定:

6.7.7 Temporary objects [class.temporary]

The materialization of a temporary object is generally delayed as long as possible in order to avoid creating unnecessary temporary objects.

在提案文档p0135r1中也对拷贝消除的描述进行了修改(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0135r1.html
)。

至此,我们已经了解了C++17关闭拷贝消除后的特殊情况的因由。最后补充一点,关于拷贝消除,除了在返回值上可以做优化,还有下面这些情况都可以进行优化,当然有一些优化是没有实现的:

  1. return语句中返回类类型,返回对象类型和函数返回类型相同,并且要求类型是非易失且有自动存储周期的对象。
  2. throw表达式,操作数类型也要求是非易失且有自动存储周期的对象,并且作用域不超过最内侧的try。
  3. 异常处理(其实就是try-catch中catch(){}),声明的对象如果和抛出对象类型相同,可以将声明对象看作抛出对象的别名,前提条件是这个对象在这个过程中除了构造和析构是不会被改变的。
  4. 在协程中,协程参数的拷贝可以被忽略,也就是直接引用参数本身,当然也有前提条件,就是在处理对象的过程中除了构造和析构是不会被改变的。