| Document Number: | P0308R0 |
| Date: | 2016-03-16 |
| Reply-to: | Peter Dimov <pdimov@pdimov.com> |
| Audience: | Library Evolution, Library |
This paper argues in section III that when variant's contained types have noexcept move constructors,
variant shall never be valueless, that is, the specification should statically guarantee
that valueless_by_exception() will never return true.
It then proposes, in section IV, a way to extend these guarantees to types such as std::list that are not guaranteed
to have a noexcept move constructor, by introducing the concept of pilfering constructor.
Finally, in section V, it ventures a suggestion that at this point, we might as well get rid of valueless_by_exception
altogether.
The variant consensus at present, reflected in D0088R2.17, its most recent specification at time of writing,
can be summarized by the following quotes by Tony Van Eerd:
The current variant basically does everything:
OK, what doesn't it do.
- "never" empty
- no extra memory
- etc
It doesn't offer the strong exception guarantee if the move constructor throws.
That's it.
Now ask yourself:
That second part is important. Basically it never happens, or if it does, you have bigger problems because you are so out of memory that you can't allocate 32 bytes for a node or something. (ie list constructor in some implementations). We are trying to solve a problem that doesn't really need a solution, but does need an answer, and one better than UB.
- how many move constructors are not noexcept
- for move constructors that are not noexcept, how many ever actually throw?
So, yeah, double buffering would be a solution, but you are paying a cost for something that never actually happens.
Or you add an empty state, and pay the programmer cost (of dealing with empty) for something that never happens.
Or you have two variants, and pay the cost of confusion, for something that never happens.
We, as a committee, want perfection and want to be concerned about the corner cases, but in reality, they never happen.
and David Sankel:
If a developer conforms to the sane subset of C++ where move constructors don't throw, then their variants won't get into the valueless state.
In other words, the current consensus acknowledges that variant getting in the valueless state is undesirable,
and I absolutely agree.
I however have two objections to the above quotes. First, the statements
variant doesn't offer the strong exception guarantee if the move constructor throws."
are simply false under the current specification. variant habitually does not give the strong exception
guarantee on assignment, and can go into the valueless state, even when the move constructors of the contained types don't throw.
Second, I do not consider the "will never happen" philosophy good enough for a standard C++ component. It's fine for a TS, which is meant to be experimental, gather experience, and can be fixed when a defect is discovered without regard to code being broken. Once a component gets into the C++ standard, changing it becomes very hard.
"Will never happen" can be, pragmatically speaking, the right strategy under many circumstances, when the cost of the solution outweighs the cost of the problem. It does have its disadvantages though, one of which is that "never happen" scenarios, being extremely rare, are never tested and therefore tend to occur in production when their costs are high. (Insert Ariane 5 reference here.)
That is why some programmers prefer to rely on static (compile-time) guarantees that the scenarios that "never happen"
do indeed never happen, and it is my opinion that it is a requirement for a C++17 variant to provide such a
static, compile-time, guarantee that it will never go into a valueless state if certain restrictions, which can be checked
at compile time, are met.
Axel Naumann prefers a different approach:
I want the wording to allow your suggestions without requiring them.
but I respectfully disagree. "Allowed but not required" is not good enough. First, "allowed but not required" does not give compile-time guarantees. Second, it hampers portability. Third, the wording is subtle and this makes it possible for what is intended to allow but not require to turn out to disallow.
The previous section concluded that variant should provide a static, compile-time, guarantee that it will never go into a valueless
state if certain restrictions, which can be checked at compile time, are met. What should those restrictions be?
Unsurprisingly, and in agreement with the existing prevailing opinion, that the move constructors of the contained types are noexcept.
(Note that move assignments being noexcept is not required. One might naively think that a type that has
a noexcept move constructor would also have a noexcept move assignment so we might as well require that
with no loss of generality, but as usual, the standard library has a surprise for us, in that std::vector's
move assignment is not necessarily noexcept.)
What do we need to change in D0088R2.17 to fulfill this requirement?
There are only two ways for a variant to become valueless: assignment and emplace. Let's consider all their variations in turn.
variant& operator=(const variant& rhs);Effects: Let
jberhs.index().
- If neither
*thisnorrhsholds a value, there is no effect. Otherwise- if
*thisholds a value butrhsdoes not, destroys the value contained in*thisand sets*thisto not hold a value. Otherwise,- if
index() ==, assigns the value contained inrhs.index()j && is_nothrow_copy_assignable_v<T_j>rhsto the value contained in*this. Otherwise,- if
index() == j && is_nothrow_move_assignable_v<T_j>, copies the value contained inrhsto a temporaryTMP, then assignsstd::forward<T_j>(TMP)to the value contained in*this. Otherwise,- if
index() == j && !is_nothrow_move_constructible_v<T_j>, assigns the value contained inrhsto the value contained in*this. Otherwise,- copies the value contained in
rhsto a temporaryTMP, then destroys any value contained in*this. Sets*thisto hold the same alternative index asrhsand initializes the value contained in*thisas if direct-non-list-initializing an object of typeT_jwithstd::forward<T_j>(TMP), with.TMPbeing the temporary andjbeingrhs.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>istruefor alli.
- 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 ofT_j's copy assignment;index()will bej.- If an exception is thrown during the call to
T_j's copy constructor (withjbeingrhs.index()),*thiswill remain unchanged.- If an exception is thrown during the call to
T_j's move constructor, thevariantwill hold no value.
These changes make sure that when the move constructor of T_j is noexcept, the assignment will never put the variant
into the valueless state and that it will provide the strong exception safety guarantee.
variant& operator=(variant&& rhs) noexcept(see below);Effects: Let
jberhs.index().
- If neither
*thisnorrhsholds a value, there is no effect. Otherwise- if
*thisholds a value butrhsdoes not, destroys the value contained in*thisand sets*thisto not hold a value. Otherwise,- if
index() ==, assignsrhs.index()j && (is_nothrow_move_assignable_v<T_j> || !is_nothrow_move_constructible_v<T_j>)std::forward<T_j>(get<j>(rhs))to the value contained in*this, with. Otherwise,jbeingindex()- destroys any value contained in
*this. Sets*thisto hold the same alternative index asrhsand initializes the value contained in*thisas if direct-non-list-initializing an object of typeT_jwithstd::forward<T_j>(get<j>(rhs))with.jbeingrhs.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>istruefor alli. The expression insidenoexceptis equivalent to:is_nothrow_move_constructible_v<T_i>for all&& is_nothrow_move_assignable_v<T_i>i.
- If an exception is thrown during the call to
T_j's move constructor(with, thejbeingrhs.index())variantwill 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 ofT_j's move assignment;index()will bej.
As above: These changes make sure that when the move constructor of T_j is noexcept, the assignment will never put the variant
into the valueless state and that it will provide the strong exception safety guarantee with respect to *this.
template <class T> variant& operator=(T&& t) noexcept(see below);Effects:
operator=(variant(std::forward<T>(t))).Returns:
*this.Remarks: This function shall not participate in overload resolution unless
is_same_v<decay_t<T>, variant>isfalseand unlessvariant(std::forward<T>(t))is a valid expression. The expression insidenoexceptisnoexcept(operator=(variant(std::forward<T>(t)))).
The existing specification of this assignment needlessly duplicates the wording in variant::variant(T&& t)that selects the alternative
using overload resolution, and does not provide any non-valueless guarantees due to initializing directly from t instead of using
the potentially noexcept move constructor of the selected contained type. I have opted to use the cleanest fix in the above suggested wording.
It's possible to expand the expression operator=(variant(std::forward<T>(t))) into the specification, but the only thing that this gains
is collapsing two adjacent move constructors calls into one, and the implementation is permitted to do this anyway under the as-if rule, so the benefits do
not outweigh the costs of using the more complicated wording, with the associated possibility of getting it wrong.
Or, another option is to remove this assignment operator altogether, which would be equivalent to this specification.
template <size_t I, class... Args> void emplace(Args&&... args);Requires:
I < sizeof...(Types)Effects:
Destroys the currently contained value ifvalueless_by_exception()isfalse. Then direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentsstd::forward<Args>(args)....
- If
is_nothrow_constructible_v<T_I, Args&&...> || !is_nothrow_move_constructible_v<T_I>, destroys the currently contained value ifvalueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentsstd::forward<Args>(args).... Otherwise,- direct-initializes a temporary
TMPof typeT_Iwith the argumentsstd::forward<Args>(args)..., destroys the currently contained value ifvalueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentstd::forward<T_I>(TMP).Postcondition:
index()isI.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&&...>istrue. If an exception is thrown during the initialization of the contained value, thevariantwill not hold a value.
As above: These changes make sure that when the move constructor of T_I is noexcept, emplace will never put the variant
into the valueless state and that it will provide the strong exception safety guarantee.
template <size_t I, class U, class... Args> void emplace(initializer_list<U> il, Args&&... args);Requires:
I < sizeof...(Types)Effects:
Destroys the currently contained value ifvalueless_by_exception()isfalse. Then direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentsil, std::forward<Args>(args)....
- If
is_nothrow_constructible_v<T_I, initializer_list<U>&, Args&&...> || !is_nothrow_move_constructible_v<T_I>, destroys the currently contained value ifvalueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentsil, std::forward<Args>(args).... Otherwise,- direct-initializes a temporary
TMPof typeT_Iwith the argumentsil, std::forward<Args>(args)..., destroys the currently contained value ifvalueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentstd::forward<T_I>(TMP).Postcondition:
index()isI.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&&...>istrue. If an exception is thrown during the initialization of the contained value, thevariantwill not hold a value.
As above.
template <class T, class... Args> void emplace(Args&&... args);template <class T, class U, class... Args> void emplace(initializer_list<U> il, Args&&... args);
These two overloads of emplace are specified in terms of the index-based ones, so no changes are required.
constexpr bool valueless_by_exception() const noexcept;Effects: Returns
falseif and only if the variant holds a value. [Note: A variant will not hold a value if an exception is thrown from the move constructor of the contained type during a type-changing assignment or emplacement. — end note]Remarks: This function shall be
staticand always returnfalsewhenis_nothrow_move_constructible_v<T_i>istruefor alli. [Note:static_assert(variant<Types...>::valueless_by_exception() == false);may be used to verify that avariant<Types...>may never become valueless. — end note]
The changes in the preceding section do give us the necessary guarantees in most cases, but there's still a problem with, for example, variant<int, std::list<int>>.
Under some implementations, every list instance allocates a sentinel node, and since list's move constructor needs to leave the moved-from object in a valid
state, it can't steal its sentinel node for the new instance, forcing an allocation and therefore precluding noexcept. This means that variant<int, std::list<int>>
would be guaranteed valueless on some implementations and not on others, which is a portability concern.
It so happens that the implementation of variant usually moves from an internal temporary that is later destroyed and is invisible to the outside code. A move-constructed
list could, therefore, steal the sentinel node of this temporary, but there is no standard protocol for doing so.
The implementation of std::variant could, of course, detect std::list and use some internal constructor instead, and one might argue that a quality implementation
ought to do so, but this cannot extend to user-defined types, or even to std::pair<T, std::list<int>>.
This section proposes a general mechanism to enable such semi-destructive move construction, after which the moved-from object can be safely destroyed, but is not guaranteed to be
usable in any other way. This semi-destructive move is called pilfering and is accessed by a constructor with the signature T::T(std::pilfered<T>) noexcept,
where std::pilfered<T> wraps a reference to T:
template<class T> class pilfered
{
private:
T& t_;
public:
explicit constexpr pilfered(T&& t) noexcept: t_(t) {}
constexpr T& get() const noexcept { return t_; }
constexpr T* operator->() const noexcept { return std::addressof(t_); }
};
and there's also a corresponding type trait std::is_pilfer_constructible<T> and a helper function std::pilfer(t) which is analogous to std::move(t):
template<class T> struct is_pilfer_constructible: std::integral_constant<bool, std::is_nothrow_move_constructible<T>::value || (std::is_nothrow_constructible<T, pilfered<T>>::value && !std::is_nothrow_constructible<T, __not_pilfered<T>>::value)>
{
};
template<class T> constexpr decltype(auto) pilfer(T&& t) noexcept
{
using U = std::remove_reference_t<T>;
return std::conditional_t<std::is_nothrow_move_constructible<U>::value || !is_pilfer_constructible<U>::value, U&&, pilfered<U>>(std::move(t));
}
is_pilfer_constructible<T> reports true when T has either a noexcept move constructor or a noexcept pilfering constructor. It checks for
construction from __not_pilfered, which has the same definition as pilfered, in order to detect false positives caused by types that are constructible
from an argument of any type.
pilfer(t) returns either an rvalue reference to t or a pilfered<T> instance that refers to t, as appropriate.
In the past I have suggested a pilfering mechanism that uses a function instead of a constructor, but a function-based approach does not compose. A pilfering constructor for
struct X
{
T t;
U u;
};
where T and U are known to be either noexcept move constructible or pilfer constructible, can be added via
struct X
{
T t;
U u;
X(std::pilfered<X> r) noexcept: t(std::pilfer(r->t)), u(std::pilfer(r->u)) {}
};
which is analogous to adding an ordinary move constructor.
The case in which T and U are not known in advance, such as with std::pair,
becomes more convoluted because the initialization of the members might throw, and we don't want to define the pilfering
constructor in this case (it makes no sense to define a pilfering constructor that is not noexcept.)
is_pilfer_constructible<T> can be used to disable the pilfering constructor via SFINAE. One possible implementation
of X's pilfering constructor for the template case would be
template<class T, class U> struct X
{
T t;
U u;
template<class T2 = T, class U2 = U, class E = int[std::is_pilfer_constructible_v<T2> && std::is_pilfer_constructible_v<U2>? 1: -1]>
X(std::pilfered<X> r) noexcept: t(std::pilfer(r->t)), u(std::pilfer(r->u)) {}
};
is_pilfer_constructible is satisfied by either a noexcept move constructor or a noexcept pilfering constructor,
and the expression t(std::pilfer(r->t)) will work with either, so a combinatorial explosion does not occur.
The standard wording for pilfering (relative to N4567) is given below.
— Add to the synopsis of header <utility> in [utility] the following:
// 20.x, Pilfering template<class T> class pilfered; template<class T> constexpr decltype(auto) pilfer(T&& t) noexcept;
— After [intseq], add new sections [pilfered] and [pilfer]:
20.x Class template
pilfered[pilfered]template<class T> class pilfered { private: T& t_; public: explicit constexpr pilfered(T&& t) noexcept; constexpr T& get() const noexcept; constexpr T* operator->() const noexcept; };
pilfered<T>wraps a reference toTand is used as an argument toT's pilfering constructor. Pilfering constructors have the formT::T(pilfered<T>) noexceptand perform a semi-destructive move. After a call to a pilfering constructor, the moved-from object can be safely destroyed, but cannot be used in any other way.
explicit constexpr pilfered(T&& t) noexcept;Effects: Initializes
t_tot.
constexpr T& get() const noexcept;Returns:
t_.
constexpr T* operator->() const noexcept;Returns:
addressof(t_).20.x Function template
pilfer[pilfer]template<class T> constexpr decltype(auto) pilfer(T&& t) noexcept;Returns:
conditional_t<is_nothrow_move_constructible_v<U> || !is_pilfer_constructible_v<U>, U&&, pilfered<U>>(move(t)), whereUisremove_reference_t<T>.
— In the synopsis of header <type_traits> [meta.type.synop], in the group of type properties, add the following:
template <class T> struct is_nothrow_move_constructible; template <class T> struct is_pilfer_constructible;
template <class T> constexpr bool is_nothrow_move_constructible_v = is_nothrow_move_constructible<T>::value; template <class T> constexpr bool is_pilfer_constructible_v = is_pilfer_constructible<T>::value;
— In section [meta.unary.prop], add the following paragraph before Table 49:
In the following table,
__not_pilferedis a class template with an unspecified name whose definition is the same as that ofpilfered.
— In section [meta.unary.prop], add to Table 49 the following row:
template <class T> struct is_pilfer_constructible; |
For a referenceable type T, the same result as
is_nothrow_move_constructible_v<T> || (is_nothrow_constructible_v<T, pilfered<T>> &&
!is_nothrow_constructible_v<T, __not_pilfered<T>>), otherwise false.[Note: __not_pilfered is used to avoid false positives caused by types that
can be constructed from any argument. — end note] |
T shall be a complete type, (possibly cv-qualified) void, or an array of unknown bound. |
— Add to struct pair in [pairs.pair] the following constructor:
pair(const pair&) = default; pair(pair&&) = default; pair(pilfered<pair> r) noexcept; constexpr pair();
— Add to [pairs.pair] the following section:
pair(pilfered<pair> r) noexcept;Effects: Initializes
firstwithpilfer(r->first)andsecondwithpilfer(r->second).Remarks: This constructor shall not participate in overload resolution unless
is_pilfer_constructible<T1>::value && is_pilfer_constructible<T2>::value.
— Add to class tuple in [tuple.tuple] the following constructor:
tuple(const tuple&) = default; tuple(tuple&&) = default; tuple(pilfered<tuple> u) noexcept;
— Add to [tuple.cnstr] the following section:
tuple(pilfered<tuple> u) noexcept;Effects: For all i, initializes the ith element of
*thiswithget<i>(u.get())whenTiis a reference type,pilfer(get<i>(u.get()))otherwise.Remarks: This constructor shall not participate in overload resolution unless
is_pilfer_constructible<Ti>::valueistruefor all i.
— Add to class deque in [deque.overview] the following constructor:
deque(const deque& x); deque(deque&&); deque(pilfered<deque> x) noexcept;
— Add to [deque.cons] the following section:
deque(pilfered<deque> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class forward_list in [forwardlist.overview] the following constructor:
forward_list(const forward_list& x); forward_list(forward_list&& x); forward_list(pilfered<forward_list> x) noexcept;
— Add to [forwardlist.cons] the following section:
forward_list(pilfered<forward_list> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class list in [list.overview] the following constructor:
list(const list& x); list(list&& x); list(pilfered<list> x) noexcept;
— Add to [list.cons] the following section:
list(pilfered<list> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class map in [map.overview] the following constructor:
map(const map& x); map(map&& x); map(pilfered<map> x) noexcept;
— Add to [map.cons] the following section:
map(pilfered<map> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class multimap in [multimap.overview] the following constructor:
multimap(const multimap& x); multimap(multimap&& x); multimap(pilfered<multimap> x) noexcept;
— Add to [multimap.cons] the following section:
multimap(pilfered<multimap> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class set in [set.overview] the following constructor:
set(const set& x); set(set&& x); set(pilfered<set> x) noexcept;
— Add to [set.cons] the following section:
set(pilfered<set> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class multiset in [multiset.overview] the following constructor:
multiset(const multiset& x); multiset(multiset&& x); multiset(pilfered<multiset> x) noexcept;
— Add to [multiset.cons] the following section:
multiset(pilfered<multiset> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class unordered_map in [unord.map.overview] the following constructor:
unordered_map(const unordered_map&); unordered_map(unordered_map&&); unordered_map(pilfered<unordered_map> x) noexcept;
— Add to [unord.map.cnstr] the following section:
unordered_map(pilfered<unordered_map> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class unordered_multimap in [unord.multimap.overview] the following constructor:
unordered_multimap(const unordered_multimap&); unordered_multimap(unordered_multimap&&); unordered_multimap(pilfered<unordered_multimap> x) noexcept;
— Add to [unord.multimap.cnstr] the following section:
unordered_multimap(pilfered<unordered_multimap> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class unordered_set in [unord.set.overview] the following constructor:
unordered_set(const unordered_set&); unordered_set(unordered_set&&); unordered_set(pilfered<unordered_set> x) noexcept;
— Add to [unord.set.cnstr] the following section:
unordered_set(pilfered<unordered_set> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
— Add to class unordered_multiset in [unord.multiset.overview] the following constructor:
unordered_multiset(const unordered_multiset&); unordered_multiset(unordered_multiset&&); unordered_multiset(pilfered<unordered_multiset> x) noexcept;
— Add to [unord.multiset.cnstr] the following section:
unordered_multiset(pilfered<unordered_multiset> x) noexcept;Effects: Constructs
*thisfrom the contents of the object referenced byx.get().Postconditions:
*thishas the value the object referenced byx.get()had before the call.Remarks: After this constructor, the object referenced by
x.get()can safely be destroyed. The behavior of any other access to the object referenced byx.get()is undefined.
Given these changes, the variant specification needs to be updated to use pilfer constructors, as follows. The differences are relative to the
wording in the preceding section.
variant& operator=(const variant& rhs);Effects: Let
jberhs.index().
- If neither
*thisnorrhsholds a value, there is no effect. Otherwise- if
*thisholds a value butrhsdoes not, destroys the value contained in*thisand sets*thisto not hold a value. Otherwise,- if
index() == j && is_nothrow_copy_assignable_v<T_j>, assigns the value contained inrhsto the value contained in*this. Otherwise,- if
index() == j && is_nothrow_move_assignable_v<T_j>, copies the value contained inrhsto a temporaryTMP, then assignsstd::forward<T_j>(TMP)to the value contained in*this. Otherwise,- if
index() == j &&, assigns the value contained in!is_nothrow_move_constructible_v<T_j>!is_pilfer_constructible_v<T_j>rhsto the value contained in*this. Otherwise,- copies the value contained in
rhsto a temporaryTMP, then destroys any value contained in*this. Sets*thisto hold the same alternative index asrhsand initializes the value contained in*thisas if direct-non-list-initializing an object of typeT_jwithstd::forward<T_j>(TMP)std::pilfer(TMP).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>istruefor alli.
- 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 ofT_j's copy assignment;index()will bej.- If an exception is thrown during the call to
T_j's copy constructor (withjbeingrhs.index()),*thiswill remain unchanged.- If an exception is thrown during the call to
T_j's move constructor, thevariantwill hold no value.
variant& operator=(variant&& rhs) noexcept(see below);Effects: Let
jberhs.index().
- If neither
*thisnorrhsholds a value, there is no effect. Otherwise- if
*thisholds a value butrhsdoes not, destroys the value contained in*thisand sets*thisto not hold a value. Otherwise,- if
index() == j && (is_nothrow_move_assignable_v<T_j> ||, assigns!is_nothrow_move_constructible_v<T_j>!is_pilfer_constructible_v<T_j>)std::forward<T_j>(get<j>(rhs))to the value contained in*this. Otherwise,- If
!is_nothrow_move_constructible_v<T_j> && is_pilfer_constructible_v<T_j>, initializes a temporaryTMPof typeT_jfromstd::forward<T_j>(get<j>(rhs)). Destroys any value contained in*this. Sets*thisto hold the same alternative index asrhsand initializes the value contained in*thisas if direct-non-list-initializing an object of typeT_jwithstd::pilfer(TMP). Otherwise,- destroys any value contained in
*this. Sets*thisto hold the same alternative index asrhsand initializes the value contained in*thisas if direct-non-list-initializing an object of typeT_jwithstd::forward<T_j>(get<j>(rhs)).Returns:
*this.Remarks: This function shall not participate in overload resolution unless
is_move_constructible_v<T_i> && is_move_assignable_v<T_i>istruefor alli. The expression insidenoexceptis equivalent to:is_nothrow_move_constructible_v<T_i>for alli.
- If an exception is thrown during the call to
T_j's move constructor in the last bullet, thevariantwill 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 ofT_j's move assignment;index()will bej.
The reason of using pilfer only when the type can be pilfered but not noexcept moved is because we are not allowed to pilfer directly from rhs,
as there is no guarantee that outside code will not access the value afterwards. So we need to first move into a temporary, and then pilfer that temporary instead. We could do that
in either case, but this would result in two moves instead of one when the type has a noexcept move constructor.
template <size_t I, class... Args> void emplace(Args&&... args);Requires:
I < sizeof...(Types)Effects:
- If
is_nothrow_constructible_v<T_I, Args&&...> ||, destroys the currently contained value if!is_nothrow_move_constructible_v<T_I>!is_pilfer_constructible_v<T_j>valueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentsstd::forward<Args>(args).... Otherwise,- direct-initializes a temporary
TMPof typeT_Iwith the argumentsstd::forward<Args>(args)..., destroys the currently contained value ifvalueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentstd::forward<T_I>(TMP)std::pilfer(TMP).Postcondition:
index()isI.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&&...>istrue. If an exception is thrown during the initialization of the contained value, thevariantwill not hold a value.
template <size_t I, class U, class... Args> void emplace(initializer_list<U> il, Args&&... args);Requires:
I < sizeof...(Types)Effects:
- If
is_nothrow_constructible_v<T_I, initializer_list<U>&, Args&&...> ||, destroys the currently contained value if!is_nothrow_move_constructible_v<T_I>!is_pilfer_constructible_v<T_j>valueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentsil, std::forward<Args>(args).... Otherwise,- direct-initializes a temporary
TMPof typeT_Iwith the argumentsil, std::forward<Args>(args)..., destroys the currently contained value ifvalueless_by_exception()isfalseand direct-initializes the contained value as if constructing a value of typeT_Iwith the argumentstd::forward<T_I>(TMP)std::pilfer(TMP).Postcondition:
index()isI.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&&...>istrue. If an exception is thrown during the initialization of the contained value, thevariantwill not hold a value.
constexpr bool valueless_by_exception() const noexcept;Effects: Returns
falseif and only if the variant holds a value. [Note: A variant will not hold a value if an exception is thrown from the move constructor of the contained type during a type-changing assignment or emplacement. — end note]Remarks: This function shall be
staticand always returnfalsewhenisis_nothrow_move_constructible_v<T_i>is_pilfer_constructible_v<T_i>truefor alli. [Note:static_assert(variant<Types...>::valueless_by_exception() == false);may be used to verify that avariant<Types...>may never become valueless. — end note]
void swap(variant& rhs) noexcept(see below);Effects: Let
ibeindex(). Letjberhs.index().
- if
valueless_by_exception() && rhs.valueless_by_exception()no effect. Otherwise,- if
index() == rhs.index(), callsswap(get<i>(*this), get<i>(rhs))with. Otherwise,ibeingindex()- if
valueless_by_exception(), constructs a value of typeT_jinto*thisfromstd::pilfer(get<j>(rhs)), setsindex()tojand destroys the contained value ofrhs, making it hold no value. Otherwise,- if
rhs.valueless_by_exception(), constructs a value of typeT_iintorhsfromstd::pilfer(get<i>(*this)), setsrhs.index()toiand destroys the contained value of*this, making it hold no value. Otherwise,- if
is_pilfer_constructible_v<T_i> && is_pilfer_constructible_v<T_j>, constructs a temporaryTMPof typeT_ifromstd::pilfer(get<i>(*this)), destroys the contained value of*this, constructs a value of typeT_jinto*thisfromstd::pilfer(get<j>(rhs)), setsindex()toj, destroys the contained value ofrhs, constructs a value of typeT_iintorhsfromstd::pilfer(TMP), and setsrhs.index()toi. Otherwise,- exchanges values of
rhsand*this.Throws: Any exception thrown by
swap(get<i>(*this), get<i>(rhs))withibeingindex()orvariant's move constructor and move assignment operator.Remarks: This function shall not participate in overload resolution unless all alternative types satisfy the
Swappablerequirements (17.6.3.2). If an exception is thrown during the call to functionswap(get<i>(*this), get<i>(rhs)), the states of the contained values of*thisand ofrhsare determined by the exception safety guarantee ofswapfor lvalues ofT_iwithibeingindex(). If an exception is thrown during the exchange of the values of*thisandrhs, the states of the values of*thisand ofrhsare determined by the exception safety guarantee ofvariant's move constructor and move assignment operator. The expression insidenoexceptistruewhennoexcept(swap(declval<T_k>(), declval<T_k>())) && is_pilfer_constructible_v<T_k>istruefor allk,falseotherwise.
At this point, we have a variant that is never valueless for types that are either noexcept move constructible or pilfer constructible, which covers a large majority of the use cases.
The natural next step is to dispense with valueless_by_exception altogether by requiring the contained types to be such. Can we afford to do so?
What use cases demand types that are neither noexcept move- nor pilfer-constructible?
First, there are the legacy C++03 types that have a copy constructor but do not have a move constructor, and for some reason, can't be changed. They can be moved, but this involves a copy, and is not noexcept.
Second, there are the types that are neither copyable nor movable, such as std::mutex. An example of a variant instantiated on such types would be variant<std::mutex, std::recursive_mutex>.
There is an easy workaround in both cases: instead of putting a prohibited type T into the variant, use unique_ptr<T> instead.
unique_ptr is noexcept moveable, and the cost in our case of switching to it is a heap allocation and the need to check for nullptr.
The benefit of relegating these two uncommon use cases to the dusty "use unique_ptr" folder is that we'll finally be able to get rid of valueless_by_exception
altogether, thereby simplifying the specification a bit and eliminating the need for the programmers to static_assert that their variants can't be valueless.
For a C++17 component, this is also the conservative approach. If these two use cases turn out to be important in practice, we can later either bring back valueless_by_exception
or provide some other way to address them, without breaking any code. If we instead provide the current, valueless_by_exception-possessing variant, we cannot
at a later date take that away.
More specifically, if we focus on variant<std::mutex, std::recursive_mutex>, we see a use case that is indeed legitimate in that it's using variant as if it
were a slightly more convenient union { std::mutex m; std::recursive_mutex rm; }. However, this variant does not have much in common with variant<int, float, std::string>, in that the
latter is Regular and the former decidedly isn't. So if we were in a situation where we already had a standard Regular, never valueless, variant and were faced with the need
to support this "convenient union" use case, we would probably take a serious look at the possibility of providing a separate component that addresses this need, instead of making
variant potentially valueless.
The case for this change is less strong. I consider the previous two sections a strict and necessary improvement to the existing proposal (in spirit and intent, not specific wording, which may well
have defects.) On the "strongly in favor" as +2 .. "strongly against" as -2 spectrum, my current position on section III would be +3, and section IV would be +2.5 unless the standard library
is instead changed to require noexcept move constructors on all of its types. This section would only score about +1.7.
Therefore, I will not provide suggested wording for the elimination of valueless_by_exception in this revision, although I will in a subsequent revision if discussion and straw polls provide
an indication that the working groups are willing to consider such a change.
— end