An option(al) to surprise you

In today's post I share a learning of a customer with you. A while back, a customer asked me to join a debugging session. They had an issue they didn't (fully) understand.

The base

What I will show you is a much down-stripped and, of course, altered version. It was about a message system, but that's not important. Have a look at the code below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
enum class State {
  Initial,
  WaitingForInput,
  GotValidInput,
  GotValidInputWithData,
  GotInvalidInput
};

std::optional<const char*> Worker(State s, const char* data)
{
  switch(s) {
    using enum State;
    case Initial: return std::nullopt;
    case WaitingForInput: return {};
    case GotValidInput: [[fallthrough]];
    case GotValidInputWithData: return data;
    case GotInvalidInput: return "<invalid>";
  }

  return std::nullopt;
}

You can see a class enum for a state and a function Worker. The function takes a State and a const char* message named data. The function's job is to create a std::optional containing the user data, a C-style string.

Using the functionality

The next piece is a debugging function called Log:

1
2
3
4
5
void Log(std::optional<const char*> value)
{
  static constexpr char DEFAULT[]{"<empty>"};
  std::println("{}", value.value_or(DEFAULT));
}

Its sole job is to print the user's message or output <empty> if there is no user message. Thanks to std::optionals value_or, the check for a valid message and the decision which string to output is easy. Of course, you can go for the longer with an if and test the has_value of the optional. However, I like this version, and for the issue, the version doesn't make a difference.

It doesn't (always) work

All right, so what they reported is that the program sometimes crashes like this:

1
2
Program returned: 139
Program terminated with signal: SIGSEGV

Clearly a nullptr that is misused. But where? Can you spot it on the code above?

Book an in-house C++ training class

Do you like this content?

I'm available for in-house C++ training classes worldwide, on-site or remote. Here is a sample list of my classes:
  • From C to C++
  • Programming with C++11 to C++17
  • Programming with C++20
All classes can be customized to your team's needs. Training services

Fighting through layers of code and understanding the application a bit better, it took me some time to figure things out.

Notice the two states GotValidInput and GotValidInputWithData. The first state has no input from the user. It sounds reasonable to treat this state as the one with user data since the std::optional handles whether there is data stored or not. Well, ... Let's have a look at a hypothetical code that uses Worker and Log:

1
2
3
auto a = Worker(State::GotValidInput, nullptr);

Log(a);

With no user input the function Worker is called with a nullptr. Still reasonable. Do you see the bug now?

nullptr is a valid value

For our std::optional nullptr is a valid value of the type const char*. Hence, setting the std::optional to nullptr makes the optional contain a value! That value is nullptr. Once you access the value regardless of how in Log a nullptr-check is required!

A fixed version of Log looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void LogFixed(std::optional<const char*> value)
{
  static constexpr char DEFAULT[]{"<empty>"};

  if(auto* stored = value.value_or(nullptr); stored != nullptr) {
    std::println("{}", *stored);
  } else {
    std::println("{}", DEFAULT);
  }
}

In the end, I can say I should have seen the issue earlier, but with thousands of lines of code around, it took me a bit longer than I wanted. Always be cautious with std::optional and pointers. Containing a value doesn't imply that the value is not a nullptr.

Andreas