warning C4297: 'A::~A': function assumed not to throw an exception but doesYou 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.
note: destructor or deallocator has a (possibly implicit)
non-throwing exception specification
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 thedelete
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.
- 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 isnoexcept(true)
by default, unless it is explicitly declarednoexcept(false)
, and unless a base class or an aggregated object declares a destructor explicitly asnoexcept(false)
. - If an exception leaves a
noexcept(true)
destructor, the C++ standard now requiresstd::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 ignorenoexcept
, and allows the exception to pass through.
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 checksstd::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:- 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 checkingstd::uncaught_exception
before throwing. - 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.
noexcept
:#define NoExcept(EXPR) \This is unfortunately necessary because otherwise, you have to resort to extensive code duplication. When used as an operator,
([&]() { static_assert(noexcept(EXPR), "Expression can throw"); }, (EXPR))
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;
Showing 3 out of 3 comments, oldest first:
Comment on Apr 12, 2016 at 22:08 by Unknown
Comment on Apr 13, 2016 at 09:38 by denisbider
These findings were based on testing. I have not yet tested this with Update 2.
Comment on Apr 26, 2016 at 06:42 by denisbider
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.