C++11: Type-Transformationen und Alias-Templates

von Hubert Schmid vom 2012-05-20

Die sogenannten Type-Transformation-Traits werden üblicherweise bei der Template-Programmierung verwendet, um zur Übersetzungszeit Typen aus Template-Parametern abzuleiten. Mehr als 20 dieser Transformation-Traits haben ihren Weg über die Boost-Bibliothek und den ersten Technical Report in die Standard-Bibliothek von C++11 gefunden. Die folgende Liste enthält ein paar typische Vertreter:

Ist Type ein Template-Type-Parameter, so steht typename trait<Type>::type für einen Typ mit bestimmten durch das Trait definierten Eigenschaften. Das Schlüsselwort typename ist in generischem Code fast immer notwendig, um type eindeutig auch als solchen zu kennzeichnen. Das folgende Beispiel stammt aus einem meiner Artikel über Enums in C++11 und zeigt die Verwendung eines solchen Traits:

template <typename EnumType> void write(std::ostream& os, EnumType value) { os << static_cast<typename std::underlying_type<EnumType>::type>(value); }

Diese Traits sind praktisch immer nach dem gleichen Schema aufgebaut. Innerhalb eines Klassen-Templates wird mit typedef ein Alias für ein Typ definiert. Bei dem folgenden, sehr einfachen Trait handelt es sich beispielsweise um die Identitätstransformation:

template <typename Type> struct identity { typedef Type type; };

Diese Form hat in den letzten Jahren durch die Boost-Bibliothek und TR1 so starke Verbreitung gefunden, dass sie kaum noch hinterfragt wird. Ich finde das ein wenig bedauerlich, denn mit Hilfe der Alias-Templates aus C++11 ließen sich die Transformation-Traits wesentlich einfacher verwenden. Die Identitätstransformation ließe sich damit beispielsweise einfach so schreiben:

template <typename Type> using identity = Type;

Bei der Verwendung entfiele dadurch sowohl das Präfix typename als auch das Suffix ::type. Das folgende Code-Fragment zeigt einerseits die einfache Definition der Alias-Templates, und andererseits wie sich die Lesbarkeit durch ihre Verwendung deutlich verbessert.

template <typename T> using underlying_type = typename std::underlying_type<T>::type; template <typename EnumType> void write(std::ostream& os, EnumType value) { os << static_cast<underlying_type<EnumType>>(value); }

Leider erst relativ spät wurden die Alias-Templates gesetzt und finalisiert. Das ist vermutlich der entscheidende Grund, warum sie von der Standard-Bibliothek kaum benutzt werden. Dabei hätte man in C++11 alle Transformation-Traits auf diese Weise definieren können – mit einer einzigen Ausnahme: std::enable_if benötigt tatsächlich die lange Form, wobei dessen Einordnung in die Gruppe der Transformation-Traits fragwürdig ist.

Die Alias-Templates haben auch über die Lesbarkeit hinaus Vorteile gegenüber den verschachtelten Typen. So verhindern sie beispielsweise nicht grundsätzlich die Typinferenz der Template-Parameter. Im folgenden Beispiel wird der Template-Parameter Type automatisch bestimmt. Mit der ersten Form der Identitätstransformation und dem Parametertyp typename identity<Type>::type übersetzt der Code hingegen nicht.

template <typename Type> void foo(identity<Type> value) { ... } void bar() { foo(42); }

Es gibt noch einige weitere interessante Anwendungsfälle. Beispielsweise war die Verwendung von Template-Templates bisher relativ umständlich, weil viele Klassen-Templates zusätzliche, optionale Template-Parameter besitzen. So lässt sich die im folgenden Ausschnitt deklarierte Funktion nicht mit filled<std::vector>(6, 7) aufrufen, da std::vector mehr als einen Typparameter besitzt – auch wenn es in der Regel nur mit einem verwendet wird.

template <template <typename> class Container, typename Type> auto filled(std::size_t size, Type value) -> Container<Type>;

Abhilfe schaffen die Alias-Templates. Damit lässt sich ein Alias für std::vector mit einem Typparameter definieren, der mit obiger Funktion verwendet werden kann.

template <typename Type> using simple_vector = std::vector<Type>; auto v = filled<simple_vector>(6, 7);

Übrigens wäre das auch mit Variadic-Templates möglich gewesen. Der Rumpf der Funktion filled muss dafür nicht geändert werden. Die drei zusätzlichen Punkte in der ersten Zeile der Deklaration genügen.

template <template <typename...> class Container, typename Type> auto filled(std::size_t size, Type value) -> Container<Type>; auto v = filled<std::vector>(6, 7);