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 | |
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 | |
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 | |
Clearly a nullptr that is misused. But where? Can you spot it on the code above?
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 | |
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 | |
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
