Strong Booleans
Consider a legacy codebase in C++ that uses int
instead of bool
.
It’s so legacy that it is probably C89, really, before the
advent of the bool
type in any form. So typical code that has
a “boolean” variable as a class member variable looks like this:
class Example {
public:
Example() : m_toggle(0) { }
void SetIt(int v) { m_toggle = v; }
void ToggleIt() { if (m_toggle) { m_toggle = 0; } else { m_toggle = 1; } }
const char * Status() { if (m_toggle) return "ON"; else return "OFF"; }
private:
int m_toggle;
};
There is an int
in this class, but we use it like a boolean value. Except
for SetIt()
, there is sort-of-an-invariant that the value is either 0 or 1.
Suppose I wake up and want to replace that int
by a bool
, but in such a
way that every use of that variable with an integer value – and not
a boolean constant – will trigger a compile error? In other words, what
type T
do I need so that the following does-it-compile holds?
T t = 0; // error
T t{17}; // error
T t = false; // ok
T t{true}; // ok
Simple Replacement
Just replacing int
with bool
does not help. It is simple though: the rest of the code does not
need to change. But the code still compiles, and so we are left with all the undesirable int
-to-bool
conversions.
Less Simple Replacement
We introduce a struct strong_bool
that just wraps a single bool. We use this
type instead of int
or bool
in the Example
class. Trivially, it looks like this:
struct strong_bool
{
bool b = false;
};
This flags down a whole bunch of uses already! Unfortunately it also flags the
if (m_toggle)
use (which ought to be ok: that’s a boolean in a boolean context),
and uniform initialization with an int
is acceptable.
Advanced Replacement
Let’s give the struct
some stronger behavioral guarantees. We can turn it into
a class (so there is no access to the inner bool b
anymore), and provide
it with:
- a constructor, so that it can be created from a
bool
, - no other constructors, so that it cannot be created from anything but a
bool
, - a boolean-conversion operator, so it can work as a
bool
in some contexts.
class strong_bool
{
bool b = false;
public:
strong_bool() = default;
template<typename T> strong_bool(T v) = delete;
strong_bool(bool v) : b(v) {};
explicit operator bool() const { return b; }
};
Templates to the rescue. It is possible to delete overloads of member functions,
including constructors, so here, all of them are deleted (template<typename T>
) all of them,
and there is one non-template overload
taking bool
. The trick here is that the template matches first, so passing
an int
, or a pointer, or whatever, matches that type first – and that overload is deleted.
Edit 2022-11-29 Meanshile, cppcoach (his site) points out that my phrasing is poor here: “matches first” is not standards language. An overload set is formed, and the best match is chosen – which may be the deleted one.
Example, Repaired
Using the new templated version of strong_bool
, we can convert the example class
to use it. Each use of an int
or integer constant where we really meant a bool
value is flagged as an error by the compiler, so we can use those as a guide and end up
with this:
class Example {
public:
Example() = default;
void SetIt(bool v) { m_toggle = v; }
void ToggleIt() { if (m_toggle) { m_toggle = false; } else { m_toggle = true; } }
const char * Status() { if (m_toggle) return "ON"; else return "OFF"; }
private:
strong_bool m_toggle = false;
};
All’s well, right? Well, almost: that bool
in the SetIt()
method is a not-so-strong
bool
, so integral values can be passed in. They convert to bool
following the standard rules.
A call SetIt(3)
is just fine. In other words,
we have the internal use of the booleans repaired, but not the external API.
Switching the API type so that SetIt()
also takes a strong_bool
might not be
possible, depending on the API stability guarantees given by the codebase.
But if it is possible, then replacing that type in the API by a strong_bool
would do the trick.
What About Performance?
This struct has the same size and alignment properties as a bool
. It has size 1 (at least
when compiled with Clang). With just about any optimizations enabled, the compiler
can turn all reads and writes to the “inner” bool b
into just that: writes to that
value. The example program (e.g. Example 6) is optimized to call puts()
three times,
since the compiler can figure out what value b
has anyway, and there is no need
to keep the strong_bool
or anything else around.
You don’t pay for what you don’t use, but here you get type safety for free!
Takeaway
With a simple type wrapper, you can use the compiler as a tool to hunt down unwanted conversions and dubious use-an-int-as-a-bool use in legacy code. And it’s gratis at runtime, and simple to implement at compile-time.
My codebase only has if-statements and assignements-with-int-constants, so the replacement does not need to be very complicated. Completing the API of
strong_bool
for a more complicated codebase is left as an exercise.