Using std::optional to store optional values

Sometimes, it is useful to be able to store either a value or a null if a value is not available. A typical example for such a case is the return value of a function that may fail to produce a return value, but this failure is not an error. For instance, think of a function that finds and returns values from a dictionary by specifying a key. Not finding a value is a probable case and, therefore, the function would either return a Boolean (or an integer value, if more error codes are necessary) and have a reference argument to hold the return value or return a pointer (raw or smart pointer). In C++17, std::optional is a better alternative to these solutions. The class template std::optional is a template container for storing a value that may or may not exist. In this section, we will see how to use this container and what are its typical use cases.

The class template std::optional<T> was designed based on boost::optional and is available in the <optional> header. If you are familiar with boost::optional and have used it in your code, you can migrate it seamlessly to std::optional.

How to Use std::optional<T>

Use the following operations to work with std::optional:

Use std::optional to model any of the following:

How std::optional Works

The class template std::optional is a class template that represents a container for an optional value. If the container does have a value, that value is stored as part of the optional object; no heap allocations and pointers are involved. The std::optional class template is conceptually implemented like this:

template <typename T>
class optional {
  bool _initialized;
  std::aligned_storage_t<sizeof(t), alignof(T)>_storage;
};

The std::aligned_storage_t alias template allows us to create uninitialized chunks of memory that can hold objects of a given type. The class template std::optional does not contain a value if it was default constructed, or if it was copy constructed or copy assigned from another empty optional object or from an std::nullopt_t value. This is a helper type, implemented as an empty class, that indicates an optional object with an uninitialized state.

The typical use for an optional type (called nullable in other programming languages) is the return type from a function that may fail. Some alternative solutions for this situation include the following:

The class template std::optional is a better approach because, on one hand, it does not involve output parameters to the function (which is unnatural for returning values) and does not require working with pointers, and, on the other hand, it better encapsulates the details of an std::pair<T, bool>. However, optional objects can also be used for class data members, and compilers are able to optimize the memory layout for an efficient storage.

The class template std::optional cannot be used to return polymorphic types. If you write, for instance, a factory method that needs to return different types from a hierarchy of types, you cannot rely on std::optional and need to return a pointer, preferably a std::shared_ptr or std::unique_ptr (depending if ownership of the object needs to be shared or not).

When you use std::optional to pass optional arguments to a function, you need to understand that it may incur creating copies, which can be a performance issue if large objects are involved. Let's consider the following example of a function that has a constant reference to the std::optional parameter:

struct bar { /* details */ };

void process(std::optional<bar>const &arg) {
  /* do something with arg */
}

std::optional<bar> b1{ bar{} };
bar b2{};

process(b1); // no copy
process(b2); // copy construction

The first call to process() does not involve any additional object construction because we pass an std::optional<bar>object. The second call, however, will involve the copy construction of a bar object, because b2 is a bar and needs to be copied to an std::optional<bar>; a copy is made even if bar has move semantics implemented. If bar was a small object, this shouldn't be of a great concern, but for large objects, it can prove a performance issue. The solution to avoid this depends on the context, and can involve creating a second overload that takes a constant reference to bar, or entirely avoiding using std::optional.