使用 -fno-elide-constructors 编译时连续调用移动构造函数

Consecutive calls to move constructor when compiling with -fno-ellide-constructors

在以下代码中(使用 -std=c++14 -Wall -fno-elide-constructors 在 gcc 9.2 上构建):


struct Noisy {
    Noisy() { std::cout << "Default construct [" << (void*)this << "]\n"; }
    Noisy(const Noisy&) { std::cout << "Copy construct [" << (void*)this << "]\n"; }
    Noisy(Noisy&&) { std::cout << "Move construct [" << (void*)this << "]\n"; }
    Noisy& operator=(const Noisy&) { std::cout << "Copy assignment" << std::endl; return *this; }
    Noisy& operator=(Noisy&&) { std::cout << "Move assignment" << std::endl; return *this; }
    ~Noisy() { std::cout << "Destructor [" << (void*)this << "]\n"; }
};

Noisy f() {
    Noisy x;
    return x;
}

Noisy g(Noisy y) {
    return y;
}
int main(void) {
    Noisy a;
    std::cout << "--- f() ---\n";
    Noisy b = f();
    std::cout << "b [" << (void*)&b << "]\n";
    std::cout << "--- g(a) ---\n";
    Noisy c = g(a);
    std::cout << "c [" << (void*)&c << "]\n";
    std::cout << "---\n";
    return 0;
}

产生这个结果:

Default construct [0x7ffc4445737a]
--- f() ---
Default construct [0x7ffc4445735f]
Move construct [0x7ffc4445737c]
Destructor [0x7ffc4445735f]
Move construct [0x7ffc4445737b]
Destructor [0x7ffc4445737c]
b [0x7ffc4445737b]
--- g(a) ---
Copy construct [0x7ffc4445737e]
Move construct [0x7ffc4445737f]
Move construct [0x7ffc4445737d]
Destructor [0x7ffc4445737f]
Destructor [0x7ffc4445737e]
c [0x7ffc4445737d]
---
Destructor [0x7ffc4445737d]
Destructor [0x7ffc4445737b]
Destructor [0x7ffc4445737a]

为什么 f() 中的本地 Noisy 对象 [0x7ffc4445735f] 的副本在移动到 f 的 return 地址后(以及之前b 开始建设);而 g() 似乎没有发生同样的事情? IE。在后一种情况下(当 g() 执行时),函数参数 Noisy y[0x7ffc4445737e] 的本地副本仅在 c 准备好构造后才会被销毁。它不应该在它被移动到 g 的 return 地址后立即被销毁,就像 f() 发生的一样吗?

这些是输出中地址的变量:

0x7ffc4445737a  a
0x7ffc4445735f  x
0x7ffc4445737c  return value of f() 
0x7ffc4445737b  b
0x7ffc4445737e  y
0x7ffc4445737f  return value of g()
0x7ffc4445737d  c

我把问题解读为:你强调了以下两点:

  • x 在构建 b 之前被销毁
  • yc 构建后销毁

并询问为什么两种情况的行为不一样。


答案是:在C++14中[expr.call]/4中规定的标准是y应该在函数returns时销毁。然而,在函数的哪个阶段 return 并没有明确说明这意味着什么。提出了 CWG 问题。

从 C++17 开始,规范现在由实现定义 y 是与函数的局部变量同时销毁,还是在包含函数调用。事实证明,这两种情况无法调和,因为这将是一个破坏性的 ABI 更改(想想如果 y 的析构函数抛出异常会发生什么);并且 Itanium C++ ABI 在完整表达式的末尾指定销毁。

我们不能明确地说 g++ -std=c++14 不符合 C++14,因为 C++14 的措辞含糊不清,但是无论如何现在都不会更改,因为ABI 问题。

有关标准和 CWG 报告链接的解释,请参阅此问题: and also

如果您查看生成的程序集(例如 on the compiler explorer),区别就很明显了。

在这里您可以看到,对于 g 的调用,参数对象实际上是在 main 函数中创建和销毁的。

所以对于 g 函数,输出顺序是

  1. a
  2. 复制构造参数y
  3. 调用函数g,传递y
  4. 在函数 g 中,y 被移动到临时 return 对象中
  5. 函数greturns
  6. 回到 main,临时 return 对象被移动到 c
  7. 临时return对象被销毁
  8. 参数对象y被破坏

对于函数 f,局部对象 xf 的范围内构造和析构:

  1. f被称为
  2. x是默认构造的
  3. 临时 return 对象是从 x
  4. 移动构造的
  5. x被破坏
  6. 函数freturns
  7. 临时 return 对象移入 b
  8. 临时return对象被销毁