Abhängige Namen

Modernes C++ Rainer Grimm  –  19 Kommentare

Ein abhängiger Name ist ein Name, der von einem Template abhängt. Er kann ein Typ, ein Nichttyp oder ein Template-Parameter sein.

Dependent Names

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 Datentypen

Integrale Datentypen sind wohl 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};

Mit C++20 lassen sich zwei neue Nichttypen verwenden: Fließkommatypen und literale Typen.

Literale Typen müssen im Wesentlichen die folgenden zwei Eigenschaften besitzen:

  • Alle Basisklassen und nicht-statischen Datenelemente sind public und nicht veränderbar.
  • Die Typen aller Basisklassen und nicht-statischen Datenelemente sind Strukturtypen oder Arrays von Strukturtypen.

Ein literaler Typ muss einen constexpr-Konstruktor besitzen.

Das folgende Programm verwendet Fließkommatypen und Literal-Typen als Template-Parameter.

// nonTypeTemplateParameter.cpp

struct ClassType {
constexpr ClassType(int) {} // (1)
};

template <ClassType cl> // (2)
auto getClassType() {
return cl;
}

template <double d> // (3)
auto getDouble() {
return d;
}

int main() {

auto c1 = getClassType<ClassType(2020)>();

auto d1 = getDouble<5.5>(); // (4)
auto d2 = getDouble<6.5>(); // (4)

}

ClassType hat einen constexpr-Konstruktor (1) und kann als Template-Argument verwendet werden (2). Das Funktions-Template getDouble (3) kann nur double-Werte annehmen. Ich möchte ausdrücklich betonen, dass jeder Aufruf der Funktionsvorlage getDouble (4) mit einem neuen Argument die Instanziierung einer neuen Spezialisierung von getDouble auslöst. Das bedeutet, dass es zwei Instanziierungen für die double-Werte 5.5 und 6.5 gibt.

Templates

Templates können selbst Template-Parameter sein. In diesem Fall werden sie Template-Template-Parameter genannt.

// 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 << '\n';

// (5)
Matrix<double, std::vector> myDoubleVec{1.1, 2.2, 3.3, 4.4, 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 einfaches Klassen-Template, dass sich über eine std::initializer_list (2) initialisieren lässt. Eine Matrix kann einen std::vector (4 und 5) oder eine std::list (6) verwenden, um ihre Werte zu speichern. Soweit, nichts besonderes.

Dependent Names

Stopp, ich habe (1) und (3) ignoriert. (1) deklariert ein Klasse-Template, das zwei Template-Parameter benötigt. Der erste Parameter steht für den Datentyp der Elemente und der zweite 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 ist ein Template, das 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<
typename T,
typename Allocator = std::allocator<T>
> class vector;

(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 nicht abhängigen und einem abhängigen Namen.

  • Nicht abhängige Name werden bei der Template-Definition aufgelöst.
  • Abhängige Namen werden bei der Template-Instanziierung aufgelöst.

Beim Verwenden eines abhängigen Namens in einer Template, 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. Das kann natürlich falsch sein. Hier muss man dem Compiler unter die Schulter greifen.

Bevor ich zwei Beispiele zeige, muss ich der Vollständigkeit halber noch auf eine Ausnahme der Regel hinweisen. Wer nur an der zentralen Idee interessiert ist, kann diese Zeile gerne ignorieren und zum nächsten Abschnitt springen. Hier ist die Ausnahme der Regel: Wenn der Name sich auf die aktuelle Instanziierung (current instantiation) bezieht, kann der Compiler den Namen bereits zum Zeitpunkt der Template-Definition bestimmen. Hier sind ein paar Beispiele:

template <typename 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 oder ein Template ist, benötigt der Compiler Hilfe.

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 (2) würde der Name std::vector<T>::const_iterator in (2) als Nichttyp interpretiert werden und damit wäre konsequenterweise das * Symbol eine Multiplikation und keine Zeigerdeklaration. Genau das passiert in (1).

Entsprechend gilt, dass der Compiler einen Hinweis benötigt, falls der abhängige Name ein Template sein soll.

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 (1) und (2). Wenn der Compiler den Name s.func liest (1), entscheidet er, diesen als Nichttyp zu interpretieren. 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 muss angegeben werden, dass s.func für ein Template steht (2): s.template func.

Hier ist die Zentralaussage dieses Artikels in einem Satz: Wer einen abhängigen Namen hat, muss typename verwenden, 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?

In meinem nächsten Artikel stelle ich automatische Rückgabetypen genauer vor. Sie sind oft ein Lebensretter, wenn es um Funktions-Templates geht.