Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates

Modernes C++ Rainer Grimm  –  12 Kommentare

In meinem letzten Beitrag "Klassen-Templates" habe ich deren Grundlagen vorgestellt. Heute halte ich Überraschungen zur Vererbung von Klassen-Templates und der Instanziierung von Memberfunktionen von Klassen-Templates parat.

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates

Hier ist die erste Überraschung. Zumindest war es eine Überraschung für mich.

Vererbte Memberfunktionen von Klassen-Templates sind nicht verfügbar

Fangen wir einfach an.

// inheritance.cpp

#include <iostream>

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

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

int main(){

std::cout << '\n';

Derived derived;
derived.callBase();

std::cout << '\n';

}

Ich habe eine Klasse Base und Derived implementiert. Derived ist public abgeleitet von Base und kann daher in seiner Memberfunktion callBase (Zeile 2) die Memberfunktion func aus der Klasse Base verwenden. Ok, der Ausgabe des Programms habe ich nichts hinzuzufügen.

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates


Wird Base als ein Klassen-Template implementiert, ändert sich das Verhalten komplett.

// templateInheritance.cpp

#include <iostream>

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

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

int main(){

std::cout << '\n';

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

std::cout << '\n';

}

Der Compilerfehler kommt überraschend.

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates

Die Zeile "there are no arguments to 'func' that depend on a template parameter, so a declaration of 'func' must be available" aus der Fehlermeldung gibt den ersten Hinweis. func ist ein sogenannter nichtabhängiger Name, da er nicht vom Template-Parameter T abhängt. Nichtabhängige Namen werden an der Stelle der Template-Definition aufgelöst und gebunden. Folglich sucht der Compiler nicht in der von T abhängigen Basisklasse Base<T> und es gibt keinen Namen func außerhalb der Klassen-Templates. Nur abhängige Namen werden zum Zeitpunkt der Template-Instanzierung aufgelöst und gebunden.

Dieser Prozess wird Two Phase Lookup genannt. Die erste Phase ist insbesondere für das Auflösen von nichtabhängigen Namen zuständig, die zweite Phase ist für das Auflösen von abhängigen Namen.

Es gibt drei Workarounds, um das Namens-Lookup auf die abhängige Basisklasse zu lösen. Das folgende Beispiel verwendet alle drei.

// templateInheritance2.cpp

#include <iostream>

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

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

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

std::cout << '\n';

}
  1. Mache den Namen abhängig: Der Aufruf this->func1 in Zeile 1 ist abhängig, weil dieser implizit abhängig ist. Die Namenssuche wird in diesem Fall alle Basisklassen berücksichtigen.
  2. Führe den Namen in den aktuellen Scope ein: Der Ausdruck using Base<T>::func2 (Zeile 2) führt func2 in den aktuellen Scope ein.
  3. Rufe den Namen voll qualifiziert auf: Der Aufruf von func3 ist voll qualifiziert (Zeile 3). Dieser bricht allerdings einen virtuellen Dispatch und kann zu neuen Überraschungen führen.

Welche dieser Option empfiehlt sich? Im Allgemeinen bevorzuge ich die erste Option, durch die func1 abhängig wird: this->func1. Diese Lösung funktioniert auch, wenn man die Basisklasse umbenennt.

Zum Abschluss hier noch die Ausgabe des Programms:

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates


Die Instanziierung von Memberfunktionen ist lazy

Lazy bedeutet, dass die Instanziierung einer Memberfunktion eines Klassen-Templates nur bei Bedarf erfolgt. Beleg? Hier ist er:

// lazy.cpp

#include <iostream>

template<class T>
struct Lazy{
void func() { std::cout << "func\n"; }
void func2(); // not defined (1)
};

int main(){

std::cout << '\n';

Lazy<int> lazy;
lazy.func();

std::cout << '\n';

}

Obwohl die Methode func2() (1) der Klasse Lazy nur deklariert, aber nicht definiert ist, akzeptiert der Compiler das Programm. Eine Definition der Memberfunktion ist in diesem Fall nicht notwendig.

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates

Die Bedarfsauswertung des Instanziierungsprozesses von Memberfunktionen hat zwei interessante Eigenschaften.

Ressourcen sparen

Wenn man beispielsweise eine Klassenvorlage wie Array2 für verschiedene Typen instanziiert, werden nur die verwendeten Memberfunktionen instanziiert. Diese Bedarfsauswertung gilt nicht für eine Nicht-Template-Klasse Array1.

// lazyInstantiation.cpp

#include <cstddef>

class Array1 {
public:
int getSize() const {
return 10;
}
private:
int elem[10];
};

template <typename T, std::size_t N>
class Array2 {
public:
std::size_t getSize() const {
return N;
}
private:
T elem[N];
};


int main() {

Array1 arr;

Array2<int, 5> myArr1;
Array2<double, 5> myArr2; // (1)
myArr2.getSize(); // (2)

}

Die Memberfunktion getSize() der Klassen-Templates Array2 wird nur für myArr2 (1) instanziiert. Diese Instanziierung wird durch den Aufruf myArr2.getSize() (2) ausgelöst

C++ Insights zeigt die Hintergründe. Die entscheidenden Zeilen im folgenden Screenshot sind die Zeilen 40 und 59.

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates


Teilweise Verwendung von Klassen-Templates

Klassen-Templates lassen sich mit Template-Argumenten instanziieren, die nicht alle Memberfunktionen unterstützen. Werden die Memberfunktionen nicht verwendet, ist alles in Ordnung.

// classTemplatePartial.cpp

#include <iostream>
#include <vector>

template <typename T> // (1)
class Matrix {
public:
explicit Matrix(std::initializer_list<T> inList): data(inList) {}
void printAll() const { // (2)
for (const auto& d: data) std::cout << d << " ";
}
private:
std::vector<T> data;
};

int main() {

std::cout << '\n';

const Matrix<int> myMatrix1({1, 2, 3, 4, 5});
myMatrix1.printAll(); // (3)

std::cout << "\n\n";

const Matrix<int> myMatrix2({10, 11, 12, 13});
myMatrix2.printAll(); // (4)

std::cout << "\n\n";

const Matrix<Matrix<int>> myMatrix3({myMatrix1, myMatrix2});
// myMatrix3.printAll(); ERROR (5)

}

Das Klassen-Template Matrix (1) ist absichtlich einfach gehalten. Matrix besitzt einen Typ-Parameter T, hält seine Daten in einem std::vector und kann durch eine std::initalizer_list initialisiert werden. Dank der Memberfunktion printAll() kann die Klasse seine Element ausgeben. (3) und (4) zeigen Matrix im Einsatz. Der Ausgabeoperator ist für Matrix nicht überladen. Folglich kann ich myMatrix3 erstellen, das andere Matrix-Objekte als Mitglieder hat, aber ich kann sie nicht ausgeben.

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates

Das Aktivieren von Zeile 5 verursacht eine ziemlich ausführliche Fehlermeldung von 274 Zeilen mit dem GCC.

Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates


Wie geht's weiter?

In meinem nächsten Artikel stelle ich Alias-Templates vor und gehe auf Template-Parameter genauer ein.

Schlechtes Marketing

Ich habe ein schlechtes Marketing betrieben. Einige Leser haben mich in den letzten Tagen gefragt, ob mein auf LeanPub erschienenes C++20 Buch auch in physischer Form erhältlich sei. Klar, seit einem Monat.