Destructors: Exception Safety Language Rules in C++11/17
Summary of the C++11/17 language rules related to destructor exception safety. We’ll look at the language rules as informal as possible without going into too much legalese.
Rule 1: Orderly destruction on (unexceptional) completion
When functions (or code blocks) complete normally, e.g. by reaching end or returning, automatic objects (created on the stack) have their destructors called in an orderly fashion. The destruction order is the reverse order to which objects would have been constructed. The reason for this reverse order is the assumptions that objects declared later might depend on earlier declared ones, hence objects created later are destroyed first.
Example: In the code below, when exiting fn
, destructors for y
and x
get
automatically called, precisely in this order.
1
2
3
4
5
void fn()
{
X x; // (2)
Y y; // (1)
}
Rule 2: Orderly destruction for subobjects
Orderly destruction applies to subobjects:
- Members and bases get destroyed in the reverse order of declaration
- Elements of an array get destroyed from the largest to the smallest index
Example: When an instance of X
is destroyed, the order of destruction is:
first the body of the X
destructor, then the destructors for members b
and
a
, and then the destructors for the bases Z
and Y
.
1
2
3
4
5
6
7
8
class X :
public Y, // (5)
public Z // (4)
{
A a; // (3)
B b; // (2)
~X(){} // (1)
};
Rule 3: Stack unwinding
Stack unwinding is a term with a specific meaning. It refers to what happens
when an exception is thrown and control is passed to it’s corresponding catch
block. During stack unwinding automatic objects that were created since the
try
blocked was entered get destroyed in an orderly fashion.
Example: In the code below, when the exception is thrown, y
and x
get
destroyed during stack unwinding (but not w
and z
), before resuming from
the catch
block.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void fn(int i)
{
W w; // outside try
try
{
X x; // (3)
Y y; // (2)
if (i < 0)
{
throw std::exception(); // (1)
}
Z z; // not created if above throws
}
catch (const std::exception &)
{ // (4)
}
}
Rule 5: std::terminate
When std::terminate
is called the program will stop abruptly for all intents
and purposes. The programmer can call std::teminate
explicitely, or it will
be called automatically in some cases.
Example: In the code below, if at runtime there is not enough memory to
allocate the array for the vector a std::bad_alloc
is thrown from within the
vector constructor. The exception would normally travel outside the function
fn
, but because of noexcept
the program will terminate.
1
2
3
4
void fn(int i) noexcept // (2)
{
std::vector x(i); // (1)
}
Rule 6: Destructors are noexcept by default
This is new rule in C++11. Destructors are noexcept
by default. That is true
unless you specified noexcept(true)
for it (or any subobjects). Combined with
the rule above, if exceptions propagate outside destructors, this will trigger
program termination by default.
Example: In the code below, the destructor of X
will terminate the program.
1
2
3
4
5
6
7
struct X
{
~X() // (2) noexcept by default causes termination
{
throw std::exception(); // (1)
}
};
Example: In the code below the exception in the destructor will not terminate.
The destructor of x
is called during stack unwinding, but the second
exception thrown inside it’s destructor is caught before leaving the
destructor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct X
{
~X()
{ // (2)
try
{
throw std::exception(); // (3)
}
catch (const std::exception &)
{ // (4)
}
}
};
void fn()
{
try
{
X x;
throw std::exception(); // (1)
}
catch (const std::exception &)
{ // (5)
}
}
Rule 7: Exception exiting destructor
Exceptions can exit a destructor. Because of the previous rule, practically one
needs to explicitely specify noexcept(false)
for the destructor. If an
exception exits a destructor during stack unwinding then the program is
terminated. But if the program is not terminated the rules of orderly
destruction apply.
Example: Let’s first look at how an exception can exit a destructor and NOT
terminate. The destructor of x
gets invoked because or normal block
completion, then it throws. Because of the noexcept(false)
specifier the
expection will propagate, and because this time we’re not yet stack unwinding
it will not terminate.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct X
{
~X() noexcept(false) // (2)
{
throw std::exception(); // (1)
}
};
void fn()
{
try
{
X x;
}
catch (const std::exception &)
{ // (3)
}
}
Example: Slight variations might terminate. Below by throwing an exception
inside fn
the destructor for x
will be called during stack unwinding and
it’s exception will cause program termination
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct X
{
~X() noexcept(false) // (3) -> terminate
{
throw std::exception(); // (2)
}
};
void fn()
{
try
{
X x;
throw std::exception(); // (1)
}
catch (const std::exception &)
{
}
}
Example: Another slight variation that terminates. Below the destructor for b
is fine, it throws, but the destructor for a
exits with throw during stack
unwinding and it will cause program termination.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct X
{
~X() noexcept(false) // (2) (4) -> terminate
{
throw std::exception(); // (1) (3)
}
};
void fn()
{
try
{
X a;
X b;
}
catch (const std::exception &)
{
}
}
Example: However this example is fine again. The destructor of ‘y’ is called
before stack unwinding so together with noexcept(false) it will be fine if it
throws. And it will throw because delete
will throw because of the destructor
of X
. In addition, it will not leak because delete
will free the memory
even if the destructor throws.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct X
{
~X() noexcept(false) // (5)
{ // (3)
throw std::exception(); //(4)
}
};
struct Y
{
X * x = new X();
~Y() noexcept(false) // (6)
{ // (1)
delete x; // (2)
}
};
void fn()
{
try
{
Y y;
}
catch (const std::exception &)
{ // (7)
}
}
NOTE: std::uncaught_exceptions
can be used to detect stack unwinding. The s
at the end (as in plural) is very important. The version without s
has
problems and is deprecated.
References
The March 2017 C++ standard working draft (free, but not exactly C++17)
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf