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.
nullopt
if the value
wasn’t present.nullopt
if the entity lies
outside the viewport.future
or a Connection
that we want to start and
end at will. We can store an instance of that type in an optional
, and emplace
or
reset
the instance to trigger the constructor/destructor behaviour on demand.Optionals can be:
operator=
std::in_place
in_place
constructionThis 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>();
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.
operator*
and operator->
act as you would expect, UB if there’s no valuevalue()
throws if there’s no valuevalue_or(def)
returns the wrapped value if available, or def
otherwiseAccess can be quite clean with initialised if statements:
if (auto maybeValue = compute(); maybeValue != std::nullopt)
// do something with *maybeValue
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!)
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!