Templates: Missverständnisse und Überraschungen

Modernes C++  –  0 Kommentare

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 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.

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.

  • Point<T, N>& operator=(const Point<T2, N>& point): Das Ziel der Zuweisung besitzt den Datentyp Point<T, N> und nimmt nur einen Datentyp an, der dieselbe Dimension besitzt, aber einen verschiedenen Datentyp verwenden kann: Point<T2, N>.
  • static_assert(std::is_convertible<T2, T>::value, "Cannot convert source type to destination type!"): Dieser Ausdruck prüft mithilfe der Funktion std::is_convertible der Type-Traits-Bibiliothek, ob sich T2 in T konvertieren lässt.

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

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.

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.

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;

}
  • Mach den Name abhängig: Der Aufruf this->func1 in Zeile 1 ist abhängig, den this ist implizit abhängig. Die Namensauflösung berücksichtigt in diesem Fall alle Basisklassen.
  • Führe den Name in den aktuellen Bereich ein: Der Ausdruck using Base<T>::func2 (Zeile 2) führt func2 in den aktuellen Bereich ein.
  • Rufe den Name vollqualifiziert auf: Indem du func3 vollqualifiziert (Zeile 3) aufrufst, brichst du eventuell einen virtuellen Dispatch und bist offen für neue Überraschungen.

Zum Abschluss, die Ausgabe des Programms:

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.