std::optional? Proceed with caution!
Tags: cpp, programming, learning, development
Published: 2024-01-15 05:16:36

The `std::optional` type is a great addition to the standard C++ library in C++ 17. It allows to end the practice of using some special (sentinel) value, e.g., `-1`, to indicate that an operation didn’t produce a meaningful result. There is one caveat though, optional types - especially `std::optional<bool>` - may in some situations behave counterintuitively and can lead to subtle bugs.

Let’s take a look at the following code:
```C++
bool isMorning = false;
if (isMorning) {
  std::cout << "Good Morning!" << std::endl;
} else {
  std::cout << "Good Afternoon" << std::endl;
}
```
Running this code prints:
```
Good Afternoon!
```
This shouldn't be a surprise. Let’s see what happens if we change the bool type to `std::optional<bool>` like this:

```C++
std::optional<bool> isMorning = false;
if (isMorning) {
  std::cout << "Good Morning!" << std::endl;
} else {
  std::cout << "Good Afternoon!" << std::endl;
}
```

This time the output is:
```
Good Morning!
```

Whoa? Why? What’s going on here?

While this is not intuitive, it’s not a bug. The `std::optional` type defines an explicit conversion to `bool` that returns `true` if the object contains a value and `false` if it doesn’t (exactly as the `has_value()` method). In some contexts – most notably the `if`, `while`, and `for` expressions, logical operators, and the conditional (ternary) operator C++ is allowed to use it to perform an implicit cast (a complete list of contexts can be found in the [_Contextual conversions_](https://en.cppreference.com/w/cpp/language/implicit_conversion) section on cppreference). In our case, it led to a behavior that, at first sight, seemed incorrect. Thinking about this a bit more, the seemingly intuitive behavior should not even be expected. An `std::optional<bool>` variable can have one of three possible values:
 * `true`
 * `false`
 * `std::nullopt` (i.e., not set)

and there is no interpretation under which the behavior of expressions like `if (std::nullopt)` is universally meaningful. Having said that, I have seen multiple engineers (myself included) fall into this trap.

The problem is that spotting the bug can be hard as there are no compiler warnings or any other indications of the issue. This is especially problematic when changing an existing variable from `bool` to `std::optional<bool>` in large codebases because it is easy to miss some usages and introduce regressions.

The problem can also sneak easily into tests. As an example, here is a test that happily passes, but shouldn't:
```C++
TEST(stdOptionalBoolTest, IncorrectTest) {
  ASSERT_TRUE(std::optional<bool>{false});
}
```

## How to deal with `std::optional<bool>`?

Before I discuss the ways to handle the `std::optional<bool>` type in code, I would like to a few strategies that can prevent bugs caused by `std::optional<bool>`:
* raise awareness of the unintuitive behavior of `std::optional<bool>` in some contexts
* when you see someone introduce a new `std::optional<bool>` variable or function, make sure all call sites are reviewed and amended if needed
* have a good unit test coverage that can detect bugs caused by introducing `std::optional<bool>`; if feasible, create a lint rule that flags suspicious usages of `std::optional<bool>`

Now, here are a few strategies to handle the `std::optional<bool>` type:

### Compare the optional value explicitly using the `==` operator

If your scenario allows treating `std::nullopt` as `true` or `false` you can use the `==` operator like this:

```C++
std::optional<bool> isMorning = std::nullopt;
if (isMorning == false) {
  std::cout << "It's not morning anymore..." << std::endl;
} else {
  std::cout << "Good Morning!" << std::endl;
}
```

This works because the `std::nullopt` value is never equal to an initialized variable of the corresponding optional type. A big disadvantage of this approach is that someone will inevitably want to 'simplify' this code by removing the 'unnecessary' `== false` and, as a result, introduce a bug.

## Unwrap the optional value with the `.value()` method

If you know that the value on the given code path is always set, you can unwrap it by calling the `.value()` method like so:

```C++
std::optional<bool> isMorning = false;
if (isMorning.value()) {
  std::cout << "Good Morning!" << std::endl;
} else {
  std::cout << "Good Afternoon!" << std::endl;
}
```
Note, however, that it won’t work if the value is not set. Invoking the `.value()` method if the value is not set will throw the [`std::bad_optional_access`](https://en.cppreference.com/w/cpp/utility/optional/bad_optional_access) exception.

### Dereference the optional value with the `*` operator

This is very similar to the previous option. If you know that the value on the given code path is always set, you can use the `*` operator to dereference it like this:

```C++
std::optional<bool> isMorning = false;
if (*isMorning) {
  std::cout << "Good Morning!" << std::endl;
} else {
  std::cout << "Good Afternoon!" << std::endl;
}
```
One big difference from using the `.value()` method is that the behavior is undefined if you dereference an optional whose value is not set. Personally, I never use this approach.

### Use `.value_or()` to provide the default value for cases when the value is not set

The `std::optional` type offers the `.value_or()` method that allows you to provide the default value that will be returned if the value is not set. Here is an example:

```C++
std::optional<bool> isMorning = std::nullopt;
if (!isMorning.value_or(false)) {
  std::cout << "It's not morning anymore..." << std::endl;
} else {
  std::cout << "Good Morning!" << std::endl;
}
```

If your scenario allows treating `std::nullopt` as `true` or `false` using `.value_or()` could be a good choice.

### Handle `std::nullopt` explicitly

If you decided to use `std::optional`, you did it because you wanted to enable a scenario where the value may not be set. Now you need to handle this case. Here is one way to do this:

```C++
std::optional<bool> isMorning = std::nullopt;    
if (isMorning.has_value()) {
  if (isMorning.value()) {
    std::cout << "Good Morning!" << std::endl;
  } else {
    std::cout << "Good Afternoon!" << std::endl;
  }
} else {
  std::cout << "I am lost in time..." << std::endl;
}
```

### Fixing tests

If your tests use `ASSERT_TRUE` or `ASSERT_FALSE` assertions with `std::optional<bool>` variables, they might suffer from the very same issue as your code. This would make them unreliable because they might pass even though they shouldn't. As an example, the following assertion doesn't fail:

```C++	
ASSERT_TRUE(std::optional{false});
```
It can be fixed by using `ASSERT_EQ` to explicitly compare with the expected value, or by using some of the techniques discussed above. Here are a couple of examples:

```C++
ASSERT_EQ(std::optional{false}, true);
ASSERT_TRUE(std::optional{false}.value());
```

## Other `std::optional` type parameters

We spent a lot of time discussing the `std::optional<bool>` case. How about other types? Do they also exhibit the same behavior? The `std::optional` type is a template, so its behavior is the same for any type parameter. We can see it by running the following code:

```C++
std::optional<int> n = 0;
if (n) {
  std::cout << "n is not 0" << std::endl;
}
```
It will print:
```
n is not 0
```
The problem with `std::optional<bool>` is just more pronounced due to the typical usage of bool. For non-bool types, it is fortunately no longer a common practice to rely on the implicit cast to bool. These days it is much more common to write the condition above explicitly as: `if (n != 0)` which yields the expected result because no implicit conversion will be involved.