Überraschungen inklusive: Vererbung und Memberfunktionen von Klassen-Templates
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.
In meinem letzten Beitrag "Klassen-Templates [1]" habe ich deren Grundlagen vorgestellt. Heute halte ich Überraschungen zur Vererbung von Klassen-Templates und der Instanziierung von Memberfunktionen von Klassen-Templates parat.
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.
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.
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 [2] 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';
}
- Mache den Namen abhängig: Der Aufruf
this->func
1 in Zeile 1 ist abhängig, weil dieser implizit abhängig ist. Die Namenssuche wird in diesem Fall alle Basisklassen berücksichtigen. - Führe den Namen in den aktuellen Scope ein: Der Ausdruck
using Base<T>::func2
(Zeile 2) führtfunc2
in den aktuellen Scope ein. - 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:
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.
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 [3] zeigt die Hintergründe. Die entscheidenden Zeilen im folgenden Screenshot sind die Zeilen 40 und 59.
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_lis
t 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.
Das Aktivieren von Zeile 5 verursacht eine ziemlich ausführliche Fehlermeldung von 274 Zeilen mit dem GCC.
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 [4] auch in physischer Form erhältlich sei. Klar, seit einem Monat [5]. ( [6])
URL dieses Artikels:
https://www.heise.de/-6057082
Links in diesem Artikel:
[1] https://heise.de/-6052426
[2] https://stackoverflow.com/questions/7767626/two-phase-lookup-explanation-needed
[3] https://cppinsights.io/s/451db374
[4] https://leanpub.com/c20
[5] https://www.amazon.de/dp/B09328NKXK
[6] mailto:rainer@grimm-jaud.de
Copyright © 2021 Heise Medien