std::optional

Introduction

Optionals (also known as Maybe types) are used to represent a type whose contents may or may not be present. They are implemented in C++17 as the std::optional class. For example, an object of type std::optional<int> may contain some value of type int, or it may contain no value.

Optionals are commonly used either to represent a value that may not exist or as a return type from a function that can fail to return a meaningful result.

Other approaches to optional

There are many other approach to solving the problem that std::optional solves, but none of them are quite complete: using a pointer, using a sentinel, or using a pair<bool, T>.

Optional vs Pointer

In some cases, we can provide a pointer to an existing object or nullptr to indicate failure. But this is limited to those cases where objects already exist - optional, as a value type, can also be used to return new objects without resorting to memory allocation.

Optional vs Sentinel

A common idiom is to use a special value to indicate that the value is meaningless. This may be 0 or -1 for integral types, or nullptr for pointers. However, this reduces the space of valid values (you cannot differentiate between a valid 0 and a meaningless 0) and many types do not have a natural choice for the sentinel value.

Optional vs std::pair<bool, T>

Another common idiom is to provide a pair, where one of the elements is a bool indicating whether or not the value is meaningful.

This relies upon the value type being default-constructible in the case of error, which is not possible for some types and possible but undesirable for others. An optional<T>, in the case of error, does not need to construct anything.

optional as return value

std::optional<float> divide(float a, float b) {
  if (b!=0.f) return a/b;
  return {};
}

Here we return either the fraction a/b, but if it is not defined (would be infinity) we instead return the empty optional.

A more complex case:

template<class Range, class Pred>
auto find_if( Range&& r, Pred&& p ) {
  using std::begin; using std::end;
  auto b = begin(r), e = end(r);
  auto r = std::find_if(b, e , p );
  using iterator = decltype(r);
  if (r==e)
    return std::optional<iterator>();
  return std::optional<iterator>(r);
}
template<class Range, class T>
auto find( Range&& r, T const& t ) {
  return find_if( std::forward<Range>(r), [&t](auto&& x){return x==t;} );
}

find( some_range, 7 ) searches the container or range some_range for something equal to the number 7. find_if does it with a predicate.

It returns either an empty optional if it was not found, or an optional containing an iterator tothe element if it was.

This allows you to do:

if (find( vec, 7 )) {
  // code
}

or even

if (auto oit = find( vec, 7 )) {
  vec.erase(*oit);
}

without having to mess around with begin/end iterators and tests.

Using optionals to represent the absence of a value

Before C++17, having pointers with a value of nullptr commonly represented the absence of a value. This is a good solution for large objects that have been dynamically allocated and are already managed by pointers. However, this solution does not work well for small or primitive types such as int, which are rarely ever dynamically allocated or managed by pointers. std::optional provides a viable solution to this common problem.

In this example, struct Person is defined. It is possible for a person to have a pet, but not necessary. Therefore, the pet member of Person is declared with an std::optional wrapper.

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

struct Animal {
    std::string name;
};

struct Person {
    std::string name;
    std::optional<Animal> pet;
};

int main() {
    Person person;
    person.name = "John";

    if (person.pet) {
        std::cout << person.name << "'s pet's name is " <<
            person.pet->name << std::endl;
    }
    else {
        std::cout << person.name << " is alone." << std::endl;
    }
}

Using optionals to represent the failure of a function

Before C++17, a function typically represented failure in one of several ways:

  • A null pointer was returned.
    • e.g. Calling a function Delegate *App::get_delegate() on an App instance that did not have a delegate would return nullptr.
    • This is a good solution for objects that have been dynamically allocated or are large and managed by pointers, but isn't a good solution for small objects that are typically stack-allocated and passed by copying.
  • A specific value of the return type was reserved to indicate failure.
    • e.g. Calling a function unsigned shortest_path_distance(Vertex a, Vertex b) on two vertices that are not connected may return zero to indicate this fact.
  • The value was paired together with a bool to indicate is the returned value was meaningful.
    • e.g. Calling a function std::pair<int, bool> parse(const std::string &str) with a string argument that is not an integer would return a pair with an undefined int and a bool set to false.

In this example, John is given two pets, Fluffy and Furball. The function Person::pet_with_name() is then called to retrieve John's pet Whiskers. Since John does not have a pet named Whiskers, the function fails and std::nullopt is returned instead.

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

struct Animal {
    std::string name;
};

struct Person {
    std::string name;
    std::vector<Animal> pets;
    
    std::optional<Animal> pet_with_name(const std::string &name) {
        for (const Animal &pet : pets) {
            if (pet.name == name) {
                return pet;
            }
        }
        return std::nullopt;
    }
};

int main() {
    Person john;
    john.name = "John";
    
    Animal fluffy;
    fluffy.name = "Fluffy";
    john.pets.push_back(fluffy);
    
    Animal furball;
    furball.name = "Furball";
    john.pets.push_back(furball);
    
    std::optional<Animal> whiskers = john.pet_with_name("Whiskers");
    if (whiskers) {
        std::cout << "John has a pet named Whiskers." << std::endl;
    }
    else {
        std::cout << "Whiskers must not belong to John." << std::endl;
    }
}

value_or

void print_name( std::ostream& os, std::optional<std::string> const& name ) {
  std::cout "Name is: " << name.value_or("<name missing>") << '\n';
}

value_or either returns the value stored in the optional, or the argument if there is nothing store there.

This lets you take the maybe-null optional and give a default behavior when you actually need a value. By doing it this way, the "default behavior" decision can be pushed back to the point where it is best made and immediately needed, instead of generating some default value deep in the guts of some engine.



2016-07-22
2017-06-24
C++ Pedia
Icon