Feb 22, 2021

bsl::optional: An Allocator-Aware Optional Type

Summary

The latest release of BDE (3.75.1) contains a new type, bsl::optional, that provides an allocator-aware optional type. This type implements the proposed standard type std::pmr::optional (P2047) and is included via the bsl_optional.h header.

For non-allocator-aware types, bsl::optional provides a standard-compliant implementation of std::optional. In fact, if std::optional is provided by the platform (e.g., when compiling with GCC with -std=c++17), bsl::optional publicly inherits from std::optional. For allocator-aware types on all platforms and for non-allocator-aware types on platforms that do not provide std::optional, bsl::optional is an independent implementation.

bdlb::NullableValue has now become a thin wrapper that inherits from bsl::optional and adapts the interface of bsl::optional to that of bdlb::NullableValue.

Differences Between std::optional and bsl::optional

If bsl::optional is instantiated with a template parameter that is not an allocator-aware type (i.e., if bslma::UsesBslmaAllocator<T> is false), it is a standard-conforming implementation of std::optional. For C++17 compilers, bsl::optional will inherit and derive its implementation from the platform-supplied std::optional.

If (and only if) bsl::optional is instantiated on an allocator-aware type, it is an independent implementation that mirrors the standard optional type but differs from it in these respects:

  • There are additional overloads for several constructors and value_or taking an allocator parameter.

  • There is an additional get_allocator method.

  • The footprint is larger to allow storage of the allocator used by the optional object.

Additional Overloads

Constructors

optional(bsl::allocator_arg_t, allocator_type a);

optional(bsl::allocator_arg_t, allocator_type a, bsl::nullopt_t);

optional(bsl::allocator_arg_t, allocator_type a, const optional& o);

optional(bsl::allocator_arg_t, allocator_type a, const optional&& o);

template <class O> optional(bsl::allocator_arg_t, allocator_type a, O&& v);

template <class O> optional(bsl::allocator_arg_t, allocator_type a, const bsl::optional<O>& original)

template <class O> optional(bsl::allocator_arg_t, allocator_type a, bsl::optional<O>&& original)

template <class O> optional(bsl::allocator_arg_t, allocator_type a, const std::optional<O>& original)

template <class O> optional(bsl::allocator_arg_t, allocator_type a, std:optional<O>&& original)

template <class... ARGS> optional(bsl::allocator_arg_t, allocator_type a, bsl::in_place_t, ARGS&&...);

template <class L, class... A> optional(allocator_arg_t, allocator_type, in_place_t, initializer_list<L> , A&&...);

Accessors/Manipulators

template <class ANY_TYPE> T value_or(bsl::allocator_arg_t, allocator_type, ANY_TYPE&& value) const&;

T value_or(bsl::allocator_arg_t, allocator_type a, ANY_TYPE&& value) &&;

Additional Free Functions

constexpr optional<decay_t<T>> make_optional(allocator_arg_t, allocator_type a, T&& v);

constexpr optional<decay_t<T>> make_optional(allocator_arg_t, allocator_type a, ARGS...);

constexpr optional<decay_t<T>> make_optional(allocator_arg_t, allocator_type a, initializer_list<L>, ARGS...);

Note that:

  • All template constructors are explicit.

  • All the constructors with a type O (for “Other”) only exist if T is convertible to O.

  • The methods returning T by-value (e.g., value_or) are only present if the compiler guarantees copy-elision (i.e., C++17 and later).

Constructor Documentation

Unfortunately, the generated doxygen component documentation for the constructors of bsl::optional is hard to understand because of the complex SFINAE constraints on some of the constructors.

Important

We recommend referring to the cpp-reference documentation for optional.

To understand the generated doxygen, the constructor parameters of the form BSLSTL_OPTIONAL_DECLARE_IF_* express the condition under which the constructor is available (e.g., BSLSTL_OPTIONAL_DECLARE_IF_SAME(T, ANY_TYPE) indicates the constructor only exists if the type of the optional being constructed matches the type of the value being passed as a constructor parameter).

Migrating From bdlb::NullableValue

This release preserves backwards compatibility with respect to bdlb::NullableValue (with one exception, see bdlb::NullableValue Behavior Change: C++03-Only Implicit Conversion to bool) and, therefore, no code changes are required to build with the new version of BDE. However, we encourage clients to adopt bsl::optional in place of bdlb::NullableValue (which will eventually be deprecated).

Since bdlb::NullableValue publicly inherits from bsl::optional, users may update public APIs that accept a bdlb::NullableValue to instead accept a bsl::optional without impacting users of that API. For example, changing the signature of the existingFunction below to use bsl::optional would be safe:

// BEFORE
void existingFunction(const bdlb::NullableValue<int>&);

// AFTER
void existingFunction(const bsl::optional<int>&);

Comparison of bsl::optional and bdlb::NullableValue

bsl::optional and bdlb::NullableValue serve the same purpose and have corresponding methods, but the corresponding methods often have different names. The table below shows a mapping of methods of bdlb::NullableValue to those of bsl::optional (operations that do not have direct counterparts in bsl::optional are marked as N/A):

bdlb::NullableValue

bsl::optional

nv.makeValue()

opt.emplace()

nv.makeValueInplace(args...)

opt.emplace(args...)

nv.reset()

opt.reset()

nv.value()

*opt

nv.valueOr(d)

opt.value_or(d)

nv.addressOr(p)

N/A

nv.valueOrNull()

N/A

if (nv.isNull())

if (!opt)

if (nv.isNull())

if (!opt.has_value())

nv = bdlb::nullOpt

opt = bsl::nullopt

Interoperability Between bsl::optional and bdlb::NullableValue

For the most part, interoperability is defined by the inheritance relationship between these two types. For a template parameter type T, the following conversions are supported:

C++17 non-allocator-aware

bdlb::NullableValue<T> -> bsl::optional<T> -> std::optional<T>

C++17 Allocator-Aware, and pre-C++17

bdlb::NullableValue<T> -> bsl::optional<T>

bdlb::NullableValue<T> is always implicitly convertible to bsl::optional<T>, irrespective of whether T is allocator-aware. Both bdlb::NullableValue<T> and bsl::optional<T> are implicitly convertible to std::optional<T> when std::optional is available and T is not allocator-aware.

When instantiated on an allocator-aware type, bsl::optional<T> and std::optional<T> are distinct, unrelated types. However, bsl::optional<T> is constructible (and assignable) from std::optional<U> of a compatible type. The converting constructors are implicit if T is implicitly constructible from U (which applies when U == T since copy constructors are normally implicit), and are explicit if construction of T from U is explicit.

The following example examines the interoperability that is afforded (or not) when initializing a bsl::optional from an std::optional:

void f_string_view(const bsl::optional<bsl::string_view>&);
void f_string     (const bsl::optional<bsl::string>&);

std::optional<bsl::string_view> stdOpt;
bsl::optional<bsl::string>      bslOpt(stdOpt);  // OK  - direct initialization considers explicit constructors
// bsl::optional<bsl::string>   bslOpt = stdOpt; // error - copy initialization can't use explicit constructor

f_string_view(stdOpt);  // OK - implicit construction of bsl::optional<T> from std::optional<T>
// f_string(stdOpt);    // error - can't implicitly construct bsl::optional<bsl::string> from std::optional<bsl::string_view>

The following example explores assignment operations between bsl::optional and std::optional, both for template parameters that are allocator-aware and template parameters that are not allocator-aware:

std::optional<bsl::string_view> stdNonAA;
bsl::optional<bsl::string_view> bslNonAA;

std::optional<bsl::string> stdAA;
bsl::optional<bsl::string> bslAA;

stdNonAA = bslNonAA;  // OK - bsl::optional inherits from std::optional
bslNonAA = stdNonAA;  // OK - assignment operator from std::optional

// stdNonAA = bslAA;  // error - bsl::optional<bsl::string> does not inherit from std::optional<bsl::string>
bslAA    = stdNonAA;  // OK - assignment operator from std::optional<U>

stdNonAA = stdAA;     // OK - no bsl::optional involved
stdAA    = stdNonAA;  // OK - no bsl::optional involved

bslNonAA = stdAA;     // OK - assignment operator from std::optional<U>
stdAA    = bslNonAA;  // OK - bsl::optional inherits from std::optional

bslNonAA = bslAA;     // OK - no std involved
bslAA    = bslNonAA;  // OK - no std involved

// stdAA = bslAA;     // error - bsl::optional<bsl::string> does not inherit from std::optional<std::string>
bslAA    = stdAA;     // assignment operator on bsl::optional taking an std::optional

Relational Operator Changes

A notable behavioral difference between bsl::optional and bdlb::NullableValue is that the relational operators for bdlb::NullableValue are all implemented in terms of operator< (e.g., operator>= is implemented using !operator< on the contained type). By contrast, bsl::optional, per the standard definition for optional, delegates directly to the corresponding operator.

bdlb::NullableValue Behavior Change: C++03-Only Implicit Conversion to bool

For standard-compliance, bsl::optional has a conversion operator to bool, which is inherited by bdlb::NullableValue. The resulting Boolean value indicates whether the nullable object is “engaged”. This conversion is explicit with C++11 and later (per the C++ Standard), but is implicit with C++03 because explicit conversion operators were not supported until C++11. Note that this implicit conversion on C++03 platforms is implemented using the “unspecified Boolean type” idiom.

For example, prior to the release of bsl::optional, the following did not assert in any build mode but it now asserts on C++03 platforms:

typedef bdlb::NullableValue<double> Arbitrary;

assert(!(bsl::is_convertible<Arbitrary, bool>::value));

The result is the same when double is substituted with any other type.