Quantcast
Channel: Tim Martin's Blog » admin
Viewing all articles
Browse latest Browse all 10

The safe bool idiom in C++

$
0
0

I’m in the process of writing a series of articles on C++11, and a passing explanation of this idiom grew to become a whole article. There’s no original content here, but I learned something in writing it and I hope it might be useful to someone out there.

It’s idiomatic C++ to evaluate a variable in boolean context, even when it’s not a boolean variable, to detect whether that variable is “not a thing”. For example, with a pointer variable it checks whether the pointer is null or not:

if (my_pointer) {
   // my_pointer is not null
}

Exactly what this boolean conversion means varies by type, but experience shows that one’s intuitive sense of how this should work is pretty consistent and this can make code more expressive. Some would say this is bad practice, but for now I’ll assume that it’s desirable.

If you’re writing a user-defined class, you want to follow this kind of pattern if possible. If 0 is false, then surely the (0, 0, 0) point in a 3D space is false? Of course, C++ lets you make your user-defined type act pretty much however you want it to, so of course you can define an implicit conversion to bool:

struct TestResult {
   //...
 
   operator bool() const {
      return m_passed;
   }
};

The problem with this is that it’s not just a conversion to bool. It’s a conversion to a type that is part of a system of numeric types, and the types allow conversions between them that allow you do do silly things. For example, you can assign it to an integer:

TestResult test_result;
int i = test_result; // Should really be a compile error

More insidiously, things like the left-shift operator suddenly work on your objects in ways that you might not expect:

test_result << i;

As is the way with all C++ design issues, you can get around this by careful application of edge cases of language tools that were meant for something else. You can expose a conversion to any type you like. Void pointers are a reasonable thing to try, because there’s not a lot you can do with a void pointer:

struct TestResult {
   operator void *() {
      return m_passed ? this : 0;
   }
};

There’s no implicit conversion of void pointers in C++, so this seems reasonably safe. However, one thing you can do with a void pointer is call delete, and there’s no way to prevent that from compiling:

TestResult tr;
delete tr; // Gets converted to pointer, attempts to delete a stack variable!

You could try returning a pointer other than this, which could eat least mean that the code would crash horribly the moment delete was called:

struct TestResult {
   operator void *() {
      return m_passed ? (void *)1 : (void *)0;
   }
};

But this is horrible. Besides, there’s no guarantee that you won’t segfault just by referencing pointers to non-allocated portions of memory (i.e. segfault even in the good case where you’re just converting the result straight to boolean).

A common trick in C++ (which dates from an earlier related trick in C) is to give code an incomplete type to work with. If you have a class declaration in scope, but no definition, then you can handle pointers to the incomplete class but there are limits to what you can do. In particular, you can’t dereference or delete the pointer. So you can safely pass such a pointer back to the caller and allow them to compare it with NULL without worrying that they can delete it.

How can you get a pointer to a type that you can be sure won’t be defined? You define one for yourself. A nested class will do nicely:

struct TestResult {
  //...
 
  class never_defined;
 
  operator const never_defined*() {
    return m_passed ? reinterpret_cast<const never_defined *>(this) : 0;
  }
};

Unfortunately, while pointers to incomplete types can’t be dereferenced, you can still compare them:

TestResult x, y;
 
// later ..
 
if (x > y) // Really shouldn't compile
{
 // ...
}

Can we prevent this? Just one more dip into the junk draw of C++ will sort us out. One of the more overlooked features of C++: the pointer-to-member (in this case, a pointer to member function).

A pointer to member isn’t like a regular pointer. A regular pointer points to data of a particular type in the memory space. A pointer to member operates in the world of classes, not objects. The pointer to member identifies a member of a particular type on a particular class; if you have a int Foo::*, it can point to any integer member of the Foo class. When you set the value of your pointer-to-member, it points to that same member on every instance of Foo (or, equivalently, on no particular Foo instance at all).

Pointers to member can’t be compared to each other, so we can combine our conversion-to-pointer trick with a pointer-to-member and have (at last) a safe boolean conversion:

struct TestResult {
  //...
 
  typedef void (TestResult::*bool_type)() const;
  void do_nothing() const;
 
  operator bool_type() const {
    return m_passed ? &TestResult::do_nothing : 0;
  }
};

If you’re not used to unpacking these definitions, the line:

typedef void (TestResult::*bool_type)() const;

deserves some explanation. This is just like any other typedef, except that since it’s a function typedef the name of the type defined (bool_type) goes in the middle and not on the right hand side. The TestResult::* bit identifies that we’re defining a type that points to a member of TestResult, rather than a regular pointer. The remaining stuff just tells us that we’re talking about a const function that takes no arguments and returns void.

For an example of this being done in the wild, you can see the safe_bool class within Boost::Spirit.


Viewing all articles
Browse latest Browse all 10

Latest Images

Trending Articles





Latest Images