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
:
-
To store a value, use the constructor or assign the value directly to an
std::optional
object:std::optional<int> v1; // v1 is empty std::optional<int> v2(42); // v2 contains 42 v1 = 42; // v1 contains 42 std::optional<int> v3 = v2; // v3 contains 42
-
To read the stored value, use operator* or operator->:
std::optional<int> v1{ 42 }; std::cout << *v1 << std::endl; // 42 std::optional<foo> v2{ foo{ 42, 10.5 } }; std::cout << v2->a << ", " << v2->b << std::endl; // 42, 10.5
-
Alternatively, use member functions
value()
andvalue_or()
to read the stored value:std::optional<std::string> v1{ "text"s }; std::cout << v1.value() << std::endl; // text std::optional<std::string> v2; std::cout << v2.value_or("default"s) << std::endl; // default
-
To check whether the container stores a value, use a conversion operator to bool or the member function
has_value()
:struct foo { int a; double b; }; std::optional<int> v1{ 42 }; if (v1) std::cout << *v1 <<std::endl; std::optional<foo>v2{ foo{ 42, 10.5 } }; if (v2.has_value()) std::cout << v2->a << ", " << v2->b << std::endl;
-
To modify the stored value, use member functions
emplace()
,reset()
, orswap()
:std::optional<int>v{ 42 }; // v contains 42 v.reset(); // v is empty
Use std::optional to model any of the following:
-
Return values from functions that may fail to produce a value:
template <typename K, typename V> std::optional<V>find(int const key, std::map<K, V>const &m) { auto pos = m.find(key); if (pos != m.end()) return pos->second; return {}; } std::map<int, std::string> m{ { 1, "one"s },{ 2, "two"s },{ 3, "three"s } }; auto value = find(2, m); if (value) std::cout << *value << std::endl; // two value = find(4, m); if (value) std::cout <<*value <<std::endl;
-
Parameters to functions that are optional:
std::string extract(std::string const &text, std::optional<int> start, std::optional<int> end) { auto s = start.value_or(0); auto e = end.value_or(text.length()); return text.substr(s, e - s); } auto v1 = extract("sample"s, {}, {}); std::cout << v1 << std::endl; // sample auto v2 = extract("sample"s, 1, {}); std::cout << v2 << std::endl; // ample auto v3 = extract("sample"s, 1, 4); std::cout << v3 << std::endl; // amp
-
Class data members that are optional:
struct book { std::string title; std::optional<std::string> subtitle; std::vector<std::string> authors; std::string publisher; std::string isbn; std::optional<int> pages; std::optional<int> year; };
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:
- Return an std::pair<T, bool>, where T is the type of the return value; the second element of the pair is a Boolean flag that indicates whether the value of the first element is valid or not.
- Return a bool and take an extra parameter of type T& and assign a value to this parameter only if the function succeeds.
- Return a raw or smart pointer type, and use
nullptr
to indicate a failure.
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.
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.