https://gcc.gnu.org/bugzilla/show_bug.cgi?id=87431
Jonathan Wakely <redi at gcc dot gnu.org> changed: What |Removed |Added ---------------------------------------------------------------------------- Status|RESOLVED |REOPENED Resolution|FIXED |--- --- Comment #21 from Jonathan Wakely <redi at gcc dot gnu.org> --- Sigh, there is yet another problem with this patch. Consider: #include <variant> #include <vector> struct DeletedMoves { DeletedMoves() = default; DeletedMoves(const DeletedMoves&) = default; DeletedMoves(DeletedMoves&&) = delete; DeletedMoves& operator=(const DeletedMoves&) = default; DeletedMoves& operator=(DeletedMoves&&) = delete; }; struct ThrowingCopy { ThrowingCopy() = default; ThrowingCopy(const ThrowingCopy&) { throw 1; } ThrowingCopy& operator=(const ThrowingCopy&) { throw 1; } }; int main() { using namespace std; variant<int, DeletedMoves, vector<ThrowingCopy>> v; v.emplace<2>(1); v.emplace<2>(1); } _Never_valueless<vector<ThrowingCopy>> is true, so we try to provide the strong-exception guarantee: else if constexpr (__detail::__variant::_Never_valueless_alt<type>()) { // This construction might throw: variant __tmp(in_place_index<_Np>, __il, std::forward<_Args>(__args)...); // But _Never_valueless_alt<type> means this won't: *this = std::move(__tmp); } However because is_move_assignable_v<DeletedMoves> is false the variant has no move assignment operator, only a copy assignment operator. That means the "move" assignment actually uses the variant's copy assignment operator, which performs a copy of the vector, and that can throw. If the variant's current contained value is not the same type as the one being emplaced, the copy assignment will destroy the previous value, and then if copying the vector throws we become valueless. But the variant thinks it can never become valueless, so we have undefined behaviour. We can restrict the strong exception safety guarantee to only happen when the variant has a move assignment operator: --- a/libstdc++-v3/include/std/variant +++ b/libstdc++-v3/include/std/variant @@ -1428,7 +1428,8 @@ namespace __variant this->_M_reset(); __variant_construct_by_index<_Np>(*this, __tmp); } - else if constexpr (__detail::__variant::_Never_valueless_alt<type>()) + else if constexpr (__detail::__variant::_Never_valueless_alt<type>() + && _Traits::_S_move_assign) { // This construction might throw: variant __tmp(in_place_index<_Np>, @@ -1474,7 +1475,8 @@ namespace __variant __variant_construct_by_index<_Np>(*this, __il, std::forward<_Args>(__args)...); } - else if constexpr (__detail::__variant::_Never_valueless_alt<type>()) + else if constexpr (__detail::__variant::_Never_valueless_alt<type>() + && _Traits::_S_move_assign) { // This construction might throw: variant __tmp(in_place_index<_Np>, __il, But this means that branch won't be taken for the variant type in the example above, and so we don't offer the strong exception safety guarantee for that type, so we also need to consider the extra condition in the __never_valueless() function: @@ -352,7 +352,8 @@ namespace __variant template <typename... _Types> constexpr bool __never_valueless() { - return (_Never_valueless_alt<_Types>::value && ...); + return _Traits<_Types...>::_S_move_assign + && (_Never_valueless_alt<_Types>::value && ...); } // Defines index and the dtor, possibly trivial.