We try to implement a boolean class for C++ that has the same sensible semantics as the Java boolean type.
Handling of boolean values is one of the weaker sides of C++. The C++ bool type was not designed to be typesafe. It is treated as just another integral type, with builtin implicit conversions to numeric types. We adopt the conservative view that implicit conversions should never discard data, as the standard conversion int to bool does.
C++ allows monstrous code like this:
vector<int> v1 (5); // OK, vector with 5 elements SmartPtr<Foo> p = new Foo; // implicitly convertible to bool vector<int> v2 (p); // OK, vector with 1 element!
It's so bad that the C++ standard itself avoids providing an operator bool for classes that should be usable in a boolean context.
Instead, it uses conversion to operator void*. The idea is that the type void* is implicitly convertible to bool, but not to int. This fixes some type safety problems, but introduces new ones:
class X { ... operator void* () { ... } }; X x; delete x; // Yikes!
Vandevoorde and Josuttis point out a better technique (apparently overlooked by the C++ committee?) in their excellent book C++ Templates.
class X { private: struct BoolConversion { int dummy; }; public: inline operator int BoolConversion::* () { return ... ? & BoolConversion::dummy : 0; } }; X x; if (x) { ... } // OK delete x; // COMPILE ERROR
The idea is that there are very few things you can do with a pointer to member of a private class other than implicitly convert it to bool.
Vandevoorde and Josuttis use this technique for smart pointers, but we want to use it to create a class Bool, a typesafe replacement for bool. In order for our class to be used in boolean contexts, it must be implicitly convertible to bool. We will use a variant of the BoolConversion trick:
class Bool { private: const bool val_; ... private: struct S_ { int M_; }; protected: typedef int S_::* bool_; inline bool_ true_ () const { return & S_::M_; } inline bool_ false_ () const { return 0; } public: operator bool_ () const { return val_ ? true_ () : false_ (); } };
This prevents dangerous implicit outward conversions. Of course, users that want to use a Bool in a numeric context can do
Bool b; int x = (bool) b;although we think this is poor style and prefer
Bool b; int x = b ? 1 : 0;
Let's start with the obvious. Our Bool class should be implicitly convertible from bool.
class Bool { public: Bool (bool val) : val_ (val) {} };
If we leave it at that, we have:
Unfortunately, this is the inverse of the desired conversions. We are really fighting the C++ type system here. C++ favors standard conversions between builtin types, relegating user-defined conversions to second-class citizen status. We want our class to wrest control of the conversion process.
As usual, our best weapon in this battle with the C++ type system is template wizardry. The key observation is this — a normal function
void foo (bool) { ... }can actually be called with an argument that is subjected to a sequence of three conversions: a standard conversion followed by a user-defined conversion followed by another standard conversion, but function template arguments have to match exactly. If we want our function foo to accept arguments of type bool and only type bool, with no conversions allowed, we can use the advanced techniques developed in Constraints for Function Template Parameters in C++:
template <bool condition, typename type = int> struct Constraint; template <typename type> struct Constraint<true, type> { typedef type Type; }; template <class T> void foo (T x, typename Constraint<boost::is_same<T, bool>::value>::Type = 0) { ... }
We apply the same idea to the converting constructors for Bool.
First, we discard the "obviously correct" Bool::Bool (bool) constructor — it was just an implementation trap.
We choose to allow implicit conversions to Bool only for types bool and (of course) Bool, but allow explicit conversions for types that are, like Bool itself, implicitly convertible to bool, but not convertible to int or void*.
class Bool { ... // ---------------------------------------------------------------- // Allow implicit conversions from bool, and _only_ from bool. // Template constructors are not chained with standard conversions, // so no implicit conversions from int, for example, are allowed. template <typename T> struct ImplicitlyConvertible { enum { value = boost::is_same<T,bool>::value }; }; // ---------------------------------------------------------------- // Allow explicit conversions from any type that, like Bool itself, // is convertible to bool, but not to int or void*. template <typename T> struct ExplicitlyConvertible { enum { value = (! ImplicitlyConvertible<T>::value && (boost::is_convertible<T,bool>::value && ! boost::is_convertible<T,int>::value && ! boost::is_convertible<T,void*>::value)) }; }; public: template <typename T> Bool (T x, typename Constraint<ImplicitlyConvertible<T>::value>::Type = 0) : val_ (x) {} template <typename T> explicit Bool (T x, typename Constraint<ExplicitlyConvertible<T>::value>::Type = 0) : val_ (x) {}
It's not clear whether this relatively arbitrary set of conversion rules is the best in practice. But at least we're in control of the types we accept.
What about class literals? The literal true is convertible to int. We want a Bool literal TRUE convertible to bool, but not int.
We use the Class Literal idiom:
class Bool { ... private: // Dummy constructor used by True and False Bool () : val_ (/* value unused */ false) {} class Literal; public: class True; Bool (const True &) : val_ (true ) {} class False; Bool (const False&) : val_ (false) {} }; class Bool::Literal : protected Bool { protected: Literal () {} private: // Prohibit nonsensical operations void* operator new (size_t); void operator delete (void*); void* operator& (); Literal& operator= (const Literal&); }; class Bool::True : private Literal { public: operator bool_ () const { return true_(); } inline False operator! () const { return FALSE; } }; class Bool::False : private Literal { public: operator Bool::bool_ () const { return false_(); } inline True operator! () const { return TRUE; } }; // Bool literals // The objects themselves are unused, and can be deleted by a good linker. extern Bool::True TRUE; extern Bool::False FALSE;
We have achieved the following tightening up of the C++ type system:
Bool t = TRUE; void f (Bool); void g (int); f (true); // OK f (t); // OK f (TRUE); // OK f (42); // COMPILE ERROR g (1); // OK g (t); // COMPILE ERROR & TRUE; // COMPILE ERROR t == true; // OK t == 1; // COMPILE ERROR new Bool::True;// COMPILE ERROR
The Bool class presented here is definitely experimental. It remains to be seen whether it can become a practical tool.
We need practical experience to find the best tradeoff between type safety and programming convenience. Future plans for the MObS object system involve incorporating a type such as Bool.
Unfortunately, we cannot rely on current C++ compilers (e.g. g++ fails) to compile code using Bool as efficiently as code using bool. In the real world we might adopt a solution such as this:
#ifdef TYPESAFE_BOOL #include "Bool.hpp" #else typedef bool Bool; #define TRUE true #define FALSE false #endif
Since all error checking is done at compile time, there is no reason to use an error-checking version in production mode.
Some of the techniques presented here are too obscure for ordinary users to have to deal with. We are drifting towards a world where we don't write any ordinary functions any more, only template functions. This works better if C++ is a target language, rather than a "real" programming language.
We have not eliminated all the pitfalls involved in type conversion. If the user defines a function overloaded on types int and Bool, we get
void f (Bool); void f (int); f (5); // OK -- f (int) f (5L); // OK -- f (int) f (TRUE); // OK -- f (Bool) f (Bool (true)); // OK -- f (Bool) f (true); // BAD -- f (int)
The author of f can prevent this problem by templatizing f as above, but this is too much to expect. Instead we (the authors of the infrastructure) can be consistent and replace int with a typesafe Int just as we have done with Bool.
void f (Bool); void f (Int); f (5); // OK -- f (Int) f ((short)5); // OK -- f (Int) f (5L); // COMPILE ERROR -- Sorry, no implicit narrowing conversions f (TRUE); // OK -- f (Bool) f (true); // OK -- f (Bool)
If we follow through on this idea, we will eventually be writing C++ programs that don't use any of the fundamental types directly.
It shouldn't be this difficult to create reasonable abstractions. This Bool class combines three different highly non-trivial techniques. How are we supposed to implement something that is actually intrinsically difficult?
A sample program using the Bool class (with test suite included) is available here.