zurück zum Artikel

Templates: Missverständnisse und Überraschungen

Modernes C++

Ich erkläre oft die Grundlagen zu Templates. Diese sind speziell, daher begegnen mir oft Missverständnisse, die zwangsläufig zu Überraschungen führen.

Mein erstes Missverständnis ist wohl für viele, aber nicht für alle C++-Entwickler offensichtlich.

Templates verwandter Datentypen sind nicht verwandt

Zuerst einmal, was meine ich mit verwandt. Dies ist mein informeller Begriff, der für Datentypen steht, die impliziert konvertiert [1] werden können. Hier geht die Geschichte los:

// genericAssignment.cpp

#include <vector>

template <typename T, int N> // (1)
struct Point{
Point(std::initializer_list<T> initList): coord(initList){}

std::vector<T> coord;
};

int main(){

Point<int, 3> point1{1, 2, 3};
Point<int, 3> point2{4, 5, 6};

point1 = point2; // (2)

auto doubleValue = 2.2;
auto intValue = 2;
doubleValue = intValue; // (3)

Point<double, 3> point3{1.1, 2.2, 3.3};
point3 = point2; // (4)

}

Das Klassen-Template Point steht für einen Punkt in einem n-dimensionalen Raum. Der Datentyp der Koordinaten und die Dimension lassen sich anpassen (Zeile 1). Zum Speichern der Koordinaten dient ein std::vector<T>. Wenn ich nun zwei Punkte mit demselben Datentyp und derselben Dimension definiere, kann ich diese zuweisen.

Hier beginnt das Missverständnis. Ein int- kann einem double-Wert zugewiesen werden (Zeile 3). Daher sollte es doch möglich sein, einen Punkt von ints einem von doubles zuzuweisen. Der Compiler besitzt aber eine eindeutige Meinung zu Zeile 4. Beide Templates sind nicht verwandt und können damit nicht zugewiesen werden. Sie stellen verschiedene Datentypen dar.

Templates: Missverstänisse und Überraschungen

Die Fehlermeldung gibt den ersten Hinweis. Ich benötige ein Zuweisungsoperator von Point<int, 3> nach Point<double, 3>. Nun besitzt die Klasse einen generischen Copy-Zuweisungsoperator.

// genericAssignment2.cpp

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

template <typename T, int N>
struct Point{

Point(std::initializer_list<T> initList): coord(initList){}

template <typename T2>
Point<T, N>& operator=(const Point<T2, N>& point){ // (1)
static_assert(std::is_convertible<T2, T>::value,
"Cannot convert source type to destination type!");
coord.clear();
coord.insert(coord.begin(), point.coord.begin(), point.coord.end());
return *this;
}

std::vector<T> coord;

};


int main(){

Point<double, 3> point1{1.1, 2.2, 3.3};
Point<int, 3> point2{1, 2, 3};

Point<int, 2> point3{1, 2};
Point<std::string, 3> point4{"Only", "a", "test"};

point1 = point2; // (2)

// point2 = point3; // (3)
// point2 = point4; // (4)

}

Dank der Zeile (1) ist die Copy-Zuweisung in Zeile (2) gültig. Hier ist ein genauerer Blick auf das Klassen-Template Point.

Wenn die Zeilen (3) und (4) zum Einsatz kommen, schlägt die Kompilierung fehl.

Templates: Missverstänisse und Überraschungen

Zeile (3) führt zum Fehler, denn beide Punkte besitzen verschieden Dimensionen. Zeile (4) löst static_assert in dem Zuweisungsoperator aus, da ein std::string nicht nach int konvertiert werden kann.

Ich nehme an, das nächste Missverständnis besitzt ein größeres Überraschungspotenzial.

Methoden, die von Klassen-Templates geerbt werden, stehen nicht automatisch zur Verfügung

Es geht wieder einfach los:

// inheritance.cpp

#include <iostream>

class Base{
public:
void func(){ // (1)
std::cout << "func" << std::endl;
}
};

class Derived: public Base{
public:
void callBase(){
func(); // (2)
}
};

int main(){

std::cout << std::endl;

Derived derived;
derived.callBase();

std::cout << std::endl;

}

Das Beispiel besitzt eine Klasse Base und Derived. Derived ist von Base public abgeleitet und kann somit in der Methode callBase (Zeile 2) die Methode func der Klasse Base verwendet werden. Die Ausgabe des Programms zeigt das erwartete Verhalten.

Templates: Missverstänisse und Überraschungen

Wenn die Klasse Base zur Template-Klasse wird, verändert sich das Verhalten grundlegend:

// templateInheritance.cpp

#include <iostream>

template <typename T>
class Base{
public:
void func(){ // (1)
std::cout << "func" << std::endl;
}
};

template <typename T>
class Derived: public Base<T>{
public:
void callBase(){
func(); // (2)
}
};

int main(){

std::cout << std::endl;

Derived<int> derived;
derived.callBase();

std::cout << std::endl;

}

Dass sich das Programm nun nicht mehr übersetzen lässt, ist für viele C++-Programmierer überraschend.

Templates: Missverstänisse und Überraschungen

Die Zeile "there are no arguments to 'func' that depend on a template parameter, so a declaration of 'func' must be available" der Fehlermeldung gibt den ersten Hinweis. func ist ein sogenannter nichtabhängiger (non-dependent) Name, denn er hängt nicht vom Template-Parameter T ab. Die Konsequenz ist es, dass der Compiler nicht die von T abhängig Basisklasse Base<T> berücksichtigt und es keinen Namen func außerhalb des Klassen-Templates gibt.

Drei Workarounds bieten sich an, um bei der Namensauflösung die abhängige Basisklasse zu berücksichtigen. Das folgende Beispiel verwendet alle drei:

// templateInheritance2.cpp

#include <iostream>

template <typename T>
class Base{
public:
void func1() const {
std::cout << "func1()" << std::endl;
}
void func2() const {
std::cout << "func2()" << std::endl;
}
void func3() const {
std::cout << "func3()" << std::endl;
}
};

template <typename T>
class Derived: public Base<T>{
public:
using Base<T>::func2; // (2)
void callAllBaseFunctions(){

this->func1(); // (1)
func2(); // (2)
Base<T>::func3(); // (3)

}
};


int main(){

std::cout << std::endl;

Derived<int> derived;
derived.callAllBaseFunctions();

std::cout << std::endl;

}

Zum Abschluss, die Ausgabe des Programms:

Templates: Missverstänisse und Überraschungen

Wie geht's weiter?

Ich möchte noch gerne mehr über abhängige Namen in meinen nächsten Artikel schreiben. Manchmal ist es notwendig, abhängige Namen mit typename oder template auszuzeichnen. Wenn du das, wie ich das erste Mal siehst, bist du vermutlich genauso überrascht wie ich.

C++-Schulungen im Großraum Stuttgart

Ich freue mich darauf, weitere C++-Schulungen halten zu dürfen.

Die Details zu meinen C++- und Python-Schulungen gibt es auf www.ModernesCpp.de [5].


URL dieses Artikels:
http://www.heise.de/-4316778

Links in diesem Artikel:
[1] https://en.cppreference.com/w/cpp/language/implicit_conversion
[2] https://www.modernescpp.de/index.php/c/2-c/3-c-11-und-c-14
[3] https://www.modernescpp.de/index.php/c/2-c/18-generische-programmierung-templates-mit-c
[4] https://www.modernescpp.de/index.php/c/2-c/19-embedded-programmierung-mit-modernem-c20190102153438
[5] https://www.modernescpp.de/