Optional Values, std::optional and Monadic Operations

A guide to handling optional data in C++ using the std::optional container
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

3D Concept Art
Ryan McCombe
Ryan McCombe
Posted

When creating classes, a common situation we’ll run into is the concept of an optional field.

For example, if we’re creating a Character class, our character may, or may not, belong to a guild:

#include <iostream>

class Character {
public:
  std::string Name;
  std::string Guild;
};

int main(){
  Character Bob{"Bob", "The Fellowship"};
  std::cout << "Bob's Guild: "
    << Bob.Guild;

  Character Anna{"Anna"};
  std::cout << "\nAnna's Guild: "
    << Anna.Guild;
}
Bob's Guild: The Fellowship
Anna's Guild:

In this example, our optional field is a simple string, but potentially any data type can be optional.

We’d like a generic, reusable way of handling such situations, and also making it explicit when a piece of data is optional.

The standard library provides a container that can help us here

Null and Nullable

In programming, the phrase “null” is often used to represent the absence of a value.

For example, a nullable field is a property on a class or struct that may have no value

A nullable boolean has three possible values: true, false, or null

std::optional

The std::optional container is available by including the optional header and can be created like any other object. We pass a template parameter to denote what type of data it will contain:

#include <optional>

std::optional<std::string> Name;

When declaring our std::optional container, we can provide an initial value:

#include <optional>

using namespace std::string_literals;
std::optional<std::string> Name {"Bob"s};

When providing an initial value, we can remove the template parameter. This asks the compiler to infer the correct type, using class template argument deduction (CTAD):

#include <optional>

using namespace std::string_literals;
std::optional Name {"Bob"s};

has_value() and value()

We can check if a std::optional container contains a value using the has_value() method, and we can access its value using the value() method:

#include <iostream>
#include <optional>

class Character {
public:
  std::string Name;
  std::optional<std::string> Guild;
};

int main(){
  Character Bob{"Bob", "The Fellowship"};
  if (Bob.Guild.has_value()) {
    std::cout << "Bob's Guild: "
      << Bob.Guild.value();
  }
}
Bob's Guild: The Fellowship

As a syntactic shortcut, we can cast a std::optional to a boolean value, which will have the same return value as the has_value() method. This removes the need to call has_value() in most situations:

#include <iostream>
#include <optional>

class Character {
public:
  std::string Name;
  std::optional<std::string> Guild;
};

int main(){
  Character Anna{"Anna"};
  if (!Anna.Guild) {
    std::cout << "Anna has no guild";
  }
}
Anna has no guild

Special attention should be paid here when dealing with optional booleans, as this behavior can be a source of bugs. A std::optional will return the boolean value of true if it contains a value, even if that value is falsy. To access the underlying boolean value, we need to remember to use the value() method:

#include <iostream>
#include <optional>

int main(){
  std::optional MyBool{false};
  if (MyBool) {
    std::cout << "MyBool has a value...";
  }

  if (!MyBool.value()) {
    std::cout << "but its value is falsy";
  }
}
MyBool has a value...but its value is false

value_or()

The value_or() method will return the value contained in the optional. But, if the optional doesn’t have a value, it will return what we passed as an argument to value_or():

#include <iostream>
#include <optional>

class Character {
public:
  std::string Name;
  std::optional<std::string> Guild;
};

int main(){
  Character Anna{"Anna"};
  std::cout << "Anna's Guild: "
    << Anna.Guild.value_or("[None]");
}
Anna's Guild: [None]

Updating Optional Values using = or emplace()

In some ways, we can treat the std::optional wrapper as invisible, and write code in the same way we would if it were a regular variable of the underlying type.

The assignment (=) operator is an example of this - we can update the value contained in a std::optional container using the = operator:

#include <iostream>
#include <optional>

int main(){
  using namespace std::string_literals;

  std::optional Name{"Bob"s};
  std::cout << "Name: " << Name.value();

  Name = "Anna"s;
  std::cout << "\nName: " << Name.value();
}
Name: Bob
Name: Anna

For non-trivial types, constructing them outside of the container, and then moving them in using the = operator has a performance cost. Instead, we can construct them in place, using the emplace() function in the same way we’ve seen with other containers.

Any arguments we pass to this function are forwarded to the constructor for the underlying type our std::optional is using:

#include <iostream>
#include <optional>

class Character {
public:
  Character(std::string Name, std::string Guild)
    : Name{Name}, Guild{Guild}{}

  std::string Name;
  std::optional<std::string> Guild;
};

int main(){
  using namespace std::string_literals;

  std::optional<Character> Player;
  Player.emplace("Bob"s, "The Fellowship"s);

  std::cout << "Name: " << Player.value().Name;
}
Name: Bob

Destroying Optional Values using reset()

We can remove the value within a std::optional using the reset() method:

#include <iostream>
#include <optional>

int main(){
  using namespace std::string_literals;

  std::optional Name{"Bob"s};
  std::cout << "Name: " << Name.value();

  Name.reset();
  std::cout << "\nName: "
    << Name.value_or("[None]");
}
Name: Bob
Name: [None]

Comparing Optional Values

Comparison operators are another scenario where the wrapping std::optional can become invisible. We can compare values with values contained in std::optional containers using comparison operators like == and < in the usual way.

This generally means we can use == and != without any additional syntax:

#include <iostream>
#include <optional>

int main(){
  std::optional Optional{5};
  if (Optional == 5) {
    std::cout << "That's a five";
  }
}
That's a five

An “empty” std::optional will always be not equal to any other object of the underlying type.

#include <iostream>
#include <optional>

enum class Faction {
  Human,
  Elf,
};

class Character {
public:
  std::optional<Faction> Faction;
};

int main(){
  Character Player;

  if (Player.Faction != Faction::Human) {
    std::cout << "Not Human";
  }
}
Not Human

Less usefully, an empty std::optional will always be less than other values.

But in general, this type of comparison operation involving an empty std::optional container is very uncommon, and often a bug.

#include <iostream>
#include <optional>

class Character {
public:
  std::optional<int> Health;
};

int main(){
  Character Player;

  if (Player.Health <= 0) {
    std::cout << "Not Alive";
  }
}
Not Alive

std::nullopt

When we want to return an empty std::optional, the std::nullopt token is the typical approach we use:

#include <iostream>
#include <optional>

std::optional<int> GetInt(){
  return std::nullopt;
}

int main(){
  if (!GetInt().has_value()) {
    std::cout << "That's a null";
  }
}
That's a null

A more complex example is below:

#include <iostream>
#include <optional>

class Character {
public:
  bool hasGuild{false};
  std::string GuildName;
};

std::optional<std::string> GetGuildName(
  const Character& Character){
  if (Character.hasGuild) {
    return Character.GuildName;
  }
  return std::nullopt;
}

int main(){
  Character Player;

  std::cout << "Guild: "
    << GetGuildName(Player).value_or("[None]");
}
Guild: [None]

Monadic Operations (C++23)

As of C++23, the std::optional container supports three monadic operations: or_else(), and_then() and transform().

These methods all accept another function as an argument. This may not be something we’ve seen before, but we will cover it in more detail later in the course.

Additionally, all of the methods return a std::optional. This allows them to be chained together, as we’ll demonstrate later.

The behavior of these methods is easier to explain by example, so if these descriptions don’t make sense, scroll ahead to the following code snippet that shows them all being used.

  • **or_else()** - if the std::optional contains a value, return the std::optional. Otherwise, invoke the provided function, and return what it returns. This should also be a std::optional.
  • **and_then()** - if the std::optional is empty, return it. If the std::optional contains a value, invoke the provided function, passing the value as an argument. Then return the std::optional
  • **transform()** - if the std::optional is empty, return it. If the std::optional contains a value, invoke the provided function. The function will receive the current value as an argument and will return a new value. The transform() function will then return a std::optional containing that new value.

These methods were added to support what is known as a functional programming style, which looks a bit different from what we may be used to. We cover topics related to this in a little more detail in our dedicated chapter on functions, which is coming up later.

For now, an example that uses all these methods, and the programming style they are designed to support, is highlighted below:

#include <iostream>
#include <optional>

using OptInt = std::optional<int>;

// For use with or_else()
OptInt GetDefaultValue(){ return OptInt{1}; }

// For use with transform()
int Increment(int x){ return ++x; }
int Double(int x){ return x * 2; }

// For use with and_then()
OptInt Log(OptInt x){
  std::cout << "Value: " << x.value() << '\n';
  return x;
}

int main(){
  OptInt x;

  x.or_else(GetDefaultValue)
   .transform(Increment)
   .and_then(Log)
   .transform(Double)
   .and_then(Log);
}
Value: 2
Value: 4

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Dynamic Data Types using std::variant and Unions

A guide to storing one of several different data types within the same container, using unions and variants
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved