47
votes
#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++ and clang output:

A
~A

While gcc outputs:

A

It seems a serious bug of GCC.

For reference, see GCC bug 66139 and "A serious bug in GCC" by Andrzej Krzemieński.

I just wonder:

Does the C++ standard guarantee that uniform initialization is exception-safe?

1
What version(s) of GCC exhibit this? - einpoklum
@einpoklum all. - Walter
I don't think that exception safety is an issue here. Barring 1) an immediate application exit, via exit(), 2) undefined behavior, -- if an object gets constructed in automatic scope, it must be destroyed when execution leaves its scope. This is fundamental to C++. This is a compiler bug. - Sam Varshavchik
First I thought it may be some stupid optimization, since static analysis figures out the program exits immediately on that catch-all handler. But no, it's a compiler bug. A very major and horrible bug. - StoryTeller - Unslander Monica
So you have a blog that says it's a bug, and a gcc bug report that says and confirms it as a bug... but then you have a question asking if it's a bug? - Barry

1 Answers

31
votes

It seems so:

Curiously found in §6.6/2 Jump Statements [stmt.jump] of all places (N4618):

On exit from a scope (however accomplished), objects with automatic storage duration (3.7.3) that have been constructed in that scope are destroyed in the reverse order of their construction. [ Note: For temporaries, see 12.2. —end note ] Transfer out of a loop, out of a block, or back past an initialized variable with automatic storage duration involves the destruction of objects with automatic storage duration that are in scope at the point transferred from but not at the point transferred to. (See 6.7 for transfers into blocks). [ Note: However, the program can be terminated (by calling std::exit() or std::abort() (18.5), for example) without destroying class objects with automatic storage duration. —end note ]

I think the emphasis here is on the "(however accomplished)" part. This includes an exception (but excludes things that cause a std::terminate).


EDIT

I think a better reference is §15.2/3 Constructors and destructors [except.ctor] (emphasis mine):

If the initialization or destruction of an object other than by delegating constructor is terminated by an exception, the destructor is invoked for each of the object’s direct subobjects and, for a complete object, virtual base class subobjects, whose initialization has completed (8.6) and whose destructor has not yet begun execution, except that in the case of destruction, the variant members of a union-like class are not destroyed. The subobjects are destroyed in the reverse order of the completion of their construction. Such destruction is sequenced before entering a handler of the function-try-block of the constructor or destructor, if any.

This would include aggregate initialization (which I learned today can be called non-vacuous initialization)

...and for objects with constructors we can cite §12.6.2/12 [class.base.init](emphasis mine):

In a non-delegating constructor, the destructor for each potentially constructed subobject of class type is potentially invoked (12.4). [ Note: This provision ensures that destructors can be called for fully-constructed subobjects in case an exception is thrown (15.2). —end note ]