Skip to content

Very explicit operator bool Is the Safe Bool Idiom still useful in C++11 / C++14?

From time to time I scroll through Qt-related forums and mailing lists, and whenever possible I try to help fellow developers out. The other day a StackOverflow thread caught my attention: a developer was asking “What is the purpose of operator RestrictedBool in QScopedPointer?”

Indeed, looking at QScopedPointer‘s implementation, one notices the strange RestrictedBool usage. Removing the other code, the bulk is:

template <typename T, typename Cleanup = QScopedPointerDeleter<T> >
class QScopedPointer
{
    typedef T *QScopedPointer:: *RestrictedBool;
public:
    inline operator RestrictedBool() const
    {
        return isNull() ? Q_NULLPTR : &QScopedPointer::d;
    }

    inline bool isNull() const
    {
        return !d;
    }
protected:
    T *d;
};

What is the reason for all of this machinery? The answer is quite simple: to make QScopedPointer usable in a boolean context. Thanks to the above we can write in our code:

QScopedPointer<int> sp;
if (sp) { ... }

Is that complicated implementation necessary to realize this? Unfortunately, it is, and the reason is rather cryptic and historical, so let’s start our journey…

Conversion operators

In C++98, it was already possible to write custom conversion operators for our classes, to allow their objects to be converted to other types. Since we’re interested in conversions to bool, given a type T, we can write an operator bool to convert it to a boolean:

struct T {
    operator bool() const { return true; }
};

This allows the usage of objects of type T whenever a boolean is expected:

T t;
if (t) { ... }
do { ... } while (t);
bool ok = t;

This is great, right? Unfortunately, not so much. One of the most prominent mis-features of C++ is the ability to happily convert and promote primitive types. In particular, bool has an implicit promotion towards int, causing any boolean to be promoted to 1 or 0 if true or false, respectively.

This unfortunately applies to our type as well. It is therefore legal to write code like this, although it does not make any sense:

T t;
int i = t;
t >> 4;
if (t < 42) { ... }
AnotherClassWithOperatorBool t2;
if (t == t2) { ... }

The Safe Bool Idiom

Since this is detrimental to type safety, people have come up with an ingenious solution which goes under the name of the Safe Bool Idiom, exploiting the fact that integer promotions and pointer arithmetic are disabled for pointers to data members and pointers to member functions, but they’re still testable in a boolean context.

Our type T can therefore be rewritten as:

struct T {
private:
    typedef void (T::*bool_type)() const;
    // or, in C++11:
    // using bool_type = void (T::*)() const;
    void some_unused_function() const;
public:
    operator bool_type() const {
     // return &some_unused_function if true,
     // return nullptr if false
    }
};

And now T instances are still usable in a boolean context, but without the dangerous promotions and conversions. This is the same trick used by QScopedPointer and by many other classes in Qt, such as the other Qt smart pointers (QScopedPointer actually uses a pointer to a data member instead of a pointer to a member function).

Enter Modern C++

In C++11 the operator conversion functions gained an interesting feature: they could be marked explicit, just like we could mark constructors in C++98. The idea was indeed filling the semantic gap of implicit conversions between two types: while one could disable an implicit conversion by means of an explicit constructor, one couldn’t do the same for an operator conversion function, which was always implicit.

This means, for instance, in C++98 we could do this:

struct A {};

struct B {
    explicit B(const A &);
};

void f(const B &);

// it is _not_ possible to implicitly convert a A to B
// because of the explicit constructor. This will fail:
A a;
f(a); // error: implicit conversion requested

However, we could not disable this implicit conversion:

struct D {};
struct C {
    operator D() const;
};

void f(const D &);

// since the conversion operator from C to D is implicit,
// it is legal to write:
C c;
f(c); // works: operator D() is implicit

This asymmetry was closed in C++11 with the introduction of explicit conversion operators, which forbid implicit conversions but still allow explicit ones:

struct D {};
struct C {
    explicit operator D() const;
};

void f(const D &);

C c;
f(c); // error; requires explicit conversion
f(static_cast<D>(c)); // works

Explicit operator bool

A direct application of an explicit conversion operator is defining an explicit operator bool for our class, with the hope that it would disable the unwanted integral promotions. For instance, we could write something like:

struct T {
    explicit operator bool() const { return true; }
};

This, indeed, makes our type system much, much better:

T t;

bool b1 = t; // fails to compile, implicit conversion

bool b2(t); // works, explicit conversion
bool b3{t}; // works
bool b4 = (bool)t; // works
bool b5 = static_cast<bool>(t); // works

int i = t; // fails to compile
int i(t); // fails to compile
t + 42; // fails to compile

How about a statement like if (t)? Does that trigger implicit or explicit conversion?

Well, neither. According to the Standard it actually triggers a contextual conversion to bool, which in practice means an explicit conversion (the exact wording is in §4.0.4 [conv]).

There are many places scattered around the Standard where such a “contextual conversion to bool” will happen:

  • in the guards of the if, while, do/while, for statements;
  • in the ternary operator condition;
  • for the arguments of the logical and (&&), or (||) and not (!) operators;
  • for the first parameter of static_asserts;
  • for the argument of the noexcept operator;
  • … and some other places around the STL, mostly involving algorithms predicates.

Back to hacking Qt

How does this apply to our original issue with QScopedPointer?

Since Qt 5.7 requires C++11, I have decided to have a try at getting rid of the Safe Bool Idiom and replacing it with an explicit operator bool. After all, at KDAB we love hacking, and Qt is our favourite codebase :-).

From that, I learned an important lesson: there are at least two things that you could do with the Safe Bool Idiom that you can’t do out of the box with explicit operator bool.

Comparison against literal 0

Consider this code:

T t;
if (t == 0) { ... }
if (0 == t) { ... }

This compiles and works with the Safe Bool Idiom, because 0 gets promoted to the null pointer, and the comparison happens between pointers. But this same code fails to compile with an explicit operator bool, because of the disabled promotion from boolean to integral.

The workaround I have found (which, at least, can be applied and makes total sense if T is a smart pointer class) is that you can add comparison operators against nullptr_t, triggering therefore a conversion from 0 to nullptr:

bool operator==(const T &lhs, std::nullptr_t) { return lhs.isNull(); }
bool operator==(std::nullptr_t, const T &rhs) { return rhs.isNull(); }

In case T is not a smart pointer class of sorts, I’m not 100% sure of a viable strategy, but I could argue that code that did t == 0 was very questionable in the first place…

This first workaround got implemented here in Qt.

Functions returning bool

Again, consider this code:

struct S {
    T t;
    bool isValid() const { return t; }
};

This works if T implements the Safe Bool Idiom, but fails to compile with explicit operator bool. The sad truth is that return statements use implicit conversion, not contextual conversion to bool (§ 4.0.2.4 [conv]). I’m quite surprised that this issue happily slipped through the Standard, but this is the way it is.

I do not see how to overcome this in an API compatible way, so this blocked my efforts of getting rid of the Safe Bool Idiom in Qt. In the meanwhile, the patch for QScopedPointer, QSharedPointer and QWeakPointer (which works, modulo these API breaks) is available here.

Conclusions

What have we learned? Well, we now know how the C++ type system gets better and better despite its roots in the C world, and we know why many classes in libraries employ this strange pattern for providing conversions to bool. We have also learned that explicit operators are not a replacement for the Safe Bool Idiom. And while doing that, we have improved the code of our favourite UI toolkit. Most importantly, we’ve had lots of fun!

FacebookTwitterLinkedInEmail

10 thoughts on “Very explicit operator bool”

  1. What is the problem with just using

    struct S {
    T t;
    bool isValid() const { return t?true:false; }
    };

    except for looking redundant?

    1. Giuseppe D'Angelo

      Hi,

      the problem is that it requires modifications to existing code, that is, it’s a source compatibility break, and that’s unacceptable.

      Otherwise, yes, you could write something like that (or just return static_cast<bool>(t);).

  2. Very good post, thanks.
    Perhaps provide a link to this page on the SO question that you reference, it might be useful for other people not following the blog.

  3. Perhaps for isValid() use the following:

    bool isValid() const { return t != nullptr; }

    Great article! I’m glad my question triggered a writeup.

      1. Giuseppe D'Angelo

        That’s the idea, but no promise yet. We would need to carefully evaluate whether the benefits outweight the source compatibility breaks (which should really be avoided at all costs).

        1. There’s also a conceptual angle here:

          If a smart pointer, let’s call it SP, models a built-in pointer, then since return ptr; works for a T *ptr it should work for a SP<T> ptr, too.

          So, at least for classes that model a pointer, I do not think explicit operator bool() can or should replace the Safe Bool Idiom. Not before the return problem is fixed.

  4. Can’t you do

    template <typename T>
    operator T(){
        static_assert(false,"conversion to unsupported type not allowed");
    }
    

    ?
    That way, an implicit conversion to an integral type will be attempted, and will fail, therefore not allowing a+2?

    1. Giuseppe D'Angelo

      Hi, took the liberty of fixing some formatting in the above comment, hope I didn’t introduce a mistake.

      A “robust” implementation is much more trickier than what you propose. static_assert isn’t SFINAE friendly. A simple check like

      is_convertible_v<MyType, int>
      

      would fail to compile rather than returning false.

      You could turn it into a constraint instead. But consider some datatype which is convertible to bool and maybe to something else; now your constraint becomes more and more complex (need to accept T1, T2, T3, reject the others). A template conversion operator is also a complicated beast for overload resolution (blog post coming up).

      Long story short, it’s good to have syntax for explicit conversion (although of course I would’ve strongly preferred that booleans weren’t arithmetic types to begin with).

Leave a Reply

Your email address will not be published. Required fields are marked *