C++ Core Guidelines: Interfaces von Templates

Modernes C++  –  16 Kommentare

Die Interfaces von Templates sind nach dem Wortlaut der C++ Core Guidelines ein "kritisches Konzept", denn das Interface eines Templates ist ein "ein Vertrag zwischen einem Anwender und einem Implementierer – und sollte sorgfältig designt werden".

Dies sind die vier Regeln für den heutigen Artikel:

T.41: Require only essential properties in a template’s concepts

Was bedeutet es, lediglich die wesentlichen Eigenschaften zu spezifizieren? Die Guidelines beantworten diese Frage mit einem Sort-Algorithmus, der Debug-Unterstützung anbietet:

template<Sortable S>
requires Streamable<S>
void sort(S& s) // sort sequence s
{
if (debug) cerr << "enter sort( " << s << ")\n";
// ...
if (debug) cerr << "exit sort( " << s << ")\n";
}

Jetzt gilt es, noch eine Frage zu beantworten: Welche Probleme können auftreten, wenn unwesentliche Eigenschaften spezifiziert werden? Das bedeutet, dass das Concept zu stark an die Implementierung gebunden ist. Die Auswirkung kann sein, dass eine leichte Veränderung der Implementierung eine Anpassung der Concepts nach sich zieht. Letztlich wird dadurch das Interface sehr unstabil.

T.42: Use template aliases to simplify notation and hide implementation details

Seit C++11 unterstützt C++ Template Aliase. Ein Template Alias ist ein Name, der für eine Familie von Typen steht. Werden diese verwendet, wird der Code lesbarer und er kann oft Type Traits vermeiden. In dem Artikel "C++ Core Guidelines: Definition von Concepts, die Zweite" gehe ich auf Type Traits genauer ein.

Was meinen die Guidelines, wenn sie von besserer Lesbarkeit sprechen? Das erste Beispiel verwendet Type Traits:

template<typename T>
void user(T& c)
{
// ...
typename container_traits<T>::value_type x; // bad, verbose
// ...
}

Im Vergleich setzt das folgende Beispiel Template Aliase ein.

template<typename T>
using Value_type = typename container_traits<T>::value_type;

void user2(T& c)
{
// ...
Value_type<T> x;
// ...
}

Lesbarkeit ist auch das Argument, das für die nächste Regel gilt.

T.43: Prefer using over typedef for defining aliases

Aus der Perspektive der Lesbarkeit betrachtet gibt es zwei Argumente, die für using anstelle von typedef sprechen. Erstens wird using direkt am Anfang des Ausdrucks verwendet. Zweitens fühlt sich using sehr ähnlich an wie auto. Darüber hinaus gilt, das using einfach für Template Aliase verwendet werden kann:

typedef int (*PFI)(int);     // OK, but convoluted

using PFI2 = int (*)(int); // OK, preferred

template<typename T>
typedef int (*PFT)(T); // error (1)

template<typename T>
using PFT2 = int (*)(T); // OK

Die ersten zwei Zeilen definieren einen Zeiger auf eine Funktion (PFI und PFI2), der ein int annimmt und ein int zurückgibt. Im ersten Fall kommt typedef zum Einsatz und im zweiten Fall using. Die letzten zwei Zeilen definieren ein Funktions-Template (PFT2), das einen Typ-Parameter T annimmt und ein int zurückgibt. Die Zeile (1) ist hingegen nicht gültig.

T.44: Use function templates to deduce class template argument types (where feasible)

Der entscheidende Grund dafür, dass wir so viele make_-Funktionen in C++ wie std::make_tuple oder std::make_unique besitzen, ist, dass Funktions-Templates ihre Template-Argumente aus den Funktionsargumenten ableiten können. Während dieses Vorgangs wendet der Compiler nur einfache Konvertierungen an. So entfernt er den äußersten const/volatile-Qualifizierer und vereinfacht C-Arrays und Funktionen zu Zeigern auf das erste Element des C-Arrays oder einen Zeiger auf die Funktion.

Mit dieser automatischen Bestimmung der Template-Argumente wird unser Leben als Programmierer deutlich einfacher.

Anstelle eins Ausdrucks

std::tuple<int, double, std::string> myTuple = {2011, 20.11, "C++11"};

lässt sich einfach die Fabrikfunktion std::make_tuple anwenden.

auto myTuple = std::make_tuple(2011, 20.11, "C++11");

Traurig, aber die automatische Bestimmung der Template-Argumente gibt es in C++ nur für Funktions-Templates. Warum? Konstruktoren von Klassen-Templates sind doch auch nur spezielle, statische Funktionen. Genau! Mit C++17 kann der Compiler die Template-Argumente direkt von seinen Konstruktorargumenten ableiten.

Das folgende Beispiel stellt die Vereinfachung vor, dank der sich myTuple in C++17 definieren lässt:

std::tuple myTuple = {2017, 20.17, "C++17"};

Eine offensichtliche Auswirkung dieses C++17-Features ist es, dass die meisten der make_-Funktionen in C++17 obsolet werden.

Falls du die Details zur automatischen Ableitung der Template-Argumente lesen willst und dich darüber hinaus den Argument Deduction Guide interessierst, lege ich dir den Artikel "Modern C++ Features – Class Template Argument Deduction" von Arne Mertz ans Herz.

C++ schulen

Ich muss zugeben, dass ich dieses C++17-Feature sehr gerne mag. Als C++-Trainer ist es meine Aufgabe, die komplexe Sprache C++ zu vermitteln. Je symmetrische C++ wird, desto leichter ist es für mich, die allgemeinen Konzepte hinter C++ zu schulen. Jetzt kann ich einfach sagen: "Ein Template kann seine Template-Argumente automatisch aus seinen Funktionsargumenten ableiten." In der Vergangenheit musste ich hingegen hinzufügen, dass dies nur für Funkions-Template möglich ist.

Hier ist ein einfaches Beispiel:

// templateArgumentDeduction.cpp

#include <iostream>

template <typename T>
void showMe(const T& t){
std::cout << t << std::endl;
}

template <typename T>
struct ShowMe{
ShowMe(const T& t){
std::cout << t << std::endl;
}
};

int main(){

std::cout << std::endl;

showMe(5.5); // not showMe<double>(5.5);
showMe(5); // not showMe<int>(5);

ShowMe(5.5); // not ShowMe<double>(5.5);
ShowMe(5); // not ShowMe<int>(5);

std::cout << std::endl;

}

Die Anwendung des Funktions-Templates showMe unterscheidet sich nicht von der des Klassen-Templates ShowMe. Der Anwender weiß nicht, dass er ein Template einsetzt.

Mit dem aktuellen GCC 8.2 lässt sich das Programm übersetzen und ausführen:

Um ein wenig genauer zu sein. Die automatische Bestimmung der Template-Argumente steht mit GCC 7, Clang 5 und MSVC 19.14 zu Verfügung. cppreference.com liefert die Details zur Compilerunterstützung.

Wie geht's weiter?

Weißt du, was Regular und SemiRegular Datentypen sind? Falls nicht, dann ist mein nächster Artikel zu den Interfaces von Templates genau der richtige für dich. Die Regel T.47 lautet: "Require template arguments to be at last Regular or SemiRegular."