December 25, 2023
bsl::variant
: An Allocator-Aware Discriminated Union¶
Summary¶
The latest release of BDE (4.0) provides the header <bsl_variant.h>
,
which contains an allocator-aware implementation of the C++17 <variant>
header with select C++20 enhancements. Some std::variant
features are not
yet supported by bsl::variant
, however.
The type bsl::variant<T...>
is a discriminated union of the types T...
,
similar to bdlb::Variant<T...>
. The types T...
are known as
alternatives. For example, an object of type
bsl::variant<int, double, const char*>
can hold either an int
, or a
double
, or a const char*
, but only one of these alternatives at a time.
The bsl::variant
object also keeps track of which alternative is currently
active, so, for example, if the bsl::variant
object holds a double
object, then an attempt to retrieve the stored const char*
object will fail
by returning a null pointer or throwing an exception.
Differences Between std::variant
and bsl::variant
¶
bsl::variant
is allocator-aware if at least one alternative is
allocator-aware. For example, if one of the alternatives is
bsl::vector<T>
, then the allocator specified at construction of the
bsl::variant
object will be used by the bsl::vector
to allocate memory
to hold its buffer of T
’s. Consequently, bsl::variant
differs from
std::variant
in that:
There are additional overloads for several constructors taking an allocator parameter. These constructors are valid even when no alternative is allocator-aware; however, the allocator parameter will be ignored in that case.
There is an additional
get_allocator
method, which participates in overload resolution only if at least one alternative is allocator-aware.If at least one alternative is allocator-aware, the footprint of
bsl::variant
is larger than that ofstd::variant
because the former needs to store the allocator that it uses to supply memory.
The declarations of the additional overloads are shown below, with some details elided for readability:
// CREATORS
variant(bsl::allocator_arg_t, allocator_type);
variant(bsl::allocator_arg_t, allocator_type, const variant& original);
variant(bsl::allocator_arg_t, allocator_type, variant&& original);
template <class t_TYPE>
variant(bsl::allocator_arg_t, allocator_type, t_TYPE&& value);
template <class t_TYPE, class... t_ARGS>
explicit variant(bsl::allocator_arg_t,
allocator_type,
bsl::in_place_type_t<t_TYPE>,
t_ARGS&&... args);
template <class t_TYPE, class t_ELEM_TYPE, class... t_ARGS>
explicit variant(bsl::allocator_arg_t,
allocator_type,
bsl::in_place_type_t<t_TYPE>,
std::initializer_list<t_ELEM_TYPE> arg,
t_ARGS&&... args);
template <size_t t_INDEX, class... t_ARGS>
explicit variant(bsl::allocator_arg_t,
allocator_type,
bsl::in_place_index_t<t_INDEX>,
t_ARGS&&... args);
template <size_t t_INDEX, class t_ELEM_TYPE, class... t_ARGS>
explicit variant(bsl::allocator_arg_t,
allocator_type,
bsl::in_place_index_t<t_INDEX>,
std::initializer_list<t_ELEM_TYPE> arg,
t_ARGS&&... args);
// ACCESSORS
get_allocator() const noexcept;
Note that the constructors above have the same constraints and mandates as their standard counterparts.
Important
We recommend referring to the cppreference documentation for variant
in order to understand the constructors of bsl::variant
, because the
documentation generated by Doxygen can be hard to understand.
The current implementation of bsl::variant
also differs from
std::variant
in the following ways:
bsl::visit
can only visit a single variant at a time.Most of the methods and free functions of
bsl::variant
are notconstexpr
and do not havenoexcept
specifications.bsl::variant
does not have any trivial member functions. In C++17,std::variant
has a conditionally trivial destructor, and in C++20, the copy constructor, move constructor, copy assignment operator, and move assignment operator ofstd::variant
are also conditionally trivial.bsl::variant
does not provide the strong exception safety guarantee for any function call that changes which alternative is currently active within absl::variant
object.
Also note that additional limitations apply in C++03 (and some of the above
constructor signatures are different). See bslstl_variant.h
for details.
Differences Between bdlb::Variant
and bsl::variant
¶
Although bsl::variant
and bdlb::Variant
are both discriminated union
types, bsl::variant
is not a drop-in replacement for bdlb::Variant
. A
quick reference, comparing analogous functions in the bdlb::Variant
and
bsl::variant
interfaces, is given in the next subsection. However, there
are also significant behavioral differences, which will be explained in more
detail. A particularly notable difference is that bdlb::Variant
and
bsl::variant
deduce the return types of visitors using different rules.
Therefore, a visitor class that was designed to visit bdlb::Variant
objects
may need to be modified before it can be used to visit bsl::variant
objects; some guidance is provided in Migrating Visitors From bdlb::Variant to bsl::variant. In addition,
bsl::variant
does not have a null state, though it may sometimes fail to
hold a value (see The “Valueless By Exception” State). If a bsl::variant
needs
to be able to represent a state that is none of T1
, …, Tn
, then
bsl::variant<bsl::monostate, T1, ..., Tn>
should be used.
Quick reference¶
The constructors for bsl::variant
are similar to those of bdlb::Variant
but use the leading allocator convention (with bsl::allocator_arg_t
) while
bdlb::Variant
uses the trailing allocator convention. Most other methods
of bdlb::Variant
have a differently-named counterpart for bsl::variant
:
bdlb::Variant |
bsl::variant |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See below |
|
|
|
See below |
|
See below |
|
See below |
Note that the numbering for alternatives of bsl::variant
starts from 0,
whereas bdlb::Variant
starts from 1. For example,
bdlb::Variant<T1, T2>::typeIndex()
returns 1 when the T2
alternative is
active, and bsl::variant<bsl::monostate, T1, T2>::index()
does likewise.
But if there is no need to represent a null state, then
bsl::variant<T1, T2>
may be a suitable replacement. In that case, the
index
method returns 1 when the T2
alternative is active.
Notable behavioral differences¶
Some of the bsl::variant
equivalents given in the table in the preceding
section behave differently from their bdlb::Variant
counterparts. In
addition, some operations that are spelled the same way for bdlb::Variant
and bsl::variant
, such as the swap
method, nonetheless have different
behavior. Here are some notable differences:
In C++11 and later,
bsl::variant
has full support for move semantics for both variants and visitors. In C++03,bsl::variant
still supports moving from variant alternatives (but not from visitors). For example, when usingbsl::variant
, a single visitor can be applied to both lvalue and rvaluebsl::variant
expressions, and the visitor will be called with an argument of the appropriate value category:struct PushBackVisitor { bsl::vector<bsl::string>& d_v; PushBackVisitor(bsl::vector<bsl::string>& v) : d_v(v) {} void operator()(const bsl::string& arg) { d_v.push_back(arg); } void operator()(bsl::string&& arg) { d_v.push_back(std::move(arg)); } void operator()(int arg) { bsl::ostringstream oss(d_v.get_allocator()); oss << arg; d_v.push_back(std::move(oss).str()); } }; template <class t_VARIANT> void pushBackVariant(bsl::vector<bsl::string>& vector, t_VARIANT&& variant) // Append to the specified 'vector' the value held by the specified // 'variant', which must be of type 'bsl::variant<int, bsl::string>'. { bsl::visit(PushBackVisitor(vector), std::forward<t_VARIANT>(variant)); }
Note that if a
bdlb::Variant
had been used in the above example, then thebsl::string&&
overload would never be called; it would be necessary to write a second visitor taking absl::string&
overload that moves from the parameter, and take care to use that visitor only on variant objects that no longer need to retain their value.The
bsl::get
methods, which return a reference to an alternative, fail by throwing an exception if the desired alternative is not active inside thebsl::variant
object, whereasbdlb::Variant
does not use exceptions. Thebsl::get_if
method, which returns a null pointer on failure, can be used to avoid exceptions:bdlb::Variant<int, double> bdlbVariant = 1; bsl::variant<int, double> bslVariant = 1; bdlbVariant.the<int>(); // returns a reference to the stored 'int' bsl::get<int>(bslVariant); // returns a reference to the stored 'int' bsl::get_if<int>(bslVariant); // returns a pointer to the stored 'int' bdlbVariant.the<double>(); // undefined behavior bsl::get<double>(bslVariant); // throws 'bsl::bad_variant_access' bsl::get_if<double>(bslVariant); // returns a null pointer
bsl::variant
does not support a null state that is comparable tobdlb::Variant
’s default-constructed state; instead,bsl::variant
always holds a value other than in certain situations where setting a value fails due to an exception. Default construction ofbsl::variant
value-initializes the first alternative type.bsl::variant
can be given a comparable null state by usingbsl::monostate
as the first alternative.// Create a null 'bdlb::Variant' object. bdlb::Variant<int, double> bdlbVariant; // Create a 'bsl::variant' object holding an 'int' with value 0. bsl::variant<int, double> bslVariant; // Create a 'bsl::variant' object that can hold a 'bsl::monostate' (which // may be used by the application to represent the null state), an 'int', // or a 'double'. Because the 'bsl::monostate' alternative is first, the // default constructor activates that alternative. bsl::variant<bsl::monostate, int, double> bslVariant2;
The
bsl::visit
method deduces its return type automatically, in contrast tobdlb::Variant::apply
, which returnsvoid
when the visitor does not have aResultType
member type. In cases where conflicting return types are deduced for different alternatives or (in C++03) return type deduction fails, an explicit return type can be passed as a template parameter tobsl::visit
(in C++03, the methodbsl::visitR
must be used). An example is provided in Migrating Visitors From bdlb::Variant to bsl::variant.In C++11 and later, the constructors and assignment operators of
bsl::variant
can accept argument types that have an unambiguous “best match” with one of the variant types, whereasbdlb::Variant
requires a type that matches exactly with one of the alternative types. (The C++03 implementation ofbsl::variant
also has this restriction.)// ERROR: initializer doesn't match any alternative type bdlb::Variant<int, bsl::string> bdlbVariant = "a C string"; // OK: activates the 'bsl::string' alternative in C++11 and later bsl::variant<int, bsl::string> bslVariant = "a C string";
bsl::variant
follows the guidance in P0178R0: a precondition of theswap
method is that the two objects being swapped have equal allocators. If this precondition is not met, the free functionswap
must be used instead. In contrast, forbdlb::Variant
, member and non-memberswap
both have wide contracts.bslma::TestAllocator ta1("ta1"); bslma::TestAllocator ta2("ta2"); const bsl::string s1 = "this is a string"; const bsl::string s2 = "this is another string"; bdlb::Variant<int, bsl::string> bdlbVariant1(s1, &ta1); bdlb::Variant<int, bsl::string> bdlbVariant2(s2, &ta2); bsl::variant<int, bsl::string> bslVariant1(bsl::allocator_arg, &ta1, s1); bsl::variant<int, bsl::string> bslVariant2(bsl::allocator_arg, &ta2, s2); swap(bdlbVariant1, bdlbVariant2); assert(bdlbVariant1.the<bsl::string>() == s2); assert(bdlbVariant2.the<bsl::string>() == s1); bdlbVariant1.swap(bdlbVariant2); assert(bdlbVariant1.the<bsl::string>() == s1); assert(bdlbVariant2.the<bsl::string>() == s2); swap(bslVariant1, bslVariant2); assert(bsl::get<bsl::string>(bslVariant1) == s2); assert(bsl::get<bsl::string>(bslVariant2) == s1); // undefined behavior, 'bslVariant1' and 'bslVariant2' have different // allocators! bslVariant1.swap(bslVariant2);
bsl::variant
supports relational and equality comparison operators. Two objects of typebsl::variant<T...>
can be compared with a particular operator if and only if all alternatives support that operator (that is, all ofT...
).
Comparison of visitation APIs¶
bdlb::Variant
has a complicated set of visitation overloads due to the fact
that it supports a null state. In contrast, bsl::variant
does not have a
null state; therefore, only two visitation facilities are provided:
bsl::visit(visitor, variant)
, which deduces the return type of visitor
,
and bsl::visit<R>(visitor, variant)
, which returns the explicitly specified
type R
. Note that bsl::visitR<R>
is identical to bsl::visit<R>
but
is provided for C++03 compatibility.
If a bdlb::Variant<T1, T2>
object that is never null is
replaced by a bsl::variant<T1, T2>
object in a new version of client code,
then bsl::visit
should replace both bdlb::Variant::apply
and
bdlb::Variant::applyRaw
, and bsl::visit<R>
should replace both
bdlb::Variant::apply<R>
and bdlb::Variant::applyRaw<R>
:
struct Visitor {
double operator()(int x) const
{
return x + 1;
}
double operator()(double x) const
{
return x + 1;
}
typedef double ResultType; // needed by 'bdlb::Variant::apply'
};
bdlb::Variant<int, double> bdlbVariant = 1;
bsl::variant<int, double> bslVariant = 1;
bdlbVariant.applyRaw(Visitor()); // returns 2.0
bsl::visit(Visitor(), bslVariant); // returns 2.0
On the other hand, if a bdlb::Variant<T1, T2>
object is replaced by a
bsl::variant<bsl::monostate, T1, T2>
object, where the first alternative is
used to represent the null state, then the visitor passed to bsl::visit
or
bsl::visit<R>
must always be able to accept an argument of type
bsl::monostate
, since the latter is one of the alternatives.
Furthermore, bsl::variant
does not provide any direct equivalent to the apply
overloads that accept a default value. Instead, the visitor must be configured
with the desired default value before bsl::visit
is called:
template <class t_TYPE>
struct MyVisitorWithDefault {
t_TYPE d_defaultValue;
MyVisitorWithDefault(t_TYPE defaultValue)
: d_defaultValue(defaultValue) {}
void operator()(int x) const
{
bsl::cout << "Visited int value " << x << '\n';
}
void operator()(double x) const
{
bsl::cout << "Visited double value " << x << '\n';
}
void operator()(bsl::monostate) const
{
(*this)(d_defaultValue);
}
};
void visitMyVariant(const bsl::Variant<bsl::monostate, int, double>& v)
{
// print 42 if 'v' is unset
bsl::visit(MyVisitorWithDefault<int>(42), v);
// print 3.14 if 'v' is unset
bsl::visit(MyVisitorWithDefault<double>(3.14), v);
}
Note that in the example above, the explicit template argument specification at
the call sites of bsl::visit
can be omitted in C++17 and later.
Because bsl::variant
does not support a null state, there is no need for a
visitor to handle the null state. However, if a user chooses to introduce an
alternative of type bsl::monostate
into their bsl::variant
type in
order to represent the absence of any other alternative, then they must ensure
that every visitor for that bsl::variant
type has an overload that can
accept bsl::monostate
, even if they know that this overload will never be
called. There is no analogue to the bdlb::Variant::applyRaw
visitation
method (which does not require the null state of bdlb::Variant
to be
handled).
Migrating Visitors From bdlb::Variant
to bsl::variant
¶
bdlb::Variant::apply
and bsl::visit
both support explicit return type
specification. In cases where the return type is not explicitly specified,
however, bdlb::Variant::apply
and bsl::visit
determine their
respective return types using different rules:
bdlb::Variant::apply
looks within the visitor’s type for a member type calledResultType
. If that type is valid,apply
’s return type is that type. Otherwise,apply
returnsvoid
.bsl::visit
attempts to deduce its return type based on the actual type that invocation of the visitor will return for each alternative. If these types do not agree with each other, the call is ill-formed. In C++03, the return type deduction facility is imperfect due to language limitations. In cases where the return type cannot be determined in C++03, the call is ill-formed.
As a consequence of this difference, some changes might be required to a
visitor that is designed for use with bdlb::Variant::apply
before that
visitor can be used with bsl::variant
.
If a visitor does not have a ResultType
typedef:
If invoking the visitor yields the same type for all alternatives, then the visitor can simply be used as-is with
bsl::visit
; the return type ofbsl::visit
is that type.If invoking the visitor does not yield the same type for all alternatives, or if return type deduction fails in C++03, then
bsl::visit<R>
must be used (orbsl::visitR<R>
in C++03), which has return typeR
. Note thatR
may bevoid
.
bdlb::Variant<int, double> bdlbVariant = 1;
bsl::variant<int, double> bslVariant = 1;
struct Visitor1 {
char operator()(int) const;
char operator()(double) const;
};
bdlbVariant.applyRaw(Visitor1()); // returns 'void'
bsl::visit(Visitor1(), bslVariant); // returns 'char'
struct Visitor2 {
char operator()(int) const;
short operator()(double) const;
};
bdlbVariant.applyRaw(Visitor2()); // returns 'void'
bsl::visit(Visitor2(), bslVariant); // ERROR: conflicting return types
bsl::visit<short>(Visitor2(), bslVariant); // returns 'short'
If a visitor has a ResultType
typedef, bsl::visit
will ignore
ResultType
other than in some cases in C++03 where the return type cannot
otherwise be deduced. If invoking the visitor would return ResultType
for
all alternatives, then bsl::visit
will automatically deduce the same return
type as bdlb::Variant::apply
. If the preceding condition is not met, then
the presence of ResultType
can cause problems when using bsl::variant
.
To avoid such problems, bsl::visit<R>
(or bsl::visitR<R>
in C++03)
should always be used instead of the deduced bsl::visit
. In addition, in
order to avoid bugs due to accidental use of the deduced bsl::visit
,
consider taking one of the following steps, if possible:
Change the definition of the visitor so that the invocation will return
ResultType
for every alternative.If the visitor will not be used with
bdlb::Variant
anymore, then removeResultType
, which will ensure that accidental use of the deducedbsl::visit
will always give a compilation error due to conflicting return types.If the visitor will still be used with
bdlb::Variant
, change the existing call sites so that they specify an explicit return type, then removeResultType
.
The “Valueless By Exception” State¶
Although bsl::variant
does not support a null state, it can sometimes be
left in a state where it does not hold any value. This state can be produced
only by operations that destroy the currently active alternative and then
construct a new one: if the construction of the new alternative exits via an
exception, then there will be no object held by the variant. For this reason,
this state is called “valueless by exception”.
The valueless by exception state is not intended to be used as a null state.
Although certain operations can produce a valueless by exception state, it is
unspecified whether they actually do; there is no supported method for creating
this state deliberately. The valueless_by_exception
accessor can be used
to check whether a bsl::variant
is valueless by exception.
An exception of type bsl::bad_variant_access
is thrown if a variant is
visited while it is valueless by exception.