【问题标题】:Consecutive calls to move constructor when compiling with -fno-ellide-constructors使用 -fno-elide-constructors 编译时连续调用移动构造函数
【发布时间】:2020-06-28 05:45:27
【问题描述】:

在以下代码中(使用-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 的返回地址后立即被破坏(并且在 b 的构造开始之前);而g() 似乎没有发生同样的情况? IE。在后一种情况下(当g() 执行时),函数参数Noisy y[0x7ffc4445737e] 的本地副本仅在c 准备好构造后被销毁。它不应该在被移动到g的返回地址后立即被销毁,就像f()一样吗?

【问题讨论】:

  • 我编辑了代码,为每个变量使用不同的名称,这样可以避免在答案谈到每个变量时造成混淆

标签: c++ move rvo


【解决方案1】:

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

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。然而,在函数返回的哪个阶段并没有明确说明这意味着什么。提出了 CWG 问题。

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

由于 C++14 的措辞含糊不清,我们不能肯定地说 g++ -std=c++14 不符合 C++14,但无论如何,由于 ABI 问题,它现在不会改变.

有关标准和 CWG 报告链接的说明,请参阅此问题:Sequencing of function parameter destructionLate destruction of function parameters

【讨论】:

【解决方案2】:

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

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

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

  1. a 复制构造参数y
  2. 调用函数g,传递y
  3. 函数内部gy被移到临时返回对象中
  4. 函数g返回
  5. 返回main,临时返回对象被移动到c
  6. 临时返回对象被破坏
  7. 参数对象y被破坏

对于函数f,本地对象xf的范围内构造和销毁:

  1. f 被调用
  2. x 是默认构造的
  3. 临时返回对象是从x移动构造的
  4. x 被破坏
  5. 函数f返回
  6. 临时返回对象移动到b
  7. 临时返回对象被破坏

【讨论】:

  • 是的,谢谢,生成的代码确实显示了 gcc 的解释。但是我试图指出的是明显的不一致,其中使用您的“g”步骤,g7 不会发生在 g3 和 g4 之间。鉴于对于 f,步骤 'x 被破坏',确实发生在 f3 和 f5 之间......
猜你喜欢
  • 2022-12-14
  • 1970-01-01
  • 1970-01-01
  • 2015-01-21
  • 2012-08-27
  • 2011-05-22
  • 1970-01-01
  • 2012-10-19
  • 2014-01-30
相关资源
最近更新 更多