3585. Variant converting assignment with immovable alternative

Section: 22.6.3.4 [variant.assign] Status: C++23 Submitter: Barry Revzin Opened: 2021-09-01 Last modified: 2023-11-22

Priority: Not Prioritized

View other active issues in [variant.assign].

View all other issues in [variant.assign].

View all issues with C++23 status.

Discussion:

Example originally from StackOverflow but with a more reasonable example from Tim Song:

#include <variant>
#include <string>

struct A {
  A() = default;
  A(A&&) = delete;
};

int main() {
  std::variant<A, std::string> v;
  v = "hello";
}

There is implementation divergence here: libstdc++ rejects, libc++ and msvc accept.

22.6.3.4 [variant.assign] bullet (13.3) says that if we're changing the alternative in assignment and it is not the case that the converting construction won't throw (as in the above), then "Otherwise, equivalent to operator=(variant(std::forward<T>(t)))." That is, we defer to move assignment.

variant<A, string> isn't move-assignable (because A isn't move constructible). Per the wording in the standard, we have to reject this. libstdc++ follows the wording of the standard.

But we don't actually have to do a full move assignment here, since we know the situation we're in is changing the alternative, so the fact that A isn't move-assignable shouldn't matter. libc++ and msvc instead do something more direct, allowing the above program.

[2021-09-20; Reflector poll]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

[2021-10-14 Approved at October 2021 virtual plenary. Status changed: Voting → WP.]

Proposed resolution:

This wording is relative to N4892.

  1. Modify 22.6.3.4 [variant.assign] as indicated:

    [Drafting note: This should cover the case that we want to cover: that if construction of Tj from T throws, we haven't yet changed any state in the variant.]

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

    -11- Let Tj be a type that is determined as follows: build an imaginary function FUN(Ti) for each alternative type Ti for which Ti x[] = {std::forward<T>(t)}; is well-formed for some invented variable x. The overload FUN(Tj) selected by overload resolution for the expression FUN(std::forward<T>(t)) defines the alternative Tj which is the type of the contained value after assignment.

    -12- Constraints: […]

    -13- Effects:

    1. (13.1) — If *this holds a Tj, assigns std::forward<T>(t) to the value contained in *this.

    2. (13.2) — Otherwise, if is_nothrow_constructible_v<Tj, T> || !is_nothrow_move_constructible_v<Tj> is true, equivalent to emplace<j>(std::forward<T>(t)).

    3. (13.3) — Otherwise, equivalent to operator=(variant(std::forward<T>(t)))emplace<j>(Tj(std::forward<T>(t))).