Exceptions in destructors and Visual Studio 2015

If you're migrating code to Visual Studio 2015, you may have run into the following warning:
warning C4297: 'A::~A': function assumed not to throw an exception but does
note: destructor or deallocator has a (possibly implicit)
    non-throwing exception specification
You may not have seen this warning with GCC or Clang, so you may think VS 2015 is just bothering you. Wrong! GCC should be warning you even more so than Visual Studio (I'll explain why), but it does not.

You may also think that throwing an exception from a destructor in C++ is inherently undefined behavior. Wrong! Throwing an exception from a destructor in C++ is extremely well defined.

In C++03, throwing an exception from a destructor works as follows:
  • If there is no exception in flight, it means the destructor is being called through forward progress. In this case an exception in the destructor causes the beginning of unwinding — backward progress — just as if the exception was thrown anywhere else. The object whose destructor threw continues to be orderly destroyed, all the subobject destructors are still called, and operator delete is still called if the object's destruction was triggered by the delete keyword.
  • If there is an exception in flight, a destructor can still throw. However, an exception thrown from a destructor cannot meet with the exception in flight. It can still pass out of a destructor and be caught by another destructor if the throwing destructor was called recursively. However, C++ does not support exception aggregation. If the two exceptions meet, such that they would have to be joined to unwind together, the program is instead terminated abnormally.
In C++11 and later:
  • Everything exactly the same as above, except that destructors now have an implicit noexcept declaration, which is deduced to be the same as the destructor that the compiler would generate. This means that a user defined destructor is noexcept(true) by default, unless it is explicitly declared noexcept(false), and unless a base class or an aggregated object declares a destructor explicitly as noexcept(false).
  • If an exception leaves a noexcept(true) destructor, the C++ standard now requires std::terminate to be called. GCC does this; Clang does this; Visual Studio 2015 does this unless you enable optimization — which of course you will for production code. If you enable optimization, then against the spec, Visual Studio 2015 appears to ignore noexcept, and allows the exception to pass through.
Even though, like other compilers, GCC will call std::terminate if an exception leaves a noexcept destructor; and even though GCC will do so more consistently than VS 2015 — the behavior doesn't go away with -O2; GCC produces absolutely no warnings about this, even with -Wall.

In this case, therefore, we have Visual Studio 2015 producing a useful warning which exposes code incorrectness, which GCC does not produce.

Why the change in C++11?

Mostly, move semantics and containers. Exceptions from destructors in stack-allocated objects are usually not problematic, assuming the destructor checks std::uncaught_exception to see if it can throw. However, because C++ supports neither exception aggregation, nor a relocatable object property, a throwing move constructor or destructor make it next-to-impossible to provide a strong exception safety guarantee when e.g. resizing a vector.

It is possible that relocatable may be supported in the future, allowing objects to be moved via trivial memcpy instead of move construction + destruction. This would make it possible to safely resize a vector containing objects whose destructors may throw. But that leaves the question of what to do when multiple destructors throw when destroying or erasing the vector. That would require exception aggregation, which in turn would be ineffective without making developers aware; and at this time, that seems not to be feasible.

It seems likely we may get relocatable some time, but probably not multi-exceptions any time soon. Planning for the next 10 years, it's best to design your code to have noexcept destructors.

What to do?

If you have code that currently throws from destructors, plausible things to do are:
  1. Band-aid to restore C++03 behavior: declare destructors noexcept(false). Not only those that trigger the warning, but also those that may call throwing code. This addresses the VS 2015 warning, and fixes behavior with compilers that should issue a warning, but do not. This is safe to do if destructors are checking std::uncaught_exception before throwing.
  2. Destructor redesign: you can comply with the spirit of C++11, and change destructors to not throw. Any errors encountered by destructors should then be logged using some logging facility, perhaps a C-style global handler. The destructor must either call only noexcept code, or must catch exceptions from throwing code.
Long-term, option 2 is more consistent with current C++ direction. In this case, the following macro may come in handy, to ensure that any code called from destructors is noexcept:
#define NoExcept(EXPR) \
    ([&]() { static_assert(noexcept(EXPR), "Expression can throw"); }, (EXPR))
This is unfortunately necessary because otherwise, you have to resort to extensive code duplication. When used as an operator, noexcept returns a boolean, so you have to test it like this:
static_assert(noexcept(INSERT_VERY_LONG_EXPRESSION), "Can throw");
INSERT_VERY_LONG_EXPRESSION;

Comments

Johan Lundberg said…
Nice post. Do you have a reference to the behavior of VS 2015 or is that based on experiments? Do you know if it change with VS2015 update 2?
denis bider said…
Hey Johan!

These findings were based on testing. I have not yet tested this with Update 2.
denis bider said…
I have retested this with VS 2015 Update 2.

I can still reproduce the noexcept being apparently optimized away under both -EHa and -O2.

The program is terminated as expected under only -EHa, or under -EHsc or -EHsc -O2.

Popular posts from this blog

"Unreachable" beauty standards

When monospace fonts aren't: The Unicode character width nightmare

Is the internet ready for DMARC with p=reject?