使用-fno ellide构造函数进行编译时,连续调用移动构造函数



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

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

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

我把这个问题解释为:你强调了以下两点:

  • x在构造b之前被破坏
  • c构建后y被破坏

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


答案是:在C++14中,[expr.call]/4中指定的标准是,当函数返回时,y应该被销毁。然而,并没有明确说明这意味着函数返回的确切阶段。有人提出了CWG问题。

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

由于C++14措辞的模糊性,我们不能确切地说g++ -std=c++14不符合C++14,但无论如何,由于ABI问题,它现在不会改变。

有关标准和CWG报告链接的解释,请参阅此问题:功能参数破坏的排序以及功能参数的后期破坏

如果您查看生成的程序集(例如在编译器资源管理器上(,则差异非常明显。

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

因此,对于g函数,输出顺序为

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

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

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

最新更新