On boost::tribool and backfiring operator overloads
- by Jaakko Moisio
- April 5, 2020
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)) {
do_something_with_object(*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))) {
do_something_with_object(*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!