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
j
berhs.index()
.
- If neither
*this
norrhs
holds a value, there is no effect. Otherwise- if
*this
holds a value butrhs
does not, destroys the value contained in*this
and sets*this
to not hold a value. Otherwise,- if
index() ==
, assigns the value contained inrhs.index()j && is_nothrow_copy_assignable_v<T_j>rhs
to the value contained in*this
. Otherwise,- if
index() == j && is_nothrow_move_assignable_v<T_j>
, copies the value contained inrhs
to 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 inrhs
to the value contained in*this
. Otherwise,- copies the value contained in
rhs
to a temporaryTMP
, then destroys any value contained in*this
. Sets*this
to hold the same alternative index asrhs
and initializes the value contained in*this
as if direct-non-list-initializing an object of typeT_j
withstd::forward<T_j>(TMP)
, with.TMP
being the temporary andj
beingrhs.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>
istrue
for 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 (withj
beingrhs.index()
),*this
will remain unchanged.- If an exception is thrown during the call to
T_j
's move constructor, thevariant
will 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
j
berhs.index()
.
- If neither
*this
norrhs
holds a value, there is no effect. Otherwise- if
*this
holds a value butrhs
does not, destroys the value contained in*this
and sets*this
to 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,j
beingindex()
- destroys any value contained in
*this
. Sets*this
to hold the same alternative index asrhs
and initializes the value contained in*this
as if direct-non-list-initializing an object of typeT_j
withstd::forward<T_j>(get<j>(rhs))
with.j
beingrhs.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>
istrue
for alli
. The expression insidenoexcept
is 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, thej
beingrhs.index()
)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 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>
isfalse
and unlessvariant(std::forward<T>(t))
is a valid expression. The expression insidenoexcept
isnoexcept(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_I
with 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()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with the argumentsstd::forward<Args>(args)...
. Otherwise,- direct-initializes a temporary
TMP
of typeT_I
with the argumentsstd::forward<Args>(args)...
, destroys the currently contained value ifvalueless_by_exception()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with 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, thevariant
will 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_I
with 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()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with the argumentsil, std::forward<Args>(args)...
. Otherwise,- direct-initializes a temporary
TMP
of typeT_I
with the argumentsil, std::forward<Args>(args)...
, destroys the currently contained value ifvalueless_by_exception()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with 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, thevariant
will 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
false
if 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
static
and always returnfalse
whenis_nothrow_move_constructible_v<T_i>
istrue
for 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 toT
and is used as an argument toT
's pilfering constructor. Pilfering constructors have the formT::T(pilfered<T>) noexcept
and 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))
, whereU
isremove_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_pilfered
is 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
first
withpilfer(r->first)
andsecond
withpilfer(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
*this
withget<i>(u.get())
whenTi
is a reference type,pilfer(get<i>(u.get()))
otherwise.Remarks: This constructor shall not participate in overload resolution unless
is_pilfer_constructible<Ti>::value
istrue
for 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
*this
from the contents of the object referenced byx.get()
.Postconditions:
*this
has 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
j
berhs.index()
.
- If neither
*this
norrhs
holds a value, there is no effect. Otherwise- if
*this
holds a value butrhs
does not, destroys the value contained in*this
and sets*this
to not hold a value. Otherwise,- if
index() == j && is_nothrow_copy_assignable_v<T_j>
, assigns the value contained inrhs
to the value contained in*this
. Otherwise,- if
index() == j && is_nothrow_move_assignable_v<T_j>
, copies the value contained inrhs
to 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>rhs
to the value contained in*this
. Otherwise,- copies the value contained in
rhs
to a temporaryTMP
, then destroys any value contained in*this
. Sets*this
to hold the same alternative index asrhs
and initializes the value contained in*this
as if direct-non-list-initializing an object of typeT_j
withstd::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>
istrue
for 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 (withj
beingrhs.index()
),*this
will remain unchanged.- If an exception is thrown during the call to
T_j
's move constructor, thevariant
will hold no value.
variant& operator=(variant&& rhs) noexcept(see below);
Effects: Let
j
berhs.index()
.
- If neither
*this
norrhs
holds a value, there is no effect. Otherwise- if
*this
holds a value butrhs
does not, destroys the value contained in*this
and sets*this
to 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 temporaryTMP
of typeT_j
fromstd::forward<T_j>(get<j>(rhs))
. Destroys any value contained in*this
. Sets*this
to hold the same alternative index asrhs
and initializes the value contained in*this
as if direct-non-list-initializing an object of typeT_j
withstd::pilfer(TMP)
. Otherwise,- destroys any value contained in
*this
. Sets*this
to hold the same alternative index asrhs
and initializes the value contained in*this
as if direct-non-list-initializing an object of typeT_j
withstd::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>
istrue
for alli
. The expression insidenoexcept
is 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, thevariant
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 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()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with the argumentsstd::forward<Args>(args)...
. Otherwise,- direct-initializes a temporary
TMP
of typeT_I
with the argumentsstd::forward<Args>(args)...
, destroys the currently contained value ifvalueless_by_exception()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with 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, thevariant
will 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()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with the argumentsil, std::forward<Args>(args)...
. Otherwise,- direct-initializes a temporary
TMP
of typeT_I
with the argumentsil, std::forward<Args>(args)...
, destroys the currently contained value ifvalueless_by_exception()
isfalse
and direct-initializes the contained value as if constructing a value of typeT_I
with 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, thevariant
will not hold a value.
constexpr bool valueless_by_exception() const noexcept;
Effects: Returns
false
if 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
static
and always returnfalse
whenis
is_nothrow_move_constructible_v<T_i>is_pilfer_constructible_v<T_i>true
for 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
i
beindex()
. Letj
berhs.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,i
beingindex()
- if
valueless_by_exception()
, constructs a value of typeT_j
into*this
fromstd::pilfer(get<j>(rhs))
, setsindex()
toj
and destroys the contained value ofrhs
, making it hold no value. Otherwise,- if
rhs.valueless_by_exception()
, constructs a value of typeT_i
intorhs
fromstd::pilfer(get<i>(*this))
, setsrhs.index()
toi
and 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 temporaryTMP
of typeT_i
fromstd::pilfer(get<i>(*this))
, destroys the contained value of*this
, constructs a value of typeT_j
into*this
fromstd::pilfer(get<j>(rhs))
, setsindex()
toj
, destroys the contained value ofrhs
, constructs a value of typeT_i
intorhs
fromstd::pilfer(TMP)
, and setsrhs.index()
toi
. Otherwise,- exchanges values of
rhs
and*this
.Throws: Any exception thrown by
swap(get<i>(*this), get<i>(rhs))
withi
beingindex()
orvariant
's move constructor and move assignment operator.Remarks: This function shall not participate in overload resolution unless all alternative types satisfy the
Swappable
requirements (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*this
and ofrhs
are determined by the exception safety guarantee ofswap
for lvalues ofT_i
withi
beingindex()
. If an exception is thrown during the exchange of the values of*this
andrhs
, the states of the values of*this
and ofrhs
are determined by the exception safety guarantee ofvariant
's move constructor and move assignment operator. The expression insidenoexcept
istrue
whennoexcept(swap(declval<T_k>(), declval<T_k>())) && is_pilfer_constructible_v<T_k>
istrue
for allk
,false
otherwise.
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 variant
s 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