函数返回值是自动对象,因此保证会被销毁吗?

57

在 [except.ctor] 中,标准文件 (N4140) 保证:

...自从进入 try 块开始构造的所有自动化对象都将调用析构函数...

然而,在下面的示例中,空的输出证明了函数 foo 的返回值没有被析构,尽管它已经被构造。编译选项为 -O0 -fno-elide-constructors -std=c++14,使用 g++ (5.2.1) 和 clang++ (3.6.2-1)。

struct A { ~A() { cout << "~A\n"; } };

struct B { ~B() noexcept(false) { throw 0; } };

A foo() {
  B b;
  return {};
}

int main() {
  try { foo(); }
  catch (...) { }
}

这是g++和clang++都存在的一个bug吗?还是函数返回值不被视为自动对象,或者这是C++语言中的一个漏洞?在[stmt.return]、[expr.call]或[dcl.fct]中,我没有找到明确说明函数返回值是否被视为自动对象的语句。我所找到的最接近的提示是6.3.3 p2:

...return语句可能涉及临时对象的构造、复制或移动...

以及5.2.2 p10:

如果结果类型是左值引用类型或对函数类型的右值引用,则函数调用是左值;如果结果类型是对象类型的右值引用,则函数调用是xvalue;否则是prvalue。

3个回答

45

函数返回值被视为临时对象,返回值的构造在局部变量销毁之前完成。

遗憾的是,标准中对此没有明确定义。有一个 缺陷 描述了这个问题并提供了一些修复措施。

[...] 仅当返回类型是 cv void 时,在带有 void 类型操作数的 return 语句中使用。在带有任何其他操作数的返回语句中,只应在其返回类型不是 cv void 的函数中使用;该返回语句使用从操作数中的复制初始化 (8.5 [dcl.init]) 初始化将要返回的对象或引用。[...]

返回实体的复制初始化在返回语句的操作数建立的完整表达式结束时的临时变量销毁之前进行,而这又在包含返回语句的块的局部变量(6.6 [stmt.jump])销毁之前进行。

由于函数返回值是临时对象,因此它们不受您帖子开头提到的“所有自动对象都调用析构函数”的引言的影响。但是,[class.temporary] / 3说:

[...] 临时对象在评估包含它们创建点的完整表达式的最后一步被销毁。即使该评估以引发异常而结束也是如此。[...]

因此,我认为您可以将此视为 GCC 和 Clang 中的一个错误。

不要从析构函数中抛出异常 ;)


6
我发现自 GCC 和 Clang 都已经有关于这个错误的报告,已经有几年了,所以我不指望他们会很快修复它:GCCClang - Florian Kaufmann
在这种情况下,编译器是否允许省略对象“ A”的真正构造?我的意思是类似于RVO的某些规则。 - Mikhail
@Mikhail 我不这么认为。RVO 无法完全消除构造函数,但可以消除中间构造。 - TartanLlama

7
这是一个bug,而且这一次,MSVC实际上做得很好:它打印了“~A”。

有时候clang和gcc会拒绝代码,但是MSVC接受具有定义行为的代码。这也曾经发生在我身上,如果需要我可以提供示例。 - Angelus Mortis
6
请问您能否添加一个参考标准,证明函数返回值被认为是自动变量,或者证明这确实是gcc和clang的一个bug? - Florian Kaufmann
@FlorianKaufmann 我认为这个例子表明,有一个A对象在创建后超出作用域,但其析构函数从未被调用。 - Antonio
@FlorianKaufmann TartanLlma在您的帖子下引用了一个缺陷报告,似乎恰好解决了手头的问题(即类型为A的待返回临时对象是否在局部变量b被销毁之前构造?)。答案应该是肯定的,但是未详细说明。答案很可能是肯定的,因为明显的使用场景涉及到本地变量组合返回值。 - Peter - Reinstate Monica
@FlorianKaufmann:默认情况下它基本上是自动的 - 还能是什么呢?不是全局变量,不是成员变量,也不是异常对象... - MSalters
4
“@MSalters. 'What else could it be?'” 我有同样的感受。但这并不是证明。我更希望能够得到确认,并在标准中看到相关段落。我们从经验中知道,C++语言有很多令人惊讶的地方。 - Florian Kaufmann

7
我修改了你的代码,现在从输出结果可以看出A没有被销毁。
#include<iostream>

using namespace std;

struct A {
    ~A() { cout << "~A\n"; }
    A() { cout << "A()"; }
};

struct B {
    ~B() noexcept( false ) { cout << "~B\n"; throw(0); }
    B() { cout << "B()"; }
};

A foo() {
    B b;
    return;
}

int main() {
    try { foo(); }
    catch (...) {}
}

输出结果为:

B()A()~B

所以,这可能是一个bug。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接