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 of std::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 not constexpr and do not have noexcept 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 of std::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 a bsl::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

bdlb::Variant<T1, T2> v;

bsl::variant<bsl::monostate, T1, T2> v;

v.assign(x)

v = x

v.assignTo<T1>(x)

v.emplace<T1>(x)

v.createInPlace<T1>(args...)

v.emplace<T1>(args...)

v.reset()

v.emplace<bsl::monostate>()

v.is<T1>()

bsl::holds_alternative<T1>(v)

v.isUnset()

bsl::holds_alternative<bsl::monostate>(v)

v.the<T1>()

bsl::get<T1>(v)

v.typeIndex()

v.index()

v.getAllocator()

v.get_allocator()

v.apply(visitor)

bsl::visit(visitor, v)

v.apply(visitor, defaultValue)

See below

v.apply<R>(visitor)

bsl::visit<R>(visitor, v)

v.apply<R>(visitor, defaultValue)

See below

v.applyRaw(...)

See below

v.applyRaw<R>(...)

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 using bsl::variant, a single visitor can be applied to both lvalue and rvalue bsl::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 the bsl::string&& overload would never be called; it would be necessary to write a second visitor taking a bsl::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 the bsl::variant object, whereas bdlb::Variant does not use exceptions. The bsl::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 to bdlb::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 of bsl::variant value-initializes the first alternative type. bsl::variant can be given a comparable null state by using bsl::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 to bdlb::Variant::apply, which returns void when the visitor does not have a ResultType 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 to bsl::visit (in C++03, the method bsl::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, whereas bdlb::Variant requires a type that matches exactly with one of the alternative types. (The C++03 implementation of bsl::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 the swap method is that the two objects being swapped have equal allocators. If this precondition is not met, the free function swap must be used instead. In contrast, for bdlb::Variant, member and non-member swap 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 type bsl::variant<T...> can be compared with a particular operator if and only if all alternatives support that operator (that is, all of T...).

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 called ResultType. If that type is valid, apply’s return type is that type. Otherwise, apply returns void.

  • 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 of bsl::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 (or bsl::visitR<R> in C++03), which has return type R. Note that R may be void.

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 remove ResultType, which will ensure that accidental use of the deduced bsl::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 remove ResultType.

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.