Alias Templates und Template Parameter
Mit Alias Templates lässt sich einer Familie von Typen ein eingängiger Name geben. Template-Parameter können Typen, Nicht-Typen und Templates selbst sein.
- Rainer Grimm
Heute schreibe ich über zwei Themen: Alias Templates und Template-Parameter. Alias-Templates sind eine Möglichkeit, einer Familie von Typen einen eingängigen Namen zu geben. Template-Parameter können Typen, Nicht-Typen und Templates selbst sein.
Mit den Alias Templates möchte ich beginnen.
Alias-Templates
Seit C++11 unterstützt die Programmiersprache Alias Templates. Sie bieten eine Möglichkeit, einer Familie von Typen einen lesbaren Namen zu geben. Der folgende Codeschnipsel zeigt die Idee für das Klassen-Template Matrix
.
template <typename T, int Line, int Col>
class Matrix{
....
};
Matrix besitzt drei Template-Parameter. Den Typ-Parameter T
und die Nicht-Typ-Parameter Line
und Col
(Details zu den Template-Parametern folgen im nächsten Abschnitt).
Aus Gründen der Lesbarkeit möchte ich zwei spezielle Matrizen haben: Square
und Vector
. Die Anzahl der Zeilen und Spalten der Matrix Square
sollten gleich sein. Die Zeilenzahl von Vector
sollte eins sein. Dank Typ-Aliasen kann ich meine Ideen direkt im Code ausdrücken.
template <typename T, int Line>
using Square = Matrix<T, Line, Line>; // (1)
template <typename T, int Line>
using Vector = Matrix<T, Line, 1>; // (2)
Das Schlüsselwort using
((1) und (2)) deklariert einen Typ-Alias. Während das primäre Template Matrix
in den drei Dimensionen T, Line
und Col
parametrisiert werden kann, reduzieren die Typ-Aliase Square
und Vector
die Parametrisierung auf die zwei Dimensionen T
und Line
. Aus dieser Sicht ermöglichen es Alias Templates, intuitive Namen für teilweise gebundene Templates zu erstellen. Die Verwendung von Square
und Vector
ist intuitiv.
Matrix<int, 5, 3> ma;
Square<double, 4> sq;
Vector<char, 5> vec;
Ein anschaulicher Anwendungsfall von Alias Templates ist die type-traits Bibliothek.
Type-Traits Bibliothek
Wenn du std::move(arg)
auf einen Wert arg
anwendest, greift der Compiler typischerweise auf std::remove_reference
zurück, um eine Referenz vom zugrunde liegenden Datentyp zu entfernen:
static_cast<std::remove_reference<decltype(arg)>::type&&>(arg); // (1)
static_cast<std::remove_reference_t<decltype(arg)>&&>(arg); // (2)
Dank der Alias Templates ist die Version (Zeile 2) seit C++14 gültig. Der folgende Hilfs-Datentyp ist verfügbar:
template< class T >
using remove_reference_t = typename remove_reference<T>::type;
Natürlich bietet die type-traits Bibliothek die entsprechenden Hilfs-Datentypen für weitere Funktionen, die ein Datentyp anbietet.
Das zuvor definierte Klassen-Template Matrix
verwendet die beiden Nicht-Typ-Template-Parameter Line
und Col
.
Template-Parameter
Template-Parameter können Typen, Nicht-Typen und Templates selbst sein.
Typen
Typen sind die am häufigsten verwendeten Template-Parameter. Hier ein paar Beispiele:
std::vector<int> myVec;
std::map<std::string, int> myMap;
std::lock_guard<std::mutex> myLockGuard;
Nicht-Typen
Nicht-Typen können sein:
- lvalue-Referenz
nullptr
- Zeiger
- Aufzähler einer
enum
- Ganzzahl-Werte
- Fließkomma-Werte (C++20)
Ganzzahl-Werte sind die am häufigsten verwendeten Nicht-Typen. std::array
ist das typische Beispiel, weil seine Größe zur Compilezeit spezifiziert werden muss:
std::array<int, 3> myArray{1, 2, 3};
Templates
Templates selbst können Template-Parameter sein. Ihre Definition wirkt auf den ersten Blick ein wenig eigentümlich:
// templateTemplateParameters.cpp
#include <iostream>
#include <list>
#include <vector>
#include <string>
template <typename T, template <typename, typename> class Cont > // (1)
class Matrix{
public:
explicit Matrix(std::initializer_list<T> inList): data(inList) { // (2)
for (auto d: data) std::cout << d << " ";
}
int getSize() const{
return data.size();
}
private:
Cont<T, std::allocator<T>> data; // (3)
};
int main(){
std::cout << '\n';
// (4)
Matrix<int, std::vector> myIntVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << '\n';
std::cout << "myIntVec.getSize(): " << myIntVec.getSize() << '\n';
std::cout << std::endl;
Matrix<double, std::vector> myDoubleVec{1.1, 2.2, 3.3, 4.4, 5.5}; // (5)
std::cout << '\n';
std::cout << "myDoubleVec.getSize(): " << myDoubleVec.getSize() << '\n';
std::cout << '\n';
// (6)
Matrix<std::string, std::list> myStringList{"one", "two", "three", "four"};
std::cout << '\n';
std::cout << "myStringList.getSize(): " << myStringList.getSize() << '\n';
std::cout << '\n';
}
Matrix
ist ein Klassen-Template, das mit einer std::initializer_list
(Zeile 2) initialisiert werden muss. Eine Matrix
kann mit einem std::vector
(Zeile 4 und Zeile 5), oder einer std::list
(Zeile 6) verwendet werden, um ihre Werte zu halten. So weit, nichts Besonderes.
Aber halt, ich habe vergessen, Zeile 1 und Zeile 3 zu erläutern. Zeile 1 deklariert ein Klassen-Template, das zwei Template-Parameter besitzt. Okay, der erste Parameter ist der Typ der Elemente und der zweite Parameter steht für den Container. Schauen wir uns den zweiten Parameter genauer an: template <typename, typename> class Cont >
. Das bedeutet, dass der zweite Template-Parameter ein Template sein soll, das zwei Template-Parameter benötigt. Der erste Template-Parameter ist der Datentyp der Elemente, die der Container speichert, und der zweite Template-Parameter ist der Allokator, den jeder Container der Standard-Template-Bibliothek besitzt. Auch der Allokator hat einen Defaultwert, wie im Fall eines std::vector
. Der Allokator hängt vom Datentyp der Elemente ab.
template<
class T,
class Allocator = std::allocator<T>
> class vector;
Zeile 3 zeigt die Verwendung des Allokators im intern verwendeten Container. Die Matrix kann alle Container verwenden, die von der Art container<Typ der Elemente, Allocator der Elemente>
sind. Dies gilt für die Sequenzcontainer wie std::vector, std::deque
oder std::list. std::array
und std::forward_list
würden scheitern, da std::array
einen zusätzlichen Nicht-Typ zur Angabe seiner Größe zur Compilezeit benötigt und std::forward_list
die size
-Memberfunktion nicht unterstützt.
Wen das Schlüsselwort class
für den Namen des Template-Parameters irritiert – mit C++17 kann dafür typename
verwendet werden:
template <typename T, template <typename, typename> class Cont > // (1)
class Matrix;
template <typename T, template <typename, typename> typename Cont > // (2)
class Matrix;
Zeile (2) ist seit C++17 gültig und äquivalent zu Zeile (1).
Das nächste pdf-Bundle: Coroutines
In dem Artikel "Which pdf bundle do you want? Make your choice!" fiel die Wahl auf das Coroutine-Bundle.
Ich bin noch dabei, das Bundle vorzubereiten. In den nächsten Tagen sollte es aber verfügbar sein.
Wenn du den englischen oder deutschen Newsletter abonnierst, bekommst du automatisch den Link zum aktuellen pdf-Bundle. Schau mal rechts oben auf dieser Seite nach. Dieser Automatismus macht es für mich ganz bequem. Leute, die bereits meinen Newsletter abonniert haben, bekommen den Link automatisch.
Wie geht's weiter?
In meinem nächsten Artikel schreibe ich über Template-Argumente. Es ist sehr interessant, wie der Compiler automatisch die Datentypen bestimmt. Die Regeln gelten nicht nur für Funktions-Templates (C++98), sondern auch für auto
(C++11), für Klassen-Templates (C++17) und Concepts (C++20).
()