How to express optional values in C++
Table of Contents
The problem #
Implement a function that accepts a player ID, an activity ID and returns the player’s score for that activity. The returned score depends on the activity ID and it is not guaranteed that the player has a score for the given activity.
The bad solution #
One possible solution is to use -1 to represent the absence of a score.
int get_score(int player_id, int activity_id)
{
if (has_player_played(player_id, activity_id))
return retrieve_score(player_id, activity_id);
return -1;
}
This approach is one of the oldest conventions in programming. You will see it in C legacy code, in UNIX system calls and other places.
However, in modern C++, using -1 is an anti-pattern. It assumes that negative numbers will never be valid data in your domain or it may lead to accidental arithmetical errors.
The signature of the function does not express the fact that the return value may be invalid, which might be confusing to the reader. All of these can lead to subtle bugs that are hard to track down.
The good solution #
C++17 introduced the std::optional type, which can be used to express the fact (as the name implies) that a value may be optional.
#include <optional>
std::optional<int> get_score(int player_id, int activity_id)
{
if (has_player_played(player_id, activity_id))
return retrieve_score(player_id, activity_id);
return std::nullopt;
}
Notice that the signature of the function now expresses the fact that the return value may be optional.
The reader doesn’t have to search through the function body to understand that the return value may be missing or to understand what the special value -1 means.
Now it is clear that the function may or may not return a value.
The caller can verify if the returned optional value is valid by using the has_value() method.
You can retrieve the value by using value() or value_or(), in which you can specify a default value.
auto score = get_score(player_id, activity_id);
if (score.has_value())
std::cout << score.value() << std::endl;
else
std::cout << "No score found" << std::endl;
C++23 introduced monadic operations on std::optional that allow you to chain operations safely without writing if-else ladders.
These operations are transform, and_then and or_else.
Complete example #
You can try the below example in C++ Shell.
#include <iostream>
#include <optional>
bool has_player_played(int player_id, int activity_id)
{
if (player_id == 1 && (activity_id == 7 || activity_id == 8))
return true;
return false;
}
int retrieve_score(int player_id, int activity_id)
{
if (player_id != 1)
return -100;
if (activity_id == 7)
return 700;
if (activity_id == 8)
return 800;
return -100;
}
// ---------------------------------------------- //
std::optional<int> get_score(int player_id, int activity_id)
{
if (has_player_played(player_id, activity_id))
return retrieve_score(player_id, activity_id);
return std::nullopt;
}
int main()
{
int score = get_score(1, 9)
.or_else([]() { return std::make_optional(0); })
.value_or(6);
std::cout << "Score for player 1 and activity 9: " << score << "\n";
score = get_score(1, 8)
.transform([](int score) { return score * 2; })
.value_or(0);
std::cout << "Score for player 1 and activity 8: " << score << "\n";
score = get_score(1, 7)
.and_then([](int score) { return std::make_optional(score + 10); })
.value_or(0);
std::cout << "Score for player 1 and activity 7: " << score << "\n";
}