Kein Safe-Bool Idiom?

Das Safe-Bool Idiom wurde früher jedem eingetrichert, der daran dachte eigene Smart-Pointer Strukturen zu bilden.

Doch plötzlich, mit dem Aufkommen von C++11 und seinen Nachfolgern, verschwand dieses Mysterium wieder …


Objekte sind (besonders im System-nahen Bereich) oft nur Wrapper um native Konstrukte (z.B. aus der C Welt).
Man nennt solche Dinge auch gerne “Smart-Handles” als Anlehnung an Smart-Pointer und viele von ihnen haben die Eigenschaft, dass sie auch “leer” oder “ungültig” sein können.

Und deshalb hätte man gerne, dass solche Objekte möglichst einfach “ausge-if-t” werden können, so wie das hier:

 1HandleWrapper handle;
 2...
 3if(handle)
 4{
 5  do_something_with(handle);
 6}
 7else
 8{
 9  throw std::runtime_error("Nothing to do");
10}

Das ist eigentlich alles kein Problem, denn da schreibt man einfach:

 1class HandleWrapper
 2{
 3public:
 4  bool empty() const
 5  {
 6    return my_secret_impl() ? true : false;
 7  }
 8
 9  operator bool() const
10  {
11    return !this->empty();
12  }    
13};

Und schon klappt der if Aufruf.

Doch unglücklicherweise klappt dann noch viel mehr, wie z.B.:

1bool b = handle;     // handle -> bool
2int i = 42 + handle; // handle -> bool -> int(1)
3void foo(long l);
4foo(handle);           // handle -> bool -> long(1)
5if(handle == handle2); // (handle -> bool) == (handle2 -> bool)

Und schon regiert das absolute Chaos.

Safe Bool über exotische Pointer

Weil bool sich in jeden anderen Integer konvertieren lässt, kam der Gedanke auf, dass man einen anderen Typen braucht, der auch gegen true / false auswertbar ist aber nicht automatisch in Rechnungen oder Vergleichen aktiv wird.

Die Lösung fand man in einem an den Typen gebunden Methoden-Pointer, der im true Fall auf eine existierende private Methode zeigt, und im false Fall ein NULL-Pointer wird.

 1class HandleWrapper
 2{
 3private:
 4  void useless_function() const 
 5  {    
 6  }
 7
 8public:
 9  typedef void(HandleWrapper::*safe_bool_type)() const;
10
11  operator safe_bool_type() const
12  {
13    return this->empty() ? 0 : &HandleWrapper::useless_function;
14  }
15};

Unser if(handle) findet dann einen “primitiven” Pointer-Typen, der ausge-if-t werden kann und nutzt ihn “implizit”.

Wenn man das ganze jetzt noch in eine Template-Klasse wirft, die als Basisklasse eines “aus-if-barem” Objektes genutzt wird, landet man bei der Implementierung, die ich im GATE Framework immer noch einsetze um C++98/03 kompatibel zu sein.
Man kann z.B.: den safe-bool cast immer auf operator!() umleiten und diesen implementieren.
Dann hat man sowohl if(x) und if(!x) erledigt:

 1template<class T> class SafeBoolBase
 2{
 3private:
 4  void safe_bool_method() const { }
 5
 6public:
 7  typedef void(SafeBoolBase::*bool_type)() const;
 8
 9  operator bool_type() const
10  {
11    T const& derived_impl = *static_cast<T const*>(this);
12    return !derived_impl ? 0 : &SafeBoolBase<T>::safe_bool_method;
13  }
14};
15
16class MyIfableType : public SafeBoolBase<MyIfableType>
17{
18public:
19  bool operator!() const
20  {
21    ...
22  }
23};

C++11 und explicit operator bool()

In C++11 ist dieser verdrehte Pointer-Kunstgriff nicht mehr notwendig, denn eine Funktion explicit operator bool() const bedient nur noch wirklich explizit casts oder eben if(x), lässt aber Rechnungen oder Vergleiche hart abblitzen.

Vor C++11 war explicit nur dem Konstruktor vorbehalten und konnte nicht auf Operatorfunktionen angewendet werden. Konvertierungen waren damals also immer “implizit”.

Die explizite Operatorfunktion macht sogar den bool operator!() const in einige Szenarien unnötig, denn ein
if(!x) landet ebenfalls im explicit operator bool() const und dreht sein Ergebnis einfach um.

Fazit

Das Safe-Bool Idiom gerät bereits in Vergessenheit, denn junge Programmierer kennen es schlicht weg nicht.

Und obwohl ich die Eleganz des neueren Standards hier sehr schätze, hat das Safe-Bool Idiom trotzdem einen hohen Lehrwert:
Genau weil ich damals vor 15 Jahren über implizite Konvertierungen von operator bool gestolpert bin, wurden mir viele Details des Typensystems bewusst, die einem sonst verborgen bleiben.

Daher meine Empfehlung an alle:

Lernt beide Welten kennen!
Die alte wie die neue.
Nur so kann man Verständnis und eine bessere Zukunft aufbauen.