Using std::optional in C++ to improve code safety

2 minute read

Attempting to use a null value is a common reason for crashes in computer programs. Many programming languages allow using null almost like any other value. If a variable can be null, we should ideally always check that case before continuing. Unfortunately, it requires a lot of discipline to remember about this.

C++17 added std::optional template which helps make our code safer.

Without using std::optional

Let’s consider an example. We have create() function which returns an object of Something class wrapped in a unique pointer. We check a condition (check()) and if a condition fails, we return nullptr (i.e. our null value) to signify something didn’t go well.

 1// Returns true if everything's fine
 2bool check();
 3
 4struct Something {
 5    int x;
 6};
 7
 8std::unique_ptr<Something> create() {
 9    if (check()) {
10        return std::make_unique<Something>(1);
11    }
12
13    // Return nullptr to suggest check() failed
14    return nullptr;
15}

A problem may occur when we try to use the returned value.

1// `something` could be a nullptr
2auto something = create();
3
4// Dangereous - it will crash if `something` is null
5std::cout << "Something: "
6            << something->x
7            << std::endl;

Due to a possibility of something variable being a nullptr, accessing it (e.g. something->x) will crash the application if something is in fact a nullptr.

Using std::optional

Let’s use std::optional to prevent issues with null values. Function create_with_optional() will now be returning std::optional<Something>.

1std::optional<Something> create_with_optional() {
2    if (check()) {
3        return Something{1};
4    }
5
6    // No value
7    return {};
8}

What happens when we try to use it? Because we’re not getting a value directly, we need to check if a value is present (using has_value()) and then get the value (by calling value()).

1auto something2 = create_with_optional();
2
3if (something2.has_value()) {
4    std::cout << "Something2 (value): "
5              << something2.value().x
6              << std::endl;
7} else {
8    // No value available
9}

There’s also a shorthand version that allows us to define a default value (0 in this case).

1std::cout << "Something2: "
2          << something2.value_or(Something{0}).x
3          << std::endl;

All the extra code is there to suggest how the data should be accessed safely (and that something bad can happen otherwise).

It’s worth mentioning that it’s possible to skip the checks using the arrow notation (->).

1// Dangerous
2std::cout << "Something2: "
3          << something2->x
4          << std::endl;

It works as expected when the value is present. However, in case of nullptr, even though it doesn’t crash the application, it returns an undefined value for x which can be seriously misleading because then the application might continue to work although in a somewhat unpredictable fashion, so it’s worth being careful about that.