Variant: a type-safe union for C++17 (v8).

Table of Contents

Variant is the very spice of life,
That gives it all its flavor.- William Cowper's "The Task", or actually a variant thereof

Introduction

C++17 needs a type-safe union:

Lets not make the same mistake we made with std::optional by putting this library into a TS. We waited three years where no substantial feedback or discussion occurred, and then moved it into the IS virtually unchanged. Meanwhile, the C++ community suffered, and we continue to suffer from lack of this essential vocabulary type in interfaces.

The implications of the consensus variant design are well understood and have been explored over several LEWG discussions, over a thousand emails, a joint LEWG/EWG session, and not to mention 12 years of experience with Boost and other libraries. The last major change made to the proposal was non-breaking and added exception throws where previously there was undefined behavior. Since then, all suggested modifications have been cosmetic, rehashes of older discussions, or would be handled just as well by defect resolutions.

The C++ community should not wait three years for a widely useful library that is already done, fits its purpose, and has had such extensive review. There is a low chance that we will regret including variant in C++17, but a high chance that we will regret omitting it.

This proposal attempts to apply the lessons learned from optional(1). It behaves as below:

Strong consensus to rename valid() to !corrupted_by_exception() (14 votes, runner-up: 7 votes) that was later on changed to !valueless_by_exception() after a discussion and based on a poll on the LWG and LEWG email lists, with 32 responses.

Revision history

Differences to revision 1 (N4218)

As requested by the LEWG review in Urbana, this revision

considerably expands the discussion of why this proposal allows the variant to be empty;

Rename valid() to !valueless_by_exception(), following the strong recommendation from a L(E)WG poll.

Remove tuple_find: it was not relevant for using variant as that already provides index- and type-based accesses; it was a considerable fraction of the proposed wording; it warrants a dedicated design paper, should someone wish to have it.

Differences to revision 6 (P0088R1)

Bring back tuple_not_found that got removed by mistake. Call it variant_npos.

Implement wording comments from Jacksonville LWG.

Rename tuple_size to variant_size, tuple_element to variant_alternative to clarify that this is not tuple-like. This avoids a clash with structured binding. The committee seems to have changed its common mind regarding these templates, reverting LEWG's decision from the first revision.

Do not implicitly force the variantvalueless_by_exception if an exception is thrown during emplace / construction; merely state that it might become valueless_by_exception.

Differences to revision 7 (P0088R2)

Resolve noexcept(see below) for swap.

Added a missing std::move().

Replace uses of "must".

Some minor wording tweaks: strike "regular" in "regular overload resolution", stray experimental:: removed, is_constructible_v<T_j, T&&> is now spelled is_constructible_v<T_j, T>; some postconditions that are already specified in the equivalent Effects elements have been removed. These tweaks have not been highlighted in blue.

Discussion

Additional empty state

LEWG opted against introducing an explicit additional variant state, representing its invalid (and possibly empty, default constructed) state. This is meant to simplify the variant use: as getting a variant into the invalid state is sufficiently difficult, it was felt that there is no need to regularly check for a variant becoming invalid. This prevents all get<int>(v) calls from being protected by if (v.valid()).

Visibility of the Invalid State

The variant's invalid state needs to be visible: accessing its contents or visiting it will violate preconditions; users must be able to verify that a variant is not in this state.

When in the invalid state, index() returns variant_npos; variant provides valid() as a usability feature.

This usually does not need to be checked given how rare the invalid case is. It (generally) keeps a variant with N alternatives as an N-state type.

Empty state and default construction

Default construction of a variant should be allowed, to increase usability for instance in containers. LEWG opted against a variant default-initialized into its invalid state, to make invalid variants really rare.

Instead, the variant can be initialized with the first alternative (similar to the behavior of initialization of a union) only if that is default constructible. For cases where this behavior should be explicit, and for cases where no such default constructible alternative exists, there is a separate type monostate that can be used as first alternative, to explicitly enable default construction.

Feature Test

No header called variant exists; testing for this header's existence is thus sufficient.

Proposed wording

The insertions and deletions in this section describe the changes to the C++ Working Paper. Grayish background indicates proposed wording.

Variant Objects

Insert a new element in Table 14, C++ library headers of [general.namespaces], named <variant>.

Insert a new section:

? Variants [variant]

?.1 In general [variant.general]

A variant object holds and manages the lifetime of a value. If the variant holds a value, that value's type has to be one of the template argument types given to variant. These template arguments are called alternatives.

Any instance of variant at any given time either holds a value of one of its alternative types, or it holds no value. When an instance of variant holds a value of alternative type T, it means that a value of type T, referred to as the variant object's contained value, is allocated within the storage of the variant object. Implementations are not permitted to use additional storage, such as dynamic memory, to allocate the contained value. The contained value shall be allocated in a region of the variant storage suitably aligned for all types in Types.... It is implementation defined whether over-aligned types are supported.

All types in Types... shall be (possibly cv-qualified) object types, (possibly cv-qualified) void, or references. [Note: Implementations could decide to store references in a wrapper. — end note]

?.3.1 Constructors [variant.ctor]

In the descriptions that follow, let i be in the range [0,sizeof...(Types)), and T_i be the ith type in Types....

constexpr variant() noexcept(see below);

Effects:

Constructs a variant holding a value-initialized value of type T_0.

Postconditions:

valueless_by_exception() is false and index() is 0.

Throws:

Any exception thrown by the value initialization of T_0.

Remarks:

This function shall be constexpr if and only if the value initialization of the alternative type T_0 would satisfy the requirements for a constexpr function. The expression inside noexcept is equivalent to is_nothrow_default_constructible_v<T_0>. This function shall not participate in overload resolution unless is_default_constructible_v<T_0> is true. [Note: see also class monostate. — end note]

variant(const variant& w);

Effects:

If w holds a value, initializes the variant to hold the same alternative as w and direct-initializes the contained value with get<j>(w), where j is w.index(). Otherwise, initializes the variant to not hold a value.

Throws:

Any exception thrown by direct-initializing any T_i for all i.

Remarks:

This function shall not participate in overload resolution unless is_copy_constructible_v<T_i> is true for all i.

variant(variant&& w) noexcept(see below);

Effects:

If w holds a value, initializes the variant to hold the same alternative as w and direct-initializes the contained value with get<j>(std::move(w)), where j is w.index(). Otherwise, initializes the variant to not hold a value.

Throws:

Any exception thrown by move-constructing any T_i for all i.

Remarks:

The expression inside noexcept is equivalent to the logical AND of is_nothrow_move_constructible_v<T_i> for all i.
This function shall not participate in overload resolution unless is_move_constructible_v<T_i> is true for all i.

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

Let T_j be a type that is determined as follows: build an imaginary function FUN(T_i) for each alternative type T_i. The overload FUN(T_j) selected by overload resolution for the expression FUN(std::forward<T>(t)) defines the alternative T_j which is the type of the contained value after construction.

Effects:

Initializes *this to hold the alternative type T_j and direct-initializes the contained value as if direct-non-list-initializing it with std::forward<T>(t).

Postconditions:

holds_alternative<T_j>(*this) is true.

Throws:

Any exception thrown by the initialization of the selected alternative T_j.

Remarks:

This function shall not participate in overload resolution unless is_same_v<decay_t<T>, variant> is false, unless is_constructible_v<T_j, T> is true, and unless the expression FUN(std::forward<T>(t)) (with FUN being the above-mentioned set of imaginary functions) is well formed.
[Note:

variant<string, string> v("abc");

is ill-formed, as both alternative types have an equally viable constructor for the argument. — end note]
The expression inside noexcept is equivalent to is_nothrow_constructible_v<T_j, T>. If T_j's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

Initializes the contained value of type T with the arguments std::forward<Args>(args)....

Postcondition:

holds_alternative<T>(*this) is true.

Throws:

Any exception thrown by calling the selected constructor of T.

Remarks:

This function shall not participate in overload resolution unless there is exactly one occurrence of T in Types... and is_constructible_v<T, Args...> is true. If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

Initializes the contained value as if constructing an object of type T with the arguments il, std::forward<Args>(args)....

Postcondition:

holds_alternative<T>(*this) is true.

Throws:

Any exception thrown by calling the selected constructor of T.

Remarks:

This function shall not participate in overload resolution unless there is exactly one occurrences of T in Types... and is_constructible_v<T, initializer_list<U>&, Args...> is true. If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

Initializes the contained value as if constructing an object of type T_I with the arguments std::forward<Args>(args)....

Postcondition:

index() is I.

Throws:

Any exception thrown by calling the selected constructor of T_I.

Remarks:

This function shall not participate in overload resolution unless I is less than sizeof...(Types) and is_constructible_v<T_I, Args...> is true. If T_I's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

Initializes the contained value as if constructing an object of type T_I with the arguments il, std::forward<Args>(args)....

Postcondition:

index() is I.

Remarks:

This function shall not participate in overload resolution unless I is less than sizeof...(Types) and is_constructible_v<T_I, initializer_list<U>&, Args...> is true. If T_I's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

Equivalent to the preceding constructors except that the contained value is constructed with uses-allocator construction (20.7.7.2).

?.3.2 Destructor [variant.dtor]

~variant();

Effects:

If valueless_by_exception() is false, destroys the currently contained value.

Remarks:

If is_trivially_destructible_v<T_i> == true for all T_i then this destructor shall be a trivial destructor.

?.3.3 Assignment [variant.assign]

variant& operator=(const variant& rhs);

Effects:

If neither *this nor rhs holds a value, there is no effect. Otherwise,

if *this holds a value but rhs does not, destroys the value contained in *this and sets *this to not hold a value. Otherwise,

if index() == rhs.index(), assigns the value contained in rhs to the value contained in *this. Otherwise,

copies the value contained in rhs to a temporary, then destroys any value contained in *this. Sets *this to hold the same alternative index as rhs and initializes the value contained in *this as if direct-non-list-initializing an object of type T_j with std::forward<T_j>(TMP), with TMP being the temporary and j being rhs.index().

Returns:

*this.

Postconditions:

index() == rhs.index()

Remarks:

This function shall not participate in overload resolution unless is_copy_constructible_v<T_i> && is_move_constructible_v<T_i> && is_copy_assignable_v<T_i> is true for all i.

If an exception is thrown during the call to T_j's copy assignment, the state of the contained value is as defined by the exception safety guarantee of T_j's copy assignment; index() will be j.

If an exception is thrown during the call to T_j's copy construction (with j being rhs.index()), *this will remain unchanged.

If an exception is thrown during the call to T_j's move construction, the variant will hold no value.

variant& operator=(variant&& rhs) noexcept(see below);

Effects:

If neither *this nor rhs holds a value, there is no effect. Otherwise,

if *this holds a value but rhs does not, destroys the value contained in *this and sets *this to not hold a value. Otherwise,

if index() == rhs.index(), assigns get<j>(std::move(rhs)) to the value contained in *this, with j being index(). Otherwise,

destroys any value contained in *this. Sets *this to hold the same alternative index as rhs and initializes the value contained in *this as if direct-non-list-initializing an object of type T_j with get<j>(std::move(rhs)) with j being rhs.index().

Returns:

*this.

Remarks:

This function shall not participate in overload resolution unless is_move_constructible_v<T_i> && is_move_assignable_v<T_i> is true for all i. The expression inside noexcept is equivalent to: is_nothrow_move_constructible_v<T_i> && is_nothrow_move_assignable_v<T_i> for all i.
If an exception is thrown during the call to T_j's move construction (with j being rhs.index()), the variant will hold no value. If an exception is thrown during the call to T_j's move assignment, the state of the contained value is as defined by the exception safety guarantee of T_j's move assignment; index() will be j.

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

Let T_j be a type that is determined as follows: build an imaginary function FUN(T_i) for each alternative type T_i. The overload FUN(T_j) selected by overload resolution for the expression FUN(std::forward<T>(t)) defines the alternative T_j which is the type of the contained value after assignment.

Effects:

If *this holds a T_j, assigns std::forward<T>(t) to the value contained in *this.
Otherwise, destroys any value contained in *this, sets *this to hold the alternative type T_j as selected by the imaginary function overload resolution described above, and direct-initializes the contained value as if direct-non-list-initializing it with std::forward<T>(t).

Postcondition:

holds_alternative<T_j>(*this) is true, with T_j selected by the imaginary function overload resolution described above.

Returns:

*this.

Remarks:

This function shall not participate in overload resolution unless is_same_v<decay_t<T>, variant> is false, unless is_assignable_v<T_j&, T> && is_constructible_v<T_j, T> is true, and unless the expression FUN(std::forward<T>(t)) (with FUN being the above-mentioned set of imaginary functions) is well formed.
[Note:

variant<string, string> v;
v = "abc";

is ill-formed, as both alternative types have an equally viable constructor for the argument. — end note]
The expression inside noexcept is equivalent to: is_nothrow_assignable_v<T_j&, T> && is_nothrow_constructible_v<T_j, T>.
If an exception is thrown during the assignment of std::forward<T>(t) to the value contained in *this, the state of the contained value and t are as defined by the exception safety guarantee of the assignment expression; valueless_by_exception() will be false. If an exception is thrown during the initialization of the contained value, the variant object might not hold a value.

?.3.4 Modifiers [variant.mod]

template <class T, class... Args> void emplace(Args&&... args);

Effects:

Equivalent to emplace<I>(std::forward<Args>(args)...) where I is the zero-based index of T in Types....

Remarks:

This function shall not participate in overload resolution unless is_constructible_v<T, Args...> is true, and T occurs exactly once in Types....

Equivalent to emplace<I>(il, std::forward<Args>(args)...) where I is the zero-based index of T in Types....

Remarks:

This function shall not participate in overload resolution unless is_constructible_v<T, initializer_list<U>&, Args...> is true, and T occurs exactly once in Types....

template <size_t I, class... Args> void emplace(Args&&... args);

Requires:

I < sizeof...(Types)

Effects:

Destroys the currently contained value if valueless_by_exception() is false. Then direct-initializes the contained value as if constructing a value of type T_I with the arguments std::forward<Args>(args)....

Postcondition:

index() is I.

Throws:

Any exception thrown during the initialization of the contained value.

Remarks:

This function shall not participate in overload resolution unless is_constructible_v<T_I, Args...> is true.
If an exception is thrown during the initialization of the contained value, the variant might not hold a value.

Destroys the currently contained value if valueless_by_exception() is false. Then direct-initializes the contained value as if constructing an object of type T_I with the arguments il, std::forward<Args>(args)....

Postcondition:

index() is I.

Throws:

Any exception thrown during the initialization of the contained value.

Remarks:

This function shall not participate in overload resolution unless is_constructible_v<T_I, initializer_list<U>&, Args...> is true.
If an exception is thrown during the initialization of the contained value, the variant might not hold a value.

?.3.5 Value status [variant.status]

constexpr bool valueless_by_exception() const noexcept;

Effects:

Returns false if and only if the variant holds a value. [Note:
A variant might not hold a value if an exception is thrown during a type-changing assignment or emplacement. The latter means that even a variant<float,int> can become valueless_by_exception(), for instance by

?.3.6 Swap [variant.swap]

if index() == rhs.index(), calls swap(get<i>(*this), get<i>(rhs)) where i is index(). Otherwise,

exchanges values of rhs and *this.

Throws:

Any exception thrown by swap(get<i>(*this), get<i>(rhs)) with i being index() and variant's move constructor and move assignment operator.

Remarks:

This function shall not participate in overload resolution unless is_swappable_v<T_i> is true for all i.
If an exception is thrown during the call to function swap(get<i>(*this), get<i>(rhs)), the states of the contained values of *this and of rhs are determined by the exception safety guarantee of swap for lvalues of T_i with i being index(). If an exception is thrown during the exchange of the values of *this and rhs, the states of the values of *this and of rhs are determined by the exception safety guarantee of variant's move constructor and move assignment operator.
The expression inside noexcept is equivalent to the logical AND of is_nothrow_move_constructible_v<T_i> && is_nothrow_swappable_v<T_i> for all i.

?.4 variant helper classes [variant.helper]

template <class T> struct variant_size;

Remarks:

All specializations of variant_size<T> shall meet the UnaryTypeTrait requirements (20.10.1) with a BaseCharacteristic of integral_constant<size_t, N> for some N.

Let VS denote variant_size<T> of the cv-unqualified type T. Then each of the three templates shall meet the UnaryTypeTrait requirements (20.10.1) with a BaseCharacteristic of
integral_constant<size_t, VS::value>.

Let VA denote variant_alternative<I, T> of the cv-unqualified type T. Then each of the three templates shall meet the TransformationTrait requirements (20.10.1) with a member typedef type that names
the following type:

Template specializations of in_place_type_t are empty structure types used as unique types to disambiguate constructor overloading. They signal (through the template parameter) the alternative to be constructed. Specifically, variant has a constructor with in_place_type_t<T> as the first argument followed by an argument pack; this indicates that T should be constructed in-place (as if by a call to a placement new expression) with the forwarded argument pack as parameters. If a variant's Types... has multiple occurrences of T, in_place_index_t shall be used.

Template specializations of in_place_index_t are empty structure types used as unique types to disambiguate constructor overloading, and signaling (through the template parameter) the alternative to be constructed. Specifically, variant has a constructor with in_place_index_t<I> as the first argument followed by an argument pack; this indicates that T_I should be constructed in-place (as if by a call to a placement new expression) with the forwarded argument pack as parameters.

The return type is the common type of all possible INVOKE expressions of the Effects element.

Throws:

bad_variant_access if any variant in vars is valueless_by_exception().

Complexity:

For sizeof...(Variants) <= 1, the invocation of the callable object is implemented in constant time, i.e. it does not depend on sizeof...(Types). For sizeof...(Variants) > 1, the invocation of the callable object has no complexity requirements.

?.9 Class monostate[variant.monostate]

struct monostate{};

The class monostate can serve as a first alternative type for a variant to make the variant type default constructible.