Reputation: 722
TL;DR: see compilers disagree on code by Godbolt link: https://godbolt.org/z/f7G6PTEsh
Should std::variant
be nothrow destructible when its alternative has potentially throwing destructor? Clang and GCC seem to disagree on it (probably different standard library implementations on Godbolt?), MSVC thinks it should. cppreference says variant's destructor does not have noexcept specifications, and in that case destructor should be unconditionally noexcept unless it has potentially throwing members or bases (https://en.cppreference.com/w/cpp/language/noexcept_spec), which is not specified for variant (duh!).
The reason I'm asking is not to mess with throwing stuff out of destructors, but to gradually refactor weird legacy stuff that is minimized to the following snippet ("exception specification of overriding function is more lax than the base version"), and also related bug on dated compiler that we can't update yet.
#include <type_traits>
#include <variant>
struct S
{
~S() noexcept(false);
};
static_assert(std::is_nothrow_destructible_v<std::variant<S>>); // assertion failed with clang
struct Y
{
virtual ~Y() {}
};
struct Z: public Y
{
virtual ~Z() {}
// clang error because of this member:
// exception specification of overriding function is more lax than the base version
std::variant<int, S> member;
}
Upvotes: 8
Views: 305
Reputation: 119034
GCC is wrong. According to [res.on.exception.handling]/3:
Destructor operations defined in the C++ standard library shall not throw exceptions. Every destructor in the C++ standard library shall behave as if it had a non-throwing exception specification.
How the destructor of std::variant
is declared is beside the point; the library implementor is responsible for ensuring that the destructor behaves as if it were noexcept
, wherever the presence or absence of such noexceptness can be detected.
Note that types used to instantiate standard library templates are not allowed to throw exceptions ([res.on.functions]/2.4) but that's not necessarily relevant to your example. The rule doesn't say that your S
type must have a noexcept
destructor. It just says that it's not allowed to actually throw. Violating this rule leads to UB, only if an execution that results in an exception escaping the destructor actually happens.
Upvotes: 4
Reputation: 39385
std::variant
doesn't work with throwing destructorsFirstly, this code could have undefined behavior because all variant alternatives must meet the Cpp17Destructible requirement ([variant.variant.general] p2), which mandates ([tab:cpp17.destructible]) for the expression u.~T()
:
All resources owned by
u
are reclaimed, no exception is propagated.
If ~S()
threw an exception, the behavior would be undefined, although you can in principle have a noexcept(false)
destructor with std::variant
as long as it never throws.
I don't believe that the result of static_assert(std::is_nothrow_destructible_v<std::variant<S>>);
is specified; the standard simply defines std::variant::~variant()
as:
constexpr ~variant();
... which is noexcept
depending on whether data members have a throwing destructor (see below for more).
Data members are an implementation detail.
However, this also appears to be a GCC bug because [except.spec] p8 states:
The exception specification for an implicitly-declared destructor, or a destructor without a noexcept-specifier, is potentially-throwing if and only if any of the destructors for any of its potentially constructed subobjects has a potentially-throwing exception specification or the destructor is virtual and the destructor of any virtual base class has a potentially-throwing exception specification.
In libstdc++, the destructor is explicitly defaulted (<variant>
line 1512):
_GLIBCXX20_CONSTEXPR ~variant() = default;
In libstdc++, the actual member is stored within an _Uninitialized
subobject, as
union {
_Empty_byte _M_empty;
_Type _M_storage;
};
// ...
~_Uninitialized() {}
A way to reproduce this situation is:
#include <type_traits>
struct D {
~D() noexcept(false);
};
struct B {
union {
char c;
D d;
};
~B() {}
};
static_assert(std::is_nothrow_destructible_v<B>);
The assertion passes only for GCC, and fails for other compilers (https://godbolt.org/z/1doaTrxsG).
Since S
has a potentially throwing subobject of type D
(indirectly), S
should not be nothrow-destructible.
This is a known compiler bug; see GCC Bug #115222.
Upvotes: 6