On boost::tribool and backfiring operator overloads

The tribool library in Boost is a nice small library for writing three-valued logic in C++. It's good for modeling situations where a state of affairs can either be true, false or indeterminate. A bit like std::optional<bool>, but often with nicer syntax and semantics.

boost::tribool is a (rare?) example of a type where overloading the logical && and || operators in C++ makes sense. Logical operators between true and false work in the usual way, but when one of the operands is the special tribool::indeterminate_value, then the result is indeterminate too unless the other operand determines the overall result. So for example false && indeterminate is false but true && indeterminate is indeterminate.

Because writing logical expressions involving tribools feels so natural, it's easy to introduce subtle bugs when assuming that overloaded logical operators work mostly the same way as their built-in counterparts. Can you spot one in the following snippet?

#include <boost/logic/tribool.hpp>

struct Object;

void do_something_with_object(Object& o);

boost::tribool can_we_do_anything();

bool is_object_valid(const Object& o);

void do_something_with_object_if_possible(Object* p)
    if (can_we_do_anything() && p != nullptr && is_object_valid(*p)) {

The answer lies in the short-circuiting behavior of the logical operators. The evaluation of built-in logical operators stops as soon as the result is known. That's why writing p != nullptr && is_object_valid(*p) is safe and idiomatic — the pointer is never dereferenced if the check at the left-hand side of the expression fails.

But remember that boost::tribool overloads logical operators between tribool and bool to yield another tribool. And overloaded logical operators, like any other overloaded operators, are just function calls in disguise. So the expression inside the if statement translates into something like this:

if (operator&&(operator&&(can_we_do_anything(), p != nullptr), is_object_valid(*p))) {

Boom! The short-circuiting is gone and the pointer is always dereferenced. If it's null, it's game over.

Because I did this in one of my projects and spent considerable time troubleshooting what's going on, let this blog post be a note to self: Be careful with overloaded logical operators!