使用 -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
之前被销毁
y
在 c
构建后销毁
并询问为什么两种情况的行为不一样。
答案是:在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
函数,输出顺序是
- 从
a
复制构造参数y
- 调用函数
g
,传递y
- 在函数
g
中,y
被移动到临时 return 对象中
- 函数
g
returns
- 回到
main
,临时 return 对象被移动到 c
- 临时return对象被销毁
- 参数对象
y
被破坏
对于函数 f
,局部对象 x
在 f
的范围内构造和析构:
f
被称为
x
是默认构造的
- 临时 return 对象是从
x
移动构造的
x
被破坏
- 函数
f
returns
- 临时 return 对象移入
b
- 临时return对象被销毁
在以下代码中(使用 -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
之前被销毁y
在c
构建后销毁
并询问为什么两种情况的行为不一样。
答案是:在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 报告链接的解释,请参阅此问题:
如果您查看生成的程序集(例如 on the compiler explorer),区别就很明显了。
在这里您可以看到,对于 g
的调用,参数对象实际上是在 main
函数中创建和销毁的。
所以对于 g
函数,输出顺序是
- 从
a
复制构造参数 - 调用函数
g
,传递y
- 在函数
g
中,y
被移动到临时 return 对象中 - 函数
g
returns - 回到
main
,临时 return 对象被移动到c
- 临时return对象被销毁
- 参数对象
y
被破坏
y
对于函数 f
,局部对象 x
在 f
的范围内构造和析构:
f
被称为x
是默认构造的- 临时 return 对象是从
x
移动构造的
x
被破坏- 函数
f
returns - 临时 return 对象移入
b
- 临时return对象被销毁