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_allocatormethod, 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::variantis larger than that ofstd::variantbecause 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::visitcan only visit a single variant at a time.Most of the methods and free functions of
bsl::variantare notconstexprand do not havenoexceptspecifications.bsl::variantdoes not have any trivial member functions. In C++17,std::varianthas a conditionally trivial destructor, and in C++20, the copy constructor, move constructor, copy assignment operator, and move assignment operator ofstd::variantare also conditionally trivial.bsl::variantdoes not provide the strong exception safety guarantee for any function call that changes which alternative is currently active within absl::variantobject.
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::varianthas full support for move semantics for both variants and visitors. In C++03,bsl::variantstill 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::variantexpressions, 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::Varianthad 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::getmethods, which return a reference to an alternative, fail by throwing an exception if the desired alternative is not active inside thebsl::variantobject, whereasbdlb::Variantdoes not use exceptions. Thebsl::get_ifmethod, 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::variantdoes not support a null state that is comparable tobdlb::Variant’s default-constructed state; instead,bsl::variantalways holds a value other than in certain situations where setting a value fails due to an exception. Default construction ofbsl::variantvalue-initializes the first alternative type.bsl::variantcan be given a comparable null state by usingbsl::monostateas 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::visitmethod deduces its return type automatically, in contrast tobdlb::Variant::apply, which returnsvoidwhen the visitor does not have aResultTypemember 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::visitRmust 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::variantcan accept argument types that have an unambiguous “best match” with one of the variant types, whereasbdlb::Variantrequires a type that matches exactly with one of the alternative types. (The C++03 implementation ofbsl::variantalso 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::variantfollows the guidance in P0178R0: a precondition of theswapmethod is that the two objects being swapped have equal allocators. If this precondition is not met, the free functionswapmust be used instead. In contrast, forbdlb::Variant, member and non-memberswapboth 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::variantsupports 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::applylooks within the visitor’s type for a member type calledResultType. If that type is valid,apply’s return type is that type. Otherwise,applyreturnsvoid.bsl::visitattempts 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::visitis 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 thatRmay 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
ResultTypefor every alternative.If the visitor will not be used with
bdlb::Variantanymore, then removeResultType, which will ensure that accidental use of the deducedbsl::visitwill 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.