C++标准是否保证统一初始化是异常安全的?

47
#include <iostream>

using namespace std;

struct A
{
    A() { cout << "A" << endl; }
    ~A() { cout << "~A" << endl; }
};

A Ok() { return {}; }
A NotOk() { throw "NotOk"; }

struct B
{
    A a1;
    A a2;
};

void f(B) {}

int main()
{
    try
    {
        f({ Ok(), NotOk() });
    }
    catch (...)
    {}
}

VC++Clang 输出:

A
~A

gcc 输出:

A

似乎是GCC的一个严重bug。

参考:GCC bug 66139Andrzej Krzemieński的"A serious bug in GCC"

我只是想知道:

C++标准是否保证统一初始化是异常安全的?


5
@einpoklum 大家好。 - Walter
4
我认为例外安全在这里不是一个问题。除了1)通过exit()立即退出应用程序和2)未定义的行为之外,如果一个对象在自动作用域中被构造,那么当执行离开其作用域时,它必须被销毁。这是C ++的基本原则。这是一个编译器错误。 - Sam Varshavchik
6
起初我认为这可能是一些愚蠢的优化,因为静态分析发现程序在那个万能处理程序上立即退出。但不是,这是编译器的一个bug。一个非常严重和可怕的bug - StoryTeller - Unslander Monica
我添加了一个记录自身的复制构造函数,并将有问题的序列更改为 A a=Ok(); f({ a, NotOk() });。我在函数调用参数的构造期间记录了复制构造函数的调用,以及在本地作用域中的 a 实例的析构函数的调用,但是没有为复制构造的实例调用析构函数。 - Sam Varshavchik
9
你有一篇博客声称这是一个 bug,还有一个 gcc 的 bug 报告确认了这个问题,但现在又有一个问题问这是不是一个 bug? - Barry
显示剩余2条评论
1个回答

31

看起来是这样的:

有趣的是,在所有地方(N4618)中在 §6.6/2 跳转语句 [stmt.jump] 中发现:

从作用域退出时(不管是如何完成的),在该作用域中构造的具有自动存储期(3.7.3)的对象将按照创建的相反顺序被销毁。【注:对于临时对象,请参见 12.2。—end note】传输离开循环、块或回退到具有自动存储期的初始化变量之前涉及销毁自动存储期对象,这些对象在传输点被解除范围,但不在传输点转移进入的范围内。(有关传输进入块的信息,请参见 6.7)。 【注:但是,程序可以在不销毁具有自动存储期的类对象的情况下终止(例如通过调用 std::exit()std::abort() (18.5)。—end note】

我认为重点在于“(however accomplished)”部分。这包括一个例外情况(但不包括导致 std::terminate 的事情)。


编辑

我认为更好的参考是 §15.2/3 构造函数和析构函数 [except.ctor](重点是我的):

如果对象的初始化或除委托构造函数之外的其他方式的销毁由异常终止,则为对象的每个直接子对象以及完整对象其初始化已经完成(8.6),且其析构函数尚未开始执行的虚基类子对象调用析构函数,但在销毁的情况下,联合类的变体成员不会被销毁。这些子对象按照它们创建完成的相反顺序进行销毁。如果有任何构造函数或析构函数的带有函数尝试块的处理程序,则在进入此处理程序之前对此类销毁进行排序。

这将包括聚合初始化(我今天学到它可以被称为非空初始化

......对于具有构造函数的对象,我们可以引用§12.6.2/12 [class.base.init](我强调):

在非委托构造函数中,类类型的每个可能构建的子对象的析构函数都可能被调用(12.4)。[ 注意:此规定确保在抛出异常的情况下调用完全构建的子对象的析构函数(15.2)。 -注结束]


1
现在我正在脑海中回顾我编写的所有程序,这些程序可能会因未捕获的异常和失败的断言而危及系统... - YSC
1
@YSC:一个失败的断言不应该危及系统。如果由于这个 bug 而导致一个关键系统被关闭和重启而不是提供随机行为,那么我会说,这个断言确实完成了它被设计来完成的工作。这正是你放置它的原因——在面对可怕的 bug 时保护系统完整性和数据一致性。 - Christian Hackl
@ChristianHackl 我担心这个_"[注意:然而,程序可以被终止(例如通过调用std::exit()或std::abort()(18.5)),而不销毁具有自动存储期的类对象。—注]"_。 - YSC
1
@YSC:啊,我想我误解了你的评论。然而,在这种情况下,失败的断言应该更少成为问题,不是吗?实际上,这几乎就是你使用assertexitabort的原因;你不希望执行任何更多的代码,因为你已经检测到了一个错误或某种紧急情况,包括析构函数中的代码。我的观点是:在某些情况下,在C++中,正确且可取的做法是执行局部对象的析构函数,而是立即退出。当然,这个可怕的GCC bug不是其中之一。 - Christian Hackl
@ChristianHackl 是的,我同意。我只是从未想过。 - YSC

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