zurück zum Artikel

AbhÀngige Namen

Rainer Grimm

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

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 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 können sein:

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:

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 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 [1]. 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.

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 [2]:

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.

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.

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.

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.

In meinem nĂ€chsten Artikel stelle ich automatische RĂŒckgabetypen genauer vor. Sie sind oft ein Lebensretter, wenn es um Funktions-Templates geht. ( [3])


URL dieses Artikels:
https://www.heise.de/-6212495

Links in diesem Artikel:
[1] https://en.cppreference.com/w/cpp/container/vector
[2] https://en.cppreference.com/w/cpp/language/dependent_name
[3] mailto:rainer@grimm-jaud.de