Statische Fallunterscheidungen in C++

von Hubert Schmid vom 2012-09-16

Bei statischen Fallunterscheidungen und C++ denken viele vermutlich an Präprozessor-Direktiven. Diese Mechanismen arbeiten allerdings nur auf dem Quelltext und sind daher sehr inflexibel. Wesentlich interessanter sind Mechanismen, die auf statischen Typinformationen der Übersetzung basieren. An einem Beispiel lässt sich das sehr einfach erklären:

struct foo { bool operator==(const foo& rhs) const; }; struct bar { bool operator<(const bar& rhs) const; };

Das Beispiel enthält zwei einfache Klassen. Die erste Klasse deklariert einen Operator um die Äquivalenz zweier Instanzen zu prüfen. Die zweite Klasse deklariert hingegen einen Operator für eine Ordnungsrelation, den man natürlich auch für die Äquivalenz verwenden kann. Die Frage ist, ob man eine generische Funktion schreiben kann, die den Äquivalenztest abhängig von den deklarierten Operatoren ausführt. In Pseudo-Code heißt das:

template <typename Lhs, typename Rhs> bool equals(Lhs&& lhs, Rhs&& rhs) { if (use_equals<Lhs, Rhs>()) { return lhs == rhs; } else { return !(lhs < rhs) && !(rhs < lhs); } }

In dynamisch typisierten Programmen findet man solche Implementierungen häufiger. Anders in statisch typisierten Programmiersprachen: Dort ist solcher Code in der Regel nicht zulässig, weil beide Zweige der if-Bedingung ausführbar sein müssen – unabhängig von ihrer tatsächlichen Erreichbarkeit.

C++ ist allerdings nicht irgendeine Programmiersprache und bietet viele interessante Möglichkeiten. Die Aufgabe besteht eigentlich aus zwei Teilen: Prüfen ob der Gleichheitsoperator verwendet werden soll und der Steuerung der beiden Zweige. Der zweite Teil ist einfach mit zwei überladenen Funktionen zu lösen, von denen immer nur eine berücksichtigt wird. Das zugehörige Zauberwort lautet SFINAE und seine Verwendung wird seit C++11 durch die Klasse std::enable_if vereinheitlicht. Wenn man das Grundprinzip verstanden hat, dann ist der Code in der Regel auch hinreichend verständlich.

template <typename Lhs, typename Rhs> auto equals(Lhs&& lhs, Rhs&& rhs) -> typename std::enable_if<use_equals<Lhs, Rhs>(), bool>::type { return lhs == rhs; } template <typename Lhs, typename Rhs> auto equals(Lhs&& lhs, Rhs&& rhs) -> typename std::enable_if<!use_equals<Lhs, Rhs>(), bool>::type { return !(lhs < rhs) && !(rhs < lhs); }

Die Funktion use_equals ist dagegen deutlich unangenehmer. Mit C++11 ist die Implementierung zwar ein wenig einfacher geworden und es kristallisieren sich immer stärker Muster heraus. Trotzdem ist es leider immer noch so, dass der Code einfach nicht das zum Ausdruck bringt, was er sehr zuverlässig macht.

template <typename Lhs, typename Rhs> auto use_equals_aux(Lhs&& lhs, Rhs&& rhs) -> decltype(lhs == rhs); void use_equals_aux(...) noexcept; // fallback template <typename Lhs, typename Rhs> constexpr bool use_equals() { return !noexcept(use_equals_aux(std::declval<Lhs>(), std::declval<Rhs>())); }

Man kann sich leicht davon überzeugen, dass damit der Vergleich für Instanzen beider Klassen funktioniert. Interessant ist noch zu sehen was passiert, wenn die generische equals-Funktion mit Instanzen einer Klasse aufgerufen wird, die keinen der beiden Operatoren deklariert:

struct baz { }; int main() { baz a, b; equals(a, b); }

In diesem Fall bricht der Compiler die Übersetzung mit folgender Fehlermeldung ab:

In instantiation of 'typename std::enable_if<(! use_equals<Lhs, Rhs>()), bool>::type equals(Lhs&&, Rhs&&) [with Lhs = baz&; Rhs = baz&; typename std::enable_if<(! use_equals<Lhs, Rhs>()), bool>::type = bool]': error: no match for 'operator<' in 'rhs < lhs' error: no match for 'operator<' in 'lhs < rhs'

Man sieht also: Es funktioniert ganz gut. Es gibt aber auch noch eine Menge Potential für die Unterstützung dieser Techniken bis zur breiten Akzeptanz. Das gilt insbesondere für komplexe Fälle, in denen nicht nur einzelne Funktionen sondern ganze Klassen einschließlich der Member-Variablen betroffen sind. Wir dürfen also gespannt sein, was die Zukunft in dieser Hinsicht bringt, und welche Früchte die aktuell Bemühungen unter dem Stichwort static_if laufenden Bemühungen tragen werden.