std::optional
, boost::optional
, and std::expected
are types that allow to represent a contained value of a
certain type, and additional data that denote the absence of a value (nullopt
for optional
, and error values for
expected
). To check if the wrapper contains a value, it is possible to call its member function has_value()
. Alternatively,
it can also be converted to bool
, which is more concise, especially when used in a place that allows contextual conversion to
bool (for instance, the condition of an if
). It means that the following syntax is valid:
std::optional<bool> flag;
if(flag) { ... }
// Equivalent to
if(flag.has_value()) { ... }
When the contained type is also convertible to bool
, using this concise syntax can be confusing. What is tested: The wrapper, or the
contained value?
This rule raises an issue when std::optional
, boost::optional
, or std::expected
wrap a basic type, and the
conversion to bool
is used to test the presence of the value.
What is the potential impact?
There are two possibilities, depending on the initial intent of the autor of the code:
- If the intent was actually to test the presence of the value, the code is correct, but it is not clear. When reading it, people might think
that the contained value is tested, and not the presence of the value.
- If the intent was to test the contained value, the code is incorrect. This situation can especially happen when evolving code that worked
directly with values to work with optional or expected values, and forgetting to update the test. This will lead to code that does not behave in the
intended way, but still works in a plausible way, and the lack of clear clue on what is tested makes finding the issue difficult.
Exceptions
If, in a single expression, the presence of the value is tested and the value is accessed, the risk of confusion is reduced, and no violation is
raised.
std::optional<bool> flag;
if(flag && *flag) { ... } // Compliant