c++variantnothrowstd-variant

Why `std:variant`'s `operator=(T&& t)`'s noexcept spec doesn't depend on inner types's destructor's noexcept spec?


Long title: Why std:variant's operator=(T&& t)'s noexcept specification doesn't depend on inner types's destructor's noexcept specification?

I can see on cppreference that

template <class T> variant& operator=(T&& t) noexcept(/* see below */);

is

noexcept(std::is_nothrow_assignable_v<T_j&, T> && 
std::is_nothrow_constructible_v<T_j, T>)

So this compiles:

struct FooThrow {
  ~FooThrow() noexcept(false) {throw;} 
};
static_assert(std::is_nothrow_assignable_v<std::variant<FooThrow, int>, int>);

But it calls FooThrow's destructor which is noexcept(false):

std::variant<FooThrow, int> x;
x = 3; // throws

It doesn't seem right. Am I missing something?


Solution

  • In general, standard library types do not take kindly to types that have throwing destructors. Or specifically, when a destructor actually emits an exception. There's a general rule about it ( [res.on.functions] )

    In certain cases (replacement functions, handler functions, operations on types used to instantiate standard library template components), the C ++ standard library depends on components supplied by a C ++ program. If these components do not meet their requirements, this International Standard places no requirements on the implementation.

    In particular, the effects are undefined in the following cases:

    ...

    • if any replacement function or handler function or destructor operation exits via an exception, unless specifically allowed in the applicable Required behavior: paragraph.

    Since variant::operator= has no special statement about throwing destructors, having those destructors actually throw is UB.