page-fault

std::optional

Sometimes we need a special sentinel value for an object that specifies a special ‘null’ state for that object. Sometimes, special values like -1, nullptr, or std::numeric_limits<int>::max() can be used (see string::find, which returns npos on failure). When using this technique, care must be taken not to accidentally use the special value in a context where a ‘non-sentinel’ value was expected.

An alternative approach is to add a bool flag to the object that specifies whether or not the object is valid, which is the approach taken by optional. This approach has the advantage of retaining value semantics, whereas using a nullable unique_ptr (for example) would require pointer semantics and heap allocation.

Use cases

Constructing optionals

Optionals can be:

in_place construction

This is mostly useful for constructing non-copyable non-moveable types into an optional, especially in contexts where auto deduction cannot be used (class data member declarations).

struct Inner
{
    Inner (std::string s, int i) {}

    Inner (const Inner&) = delete;
    Inner (Inner&&) noexcept = delete;

    Inner& operator= (const Inner&) = delete;
    Inner& operator= (Inner&&) noexcept = delete;
};

struct Outer
{
    // If we used `make_optional<Inner>` here instead, we'd be repeating the type `Inner`
    // unnecessarily
    std::optional<Inner> optionalInner { std::in_place, "foo", 42 };
};

In other contexts, C++17 guarantees RVO, so it should be possible to use make_optional even with non-copyable non-moveable types:

auto optionalInner = std::make_optional<Inner> ("foo", 42);
// The book says that non-copyable non-moveable types require `in_place` but this seems to build...
auto opt = std::make_optional<std::mutex>();

Returning optional values

Optionals default-construct as empty, and can be implicitly converted from an instance of their wrapped type, so it’s often simplest to do something like this:

std::optional<int> computeValue (Input input)
{
    if (input.valid())
        return input.getIntValue(); // Mandatory RVO applies here, don't wrap this in `{}`!

    return {};
}

Note that wrapping the returned value with {} will force a copy, so make sure to omit the braces if you want to avoid unnecessary temporaries.

Accessing optionals

Access can be quite clean with initialised if statements:

if (auto maybeValue = compute(); maybeValue != std::nullopt)
    // do something with *maybeValue

Other features

The contained object can be replaced or destroyed using emplace, reset, swap, and operator=.

Optionals can be compared using operator< etc., and nullopt will always compare less-than a ‘real’ value. (Think carefully before actually using this functionality!)

Warnings

Optional objects may double the required memory for a wrapped object, due to alignment rules. For single optionals, this probably doesn’t matter, but if you have lots of optionals in a class, or are using very large optional objects, there may be more efficient alternatives. Profile before writing anything custom, though!