Destructors are not always called in case of exceptions!

Wrong assumption: when an exception occurs, a stack unwinding occurs, which means that the relevant destructors of objects are called in the appropriate order.

My wrong assumption

Suppose we have this snippet:

1
2
3
4
5
6
7
8
9
struct A {
[[ noreturn ]] void op() { throw 1; }
~A() { std::cout << "Deleting obj...\n"; }
};

int main() {
A a;
a.op(); // The dtor might never be called!
}

Is a‘s destructor called when the exception is thrown? I thought so; I was wrong. This is the output of the snippet above:

terminate called after throwing an instance of 'int'
Aborted (core dumped)

Explanation

When op() is called, the exception is thrown, and there is no handle for it; it gets out of the main function, so that std::terminate is called and the program aborts (see this question on StackOverflow). From the standard ([except.throw]):

When an exception is thrown, control is transferred to the nearest handler with a matching type; “nearest” means the handler for which the compound-statement or ctor-initializer following the try keyword was most recently entered by the thread of control and not yet exited.

and later on:

If no matching handler is found, the function std::terminate() is called; whether or not the stack is unwound before this call to std::terminate() is implementation-defined

Correction

A way to have this exception handled is to wrap the construction of A and the call to op() into a try block:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct A {
[[ noreturn ]] void op() { throw 1; }
~A() { std::cout << "Deleting obj...\n"; }
};

int main() {
try {
A a;
a.op(); // the dtor is guaranteed to be called
} catch(...) {
throw;
}
}
Deleting obj...
terminate called after throwing an instance of 'int'
Aborted (core dumped)

Notice that just putting the constructor of A out of the try gets us back to the destructor not being called! Read carefully the first snippet from the standard:

… “nearest” means the handler for which the compound-statement or ctor-initializer following the try keyword.

So if we had:

1
2
3
4
5
6
7
8
9
// This might not call the dtor!
int main() {
A a;
try {
a.op();
} catch(...) {
throw;
}
}

there would be no ctor-initializer (nor compound-statement, but that wasn’t there before either) after the try.

Apparently, whether or not the stack is unwound is implementation-defined, meaning that on other configurations the result of my experiment wouldn’t be the same. But why risking?