Tip of the Week #141: Beware Implicit Conversions to bool

Originally posted as TotW #141 on January 19, 2018

by Samuel Freilich ([email protected])

Two Kinds of Null Pointer Checks

Checking a pointer for null before dereferencing is important to avoid crashes and bugs. This can be done in two ways:

if (foo) {
  DoSomething(*foo);
}
if (foo != nullptr) {
  DoSomething(*foo);
}

Both of these conditionals have the same semantics given that foo is a pointer, but the type-checking on the latter is a little tighter. Many types in C++ can be implicitly converted to bool, and additional caution is required when the pointed-to type can itself be converted to bool.

Consider the following code which could have two very different meanings:

// Bad code
bool* is_migrated = ...;

// Is this checking that `is_migrated` is not null, or was the actual
// intent to verify that `*is_migrated` is true?
if (is_migrated) {
  ...
}

This code is clearer:

// Good code
// Looks like a null-pointer check for a bool*
if (is_migrated != nullptr) {
  ...
}

Both styles are acceptable in Google C++ code. So when the underlying type is not implicitly convertible to bool, follow the style of surrounding code. If the value in question is a “smart pointer” like std::unique_ptr, the semantics and tradeoffs are the same.

Optional Values and Scoped Assignments

What about (e.g. absl::optional) values? They deserve more careful consideration.

For example:

// Bad code
absl::optional<bool> b = MaybeBool();
if (b) { ... }  // What happens when the function returns absl::optional(false)?

This is less clear than the following code:

// Better code
absl::optional<bool> b = MaybeBool();
if (b.has_value()) { ... }

Note that, in fact, the code snippets above are actually equivalent: absl::optional’s conversion to bool only looks at whether the optional contains a value, not at the value itself. A reader may find it counterintuitive that optional(false) is true, but it’s immediately clear that optional(false) has a value. Again, it’s worth extra caution when the underlying type is implicitly convertible to bool.

One pattern for optional return-values is to put a variable declaration in the conditional of the if. This limits the scope of the variable, but involves an implicit conversion to bool:

if (absl::optional<Foo> foo = MaybeFoo()) {
  DoSomething(*foo);
}

However, avoid this pattern if the underlying type is implicitly convertible to bool.

Note: In C++17, if statements can contain an initializer, so the scope of the declaration can be limited while avoiding the implicit conversion:

if (absl::optional<Foo> foo = MaybeFoo(); foo.has_value()) {
  DoSomething(*foo);
}

“Boolean-like” Enums

Let’s say you’ve taken the advice of TotW #94 and decided to use an enum in your function signature instead of a bool for better readability at the call sites. This kind of refactoring might introduce an implicit conversion in the function definition:

// Bad code
void ParseCommandLineFlags(
    const char* usage, int* argc, char*** argv,
    StripFlagsMode strip_flags_mode) {
  if (strip_flags_mode) {  // Wait, which value was true again?
    ...
  }
}

You can gain additional clarity by replacing the implicit conversion with an explicit comparison:

// Good code
void ParseCommandLineFlags(
    const char* usage, int* argc, char*** argv,
    StripFlagsMode strip_flags_mode) {
  if (strip_flags_mode == kPreserveFlags) {
    ...
  }
}

Summary

In summary, be aware that implicit conversions to bool can be unclear, so consider writing more explicit code:

  • Compare pointer types to nullptr (especially if the pointed-at type is implicitly convertible to bool).
  • Test container emptiness with boolean functions like absl::optional<T>::has_value() (especially if the contained type is implcitly convertible to bool).
  • Compare enums to specific values.

Subscribe to the Abseil Blog