Typen, Nichttypen und Templates als Template-Parameter

Modernes C++  –  2 Kommentare

Ich denke, dir ist das zusätzliche Schlüsselwort typename oder template bereits vor einem Namen in einem Template aufgefallen. Mir auch. Ehrlich gesagt war ich überrascht. Heute geht es um abhängige Namen und verschiedene Template-Parameter.

Um abhängige Namen in Template zu verstehen, beginnt dieser Artikel mit Template-Parametern. Sie können Typen, Nichttypen oder Templates sein.

Typen

Typen sind die am häufigsten verwendeten Template-Parameter. Hier sind ein paar Beispiele:

std::vector<int> myVec;
std::map<std::string, int> myMap;
std::lock_guard<std::mutex> myLockGuard;
Nichttypen

Nichttypen können sein:

  • Lvalue-Referenzen
  • nullptr
  • Zeiger
  • Aufzähler
  • Integrale

Integrale sind sicher die bekanntesten Nichttypen. std::array ist ein typisches Beispiel, denn sein Datentyp und seine Größe müssen zur Compilezeit angegeben werden:

std::array<int, 3> myArray{1, 2, 3};
Templates

Templates können selbst Template-Parameter sein. In diesem Fall werden sie Template-Templates-Parameter genannt. Die Adaptoren für Container std::stack, std::queue und std::priority_queue verwenden per Default std::deque, um ihre Argumente zu speichern. Es lässt sich aber auch ein anderer Container einsetzen. Ihr Einsatz sollte kein Überraschungspotenzial bergen:

std::stack<int> stack1;
stack1.push(5);

std::stack<double, std::vector<double>> stack2;
stack2.push(10.5);

Ihre Definition hingegen schon:

// 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 << std::endl;

// (4)
Matrix<int, std::vector> myIntVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << std::endl;
std::cout << "myIntVec.getSize(): " << myIntVec.getSize() << std::endl;

std::cout << std::endl;

// (5)
Matrix<double, std::vector> myDoubleVec{1.1, 2.2, 3.3, 4.4, 5.5};
std::cout << std::endl;
std::cout << "myDoubleVec.getSize(): " << myDoubleVec.getSize() << std::endl;

std::cout << std::endl;
// (6)
Matrix<std::string, std::list> myStringList{"one", "two", "three", "four"};
std::cout << std::endl;
std::cout << "myStringList.getSize(): " << myStringList.getSize() << std::endl;

std::cout << std::endl;

}

Matrix ist ein einfaches Klassen-Template, dass sich über eine std::initializer_list (Zeile 2) initialisieren lässt. Eine Matrix kann einen std::vector (Zeile 4 und 5) oder eine std::list (Zeile 6) verwenden, um ihre Werte zu speichern. Soweit, nichts besonderes.

Stopp, ich habe die Zeilen 1 und 3 ignoriert. Zeile 1 deklariert ein Klasse-Templates, das zwei Template-Parameter benötigt. Der erste Parameter steht für den Datentyp der Elemente und der zweite Parameter für den Container, der die Datenelemente speichert. Insbesondere der zweite Parameter verdient eine genauere Betrachtung: template <typename, typename> class Cont >. Das heißt, das zweite Template-Argument sollte ein Template sein, dass selbst zwei Template-Parameter benötigt. Der erste Template-Parameter steht in diesem Fall für den Datentyp der Elemente, die der Container speichert und der zweite ist der Allokator, den ein Container der Standard Template Library besitzt. Der Allokator besitzt einen Default wie im Fall von std::vector. Der Allokator ist vom Elemente abhängig:

template<
class T,
class Allocator = std::allocator<T>
> class vector;

Zeile 3 zeigt die Anwendung des Allokators in der Matrix. Matrix kann alle Container verwenden, die nach dem Muster Container< Datentyp der Elemente, Allokator für die Elemente> gestrickt sind. Dies trifft auf die sequenziellen Container wie std::vector, std::deque oder std::list zu. std::array und std::forward_list können nicht verwendet werden, da std::array einen zusätzlichen Parameter besitzt, um seine Größe zur Compilezeit anzugeben und std::forward_list die size-Methode nicht unterstützt.

Nun habe ich die Grundlagen gelegt und komme zum zentralen Punkt dieses Artikels.

Abhängige Namen

Zuerst einmal: Was ist ein abhängiger Name? Er ist im Wesentlichen ein Name, der von einem Template-Parameter abhängt. Hier sind sein paar Beispiel, basierend auf cppreference.com:

template<typename T>
struct X : B<T> // "B<T>" is dependent on T
{
typename T::A* pa; // "T::A" is dependent on T
void f(B<T>* pb) {
static int i = B<T>::i; // "B<T>::i" is dependent on T
pb->j++; // "pb->j" is dependent on T
}
};

Jetzt geht es los mit dem Spaß. Ein abhängiger Name kann ein Typ, ein Nichttyp oder ein Template selbst sein. Die Namensauflösung ist der erste große Unterschied zwischen einem nichtabhängigen und einem abhängigen Namen.

  • Nichtabhängige Name werden bei der Template-Definition aufgelöst.
  • Abhängige Namen werden dann aufgelöst, wenn die Template-Argumente bekannt sind. Dies bedeutet leider Template-Instanziierung.

Falls du nun einen abhängigen Namen in einer Template-Deklaration oder einer Template-Defintion verwendest, weiß der Compiler nicht, ob sich der Name auf einen Typen, einen Nichttypen oder ein Template bezieht. In diesem Fall nimmt der Compiler an, dass der abhängig Namen für einen Nichttyp steht. Dies kann natürlich falsch sein. Hier musst du dem Compiler unter die Schulter greifen.

Bevor ich dir zwei Beispiele zeige, muss ich der Vollständigkeit halber noch auf eine Ausnahme der Regel hinweisen. Du kannst diese Zeile aber gerne ignorieren und zum nächsten Abschnitt springen, wenn du an der zentralen Idee interessiert bist. Hier ist die Ausnahme der Regel: Wenn der Name sich auf die aktuelle Instanziierung bezieht, kann der Compiler den Name bereits zum Zeitpunkt der Template-Definition bestimmen. Hier sind ein paar Beispiele:

template <class T> class A {
A* p1; // A is the current instantiation
A<T>* p2; // A<T> is the current instantiation
::A<T>* p4; // ::A<T> is the current instantiation
A<T*> p3; // A<T*> is not the current instantiation
};
template <class T> class A<T*> {
A<T*>* p1; // A<T*> is the current instantiation
A<T>* p2; // A<T> is not the current instantiation
};
template <int I> struct B {
static const int my_I = I;
static const int my_I2 = I+0;
static const int my_I3 = my_I;
B<my_I>* b3; // B<my_I> is the current instantiation
B<my_I2>* b4; // B<my_I2> is not the current instantiation
B<my_I3>* b5; // B<my_I3> is the current instantiation
};

Hier ist nochmals der zentrale Punkt meines Artikels. Falls ein abhängiger Name ein Typ, ein Nichttyp oder ein Template sein kann, musst du dem Compiler unter die Schulter greifen.

Verwende typename, falls der abhängige Name ein Typ ist

Nach solch einer langen Einleitung sollte das nächste Programmbeispiel die Mehrdeutigkeit auf den Punkt bringen:

template <typename T>
void test(){
std::vector<T>::const_iterator* p1; // (1)
typename std::vector<T>::const_iterator* p2; // (2)
}

Ohne das Schlüsselwort typename in Zeile 2 würde der Name std::vector<T>::const_iterator in Zeile 2 als Nichttyp interpretiert werden und damit wäre konsequenterweise das * Symbol eine Multiplikation und keine Zeigerdeklaration. Genau das passiert in Zeile 1.

Entsprechend gilt, falls der abhängige Name ein Template sein soll, dass du dem Compiler einen Hinweis geben musst.

Verwende .template, falls der abhängige Name ein Template ist

Ehrlich gesagt, schaut die Syntax sehr gewöhnungsbedürftig aus:

template<typename T>
struct S{
template <typename U> void func(){}
}
template<typename T>
void func2(){
S<T> s;
s.func<T>(); // (1)
s.template func<T>(); // (2)
}

Dieselbe Geschichte wie gerade eben. Vergleiche die Zeilen 1 und 2. Wenn der Compiler den Name s.func liest (Zeile 1), entscheidet er, diesen als Nichttyp zu lesen. Dies bedeutet, dass das <-Zeichen für einen Vergleichsoperator steht, aber nicht für die öffnende Klammer des Template-Arguments der generischen Methode func. In diesem Fall musst du angeben, dass s.func für ein Template steht (Zeile 2): s.template func.

Hier ist die Zentralaussage dieses Artikels in einem Satz: Wenn du einen abhängigen Namen hast, verwende typename, um auszudrücken, dass es sich um einen Typ handelt oder .template, um auszudrücken, dass es sich um ein Template handelt.

Wie geht es weiter?

Die nächsten Regeln in den C++ Core Guidelines sind über C-Style-Programmierung und Sourcecode-Dateien. Genau davon handelt mein nächster Artikel.