Table of Contents

Let’s take a pair of two types <YourType, bool> - what can you do with such composition?

In this article, I’ll describe std:optional - a new helper type added in C++17. It’s a wrapper for your type and a flag that indicates if the value is initialized or not. Let’s see where it can be useful and how you can use it.

Intro  

By adding the boolean flag to other types, you can achieve a thing called “nullable types”. As mentioned, the flag is used to indicate whether the value is available or not. Such wrapper represents an object that might be empty in an expressive way (so not via comments :))

While you can achieve “null-ability” by using unique values (-1, infinity, nullptr), it’s not as clear as the separate wrapper type. Alternatively, you could even use std::unique_ptr<Type> and treat the empty pointer as not initialized - this works, but comes with the cost of allocating memory for the object.

Optional types - that come from functional programming world - bring type safety and expressiveness. Most of other languages have something similar: for example std::option in Rust, Optional<T> in Java, Data.Maybe in Haskell.

std::optional was added in C++17 and brings a lot of experience from boost::optional that was available for many years. Since C++17 you can just #include <optional> and use the type.

Such wrapper is still a value type (so you can copy it, via deep copy). What’s more, std::optional doesn’t need to allocate any memory on the free store.

std::optional is a part of C++ vocabulary types along with std::any, std::variant and std::string_view.

When to use  

Usually, you can use an optional wrapper in the following scenarios:

  • If you want to represent a nullable type nicely.
    • Rather than using unique values (like -1, nullptr, NO_VALUE or something)
    • For example, user’s middle name is optional. You could assume that an empty string would work here, but knowing if a user entered something or not might be important. With std::optional<std::string> you get more information.
  • Return a result of some computation (processing) that fails to produce a value and is not an error.
    • For example finding an element in a dictionary: if there’s no element under a key it’s not an error, but we need to handle the situation.
  • To perform lazy-loading of resources.
    • For example, a resource type has no default constructor, and the construction is substantial. So you can define it as std::optional<Resource> (and you can pass it around the system), and then load only if needed later.
  • To pass optional parameters into functions.

I like the description from boost optional which summarizes when we should use the type:

From the boost::optional documentation: When to use Optional

It is recommended to use optional<T> in situations where there is exactly one, clear (to all parties) reason for having no value of type T, and where the lack of value is as natural as having any regular value of T

While sometimes the decision to use optional might be blurry, you shouldn’t use it for error handling. As it best suits the cases when the value is empty and it’s a normal state of the program.

Basic Example  

Here’s a simple example of what you can do with optional:

std::optional<std::string> UI::FindUserNick()
{
    if (nick_available)
        return { mStrNickName };

    return std::nullopt; // same as return { };
}

// use:
std::optional<std::string> UserNick = UI->FindUserNick();
if (UserNick)
    Show(*UserNick);

In the above code we define a function that returns optional containing a string. If the user’s nickname is available, then it will return a string. If not, then it returns nullopt. Later we can assign it to an optional and check (it converts to bool) if it contains any value or not. Optional defines operator* so we can easily access the contained value.

In the following sections you’ll see how to createstd::optional, operate on it, pass around and even what is the performance cost you might want to consider.

The C++17 Series  

This article is part of my series about C++17 Library Utilities. Here’s the list of the other topics that I’ll cover:

Resources about C++17 STL:

OK, so let’s move to std::optional.

std::optional Creation  

There are several ways to create std::optional:

// empty:
std::optional<int> oEmpty;
std::optional<float> oFloat = std::nullopt;

// direct:
std::optional<int> oInt(10);
std::optional oIntDeduced(10); // deduction guides

// make_optional
auto oDouble = std::make_optional(3.0);
auto oComplex = make_optional<std::complex<double>>(3.0, 4.0);

// in_place
std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};

// will call vector with direct init of {1, 2, 3}
std::optional<std::vector<int>> oVec(std::in_place, {1, 2, 3});

// copy/assign:
auto oIntCopy = oInt;

As you can see in the above code sample, you have a lot of flexibility with the creation of optional. It’s very simple for primitive types and this simplicity is extended for even complex types.

The in_place construction is especially interesting, and the tag std::in_place is also supported in other types like any and variant.

For example, you can write:

// https://godbolt.org/g/FPBSak
struct Point
{
    Point(int a, int b) : x(a), y(b) { }

    int x;
    int y;
};

std::optional<Point> opt{std::in_place, 0, 1};
// vs
std::optional<Point> opt{{0, 1}};

This saves the creation of a temporary Point object.

I’ll address std::in_place later in a separate post, so stay tuned.

Returning std::optional  

If you return an optional from a function, then it’s very convenient to return just std::nullopt or the computed value.

std::optional<std::string> TryParse(Input input)
{
    if (input.valid())
        return input.asString();

    return std::nullopt;
}

In the above example you can see that I return std::string computed from input.asString() and it’s wrapped in optional. If the value is unavailable then you can just return std::nullopt.

Of course, you can also declare an empty optional at the beginning of your function and reassign if you have the computed value. So we could rewrite the above example as:

std::optional<std::string> TryParse(Input input)
{
    std::optional<std::string> oOut; // empty

    if (input.valid())
        oOut = input.asString();

    return oOut;    
}

It probably depends on the context which version is better. I prefer short functions, so I’d chose the first option (with multiple returns).

Accessing The Stored Value  

Probably the most important operation for optional (apart from creation) is the way how you can fetch the contained value.

There are several options:

  • operator* and operator->- similar to iterators. If there’s no value the behaviour is undefined!
  • value() - returns the value, or throws std::bad_optional_access
  • value_or(defaultVal) - returns the value if available, or defaultVal otherwise.

To check if the value is present you can use has_value() method or just check if (optional) as optional is automatically converted to bool.

Here’s an example:

// by operator*
std::optional<int> oint = 10;
std::cout<< "oint " << *opt1 << '\n';

// by value()
std::optional<std::string> ostr("hello");
try
{
    std::cout << "ostr " << ostr.value() << '\n';  
}
catch (const std::bad_optional_access& e)
{
    std::cout << e.what() << "\n";
}

// by value_or()
std::optional<double> odouble; // empty
std::cout<< "odouble " << odouble.value_or(10.0) << '\n';

So the most useful way is probably just to check if the value is there and then access it:

    // compute string function:
    std::optional<std::string> maybe_create_hello();  
    // ...  

    if (auto ostr = maybe_create_hello(); ostr)
        std::cout << "ostr " << *ostr << '\n';  
    else  
        std::cout << "ostr is null\n";

std::optional Operations  

Let’s see what are other operations on the type:

Changing the value  

If you have existing optional object, then you can easily change the contained value by using several operations like emplace, reset, swap, assign. If you assign (or reset) with a nullopt then if the optional contains a value its destructor will be called.

Here’s a little summary:

#include <optional>
#include <iostream>
#include <string>

class UserName
{
public:
    explicit UserName(const std::string& str) : mName(str)
    { 
        std::cout << "UserName::UserName(\'";
        std::cout << mName << "\')\n"; 
    }
    ~UserName() 
    {
        std::cout << "UserName::~UserName(\'";
        std::cout << mName << "\')\n"; 
    }

private:
    std::string mName;
};

int main()
{
    std::optional<UserName> oEmpty;

    // emplace:
    oEmpty.emplace("Steve");

    // calls ~Steve and creates new Mark:
    oEmpty.emplace("Mark");


    // reset so it's empty again
    oEmpty.reset(); // calls ~Mark
    // same as:
    //oEmpty = std::nullopt;

    // assign a new value:
    oEmpty.emplace("Fred");
    oEmpty = UserName("Joe"); 
}

The code is available here: @Coliru

Comparisons  

std::optional allows you to compare contained objects almost “normally”, but with a few exceptions when the operands are nullopt.

See below:

#include <optional>
#include <iostream>

int main()
{
    std::optional<int> oEmpty;
    std::optional<int> oTwo(2);
    std::optional<int> oTen(10);

    std::cout << std::boolalpha;
    std::cout << (oTen > oTwo) << "\n";
    std::cout << (oTen < oTwo) << "\n";
    std::cout << (oEmpty < oTwo) << "\n";
    std::cout << (oEmpty == std::nullopt) << "\n";
    std::cout << (oTen == 10) << "\n";
}

The above code generates:

true  // (oTen > oTwo)
false // (oTen < oTwo)
true  // (oEmpty < oTwo)
true  // (oEmpty == std::nullopt)
true  // (oTen == 10)

The code is available here: @Coliru

Examples of std::optional  

Here are two a few longer examples where std::optional fits nicely.

User name with an optional nickname and age  

#include <optional>
#include <iostream>

class UserRecord
{
public:
    UserRecord (const std::string& name, std::optional<std::string> nick, std::optional<int> age)
    : mName{name}, mNick{nick}, mAge{age}
    {
    }

    friend std::ostream& operator << (std::ostream& stream, const UserRecord& user);

private:
    std::string mName;
    std::optional<std::string> mNick;
    std::optional<int> mAge;

};

std::ostream& operator << (std::ostream& os, const UserRecord& user) 
{
    os << user.mName << ' ';
    if (user.mNick) {
        os << *user.mNick << ' ';
    }
    if (user.mAge)
        os << "age of " << *user.mAge;

    return os;
}

int main()
{
    UserRecord tim { "Tim", "SuperTim", 16 };
    UserRecord nano { "Nathan", std::nullopt, std::nullopt };

    std::cout << tim << "\n";
    std::cout << nano << "\n";
}

The code is available here: @Coliru

Parsing ints from the command line  

#include <optional>
#include <iostream>
#include <string>

std::optional<int> ParseInt(char*arg)
{
    try 
    {
        return { std::stoi(std::string(arg)) };
    }
    catch (...)
    {
        std::cout << "cannot convert \'" << arg << "\' to int!\n";
    }

    return { };
}

int main(int argc, char* argv[])
{
    if (argc >= 3)
    {
        auto oFirst = ParseInt(argv[1]);
        auto oSecond = ParseInt(argv[2]);

        if (oFirst && oSecond)
        {
            std::cout << "sum of " << *oFirst << " and " << *oSecond;
            std::cout << " is " << *oFirst + *oSecond << "\n";
        }
    }
}

The code is available here: @Coliru

The above code uses optional to indicate if we performed the conversion or not. Note that we in fact converted exceptions handling into optional, so we skip the errors that might appear. This might be “controversial” as usually, we should report errors.

Other examples  

  • Representing other optional entries for your types. Like in the example of a user record. It’s better to write std::optonal<Key> rather than use a comment to make notes like // if the 'key is 0x7788 then it's empty or something :)
  • Return values for Find*() functions (assuming you don’t care about errors, like connection drops, database errors or something)

See more in: A Wall of Your std::optional Examples - C++ Stories

Performance & Memory consideration  

When you use std::optional you’ll pay with increased memory footprint. At least one extra byte is needed.

Conceptually your version of the standard library might implement optional as:

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

public:
   // operations
};

In short optional just wraps your type, prepares a space for it and then adds one boolean parameter. This means it will extend the size of your Type according do the alignment rules.

There was one comment about this construction:

“And no standard library can implement optional this way (they need to use a union, because constexpr)”. So the code above is only to show an example, not real implementation.

Alignment rules are important as The standard defines:

Class template optional [optional.optional]:
The contained value shall be allocated in a region of the optional storage suitably aligned for the type T.

For example:

// sizeof(double) = 8
// sizeof(int) = 4
std::optional<double> od; // sizeof = 16 bytes
std::optional<int> oi; // sizeof = 8 bytes

While bool type usually takes only one byte, the optional type need to obey the alignment rules and thus the whole wrapper is larger than just sizeof(YourType) + 1 byte.

For example, if you have a type like:

struct Range
{
    std::optional<double> mMin;
    std::optional<double> mMax;
};

it will take more space than when you use your custom type:

struct Range
{
    bool mMinAvailable;
    bool mMaxAvailable;
    double mMin;
    double mMax;
};

In the first case, we’re using 32 bytes! The second version is 24 bytes.

Test code using Compiler Explorer

Here’s a great description about the performance and memory layout taken from boost documentation: Performance considerations - 1.67.0.

And in Efficient optional values | Andrzej’s C++ blog the author discusses how to write a custom optional wrapper that might be a bit faster

I wonder if there’s a chance to do some compiler magic and reuse some space and fit this extra “initialized flag” inside the wrapped type. So no extra space would be needed.

Migration from boost::optional  

std::optional was adapted directly from boost::optional, so you should see the same experience in both versions. Moving from one to another should be easy, but of course, there are little differences.

In the paper: N3793 - A proposal to add a utility class to represent optional objects (Revision 4) - from 2013-10-03 I’ve found the following table (and I tried to correct it when possible with the current state).

aspect std::optional boost::optional (as of 1.67.0)
Move semantics yes no yes in current boost
noexcept yes no yes in current boost
hash support yes no
a throwing value accessor yes yes
literal type (can be used in constexpr expressions) yes no
in place construction `emplace`, tag `in_place` emplace(), tags in_place_init_if_t, in_place_init_t, utility in_place_factory
disengaged state tag nullopt none
optional references no yes
conversion from optional<U> to optional<T> yes yes
explicit convert to ptr (get_ptr) no yes
deduction guides yes no

Special case: optional<bool> and optional<T*>  

While you can use optional on any type you need to pay special attention when trying to wrap boolean or pointers.

std::optional<bool> ob - what does it model? With such construction you basically have a tri-state bool. So if you really need it, then maybe it’s better to look for a real tri-state bool like boost::tribool.

Whet’s more it might be confusing to use such type because ob converts to bool if there’s a value inside and *ob returns that stored value (if available).

Similarly you have a similar confusion with pointers:

// don't use like that! only an example!
std::optional<int*> opi { new int(10) };
if (opi && *opi)
{
   std::cout << **opi << std::endl;
   delete *opi;
}
if (opi)
    std::cout << "opi is still not empty!";

The pointer to int is naturally “nullable”, so wrapping it into optional makes it very hard to use.

C++20 & C++23 Changes  

This section will be extended in the future

In C++17, std::optional was mostly marked with constexpr, but some key functionalities like construction were not allowed. In C++23… marked as DR against C++20, those limitations were lifted - see P2231

As for C++23 the main addition are “monadic operations”: P0798. See an example at https://godbolt.org/z/s3rs8zKMn

Wrap up  

Uff… ! it was a lot of text about optional, but still it’s not all :)

Yet, we’ve covered the basic usage, creation and operations of this useful wrapper type. I believe we have a lot of cases where optional fits perfectly and much better than using some predefined values to represent nullable types.

I’d like to remember the following things about std::optional:

  • std::optional is a wrapper type to express “null-able” types.
  • std::optional won’t use any dynamic allocation
  • std::optional contains a value or it’s empty
    • use operator *, operator->, value() or value_or() to access the underlying value.
  • std::optional is implicitly converted to bool so that you can easily check if it contains a value or not.

In the next article I’ll try to explain error handling and why optional is maybe not the best choice there.

I’d like to thank Patrice Roy (@PatriceRoy1), Jacek Galowicz (@jgalowicz) and Andrzej Krzemienski (akrzemi) for finding time do do a quick review of this article!