smitsyn
smitsyn

Reputation: 722

Should std::variant be nothrow destructible when its alternative has potentially throwing destructor?

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

Answers (2)

Brian Bi
Brian Bi

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

Jan Schultke
Jan Schultke

Reputation: 39385

std::variant doesn't work with throwing destructors

Firstly, 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.

But why do compilers disagree?

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

Related Questions