Mehr Details zur statischen und dynamischen Polymorphie
Nach dem Einblick in dynamische Polymorphie fahre ich mit der statischen Polymorphie fort und stelle ein interessantes Idiom in C++ vor: das Curiously Recurring Template Pattern (CRTP).
Nach dem Einblick in dynamische Polymorphie fahre ich mit der statischen Polymorphie fort und stelle ein interessantes Idiom in C++ vor: das Curiously Recurring Template Pattern (CRTP).
![Mehr Details zur statischen und dynamischen Polymorphie](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/3/5/0/1/5/9/StaticDynamic-ca01e750f13c4a1c.png)
Starten möchte ich diesen Artikel mit einer kurzen Erinnerung an den letzten Beitrag zur dynamischen Polymorphie [1].
Dynamische Polymorphie basiert auf der Objektorientierung und ermöglicht es uns, zwischen dem Interface und der Implementierung einer Klassenhierarchie zu unterscheiden. Um Dynamic Dispatch zu erhalten, genügen zwei Zutaten: Virtualität und eine Indirektion wie einen Zeiger oder eine Referenz. Das folgende Programm liefert ein Beispiel für dynamische Polymorphie:
// dispatchDynamicPolymorphism.cpp
#include <chrono>
#include <iostream>
auto start = std::chrono::steady_clock::now();
void writeElapsedTime(){
auto now = std::chrono::steady_clock::now();
std::chrono::duration<double> diff = now - start;
std::cerr << diff.count() << " sec. elapsed: ";
}
struct MessageSeverity{
virtual void writeMessage() const {
std::cerr << "unexpected" << '\n';
}
};
struct MessageInformation: MessageSeverity{
void writeMessage() const override {
std::cerr << "information" << '\n';
}
};
struct MessageWarning: MessageSeverity{
void writeMessage() const override {
std::cerr << "warning" << '\n';
}
};
struct MessageFatal: MessageSeverity{};
void writeMessageReference(const MessageSeverity& messServer){ // (1)
writeElapsedTime();
messServer.writeMessage();
}
void writeMessagePointer(const MessageSeverity* messServer){ // (2)
writeElapsedTime();
messServer->writeMessage();
}
int main(){
std::cout << '\n';
MessageInformation messInfo;
MessageWarning messWarn;
MessageFatal messFatal;
MessageSeverity& messRef1 = messInfo;
MessageSeverity& messRef2 = messWarn;
MessageSeverity& messRef3 = messFatal;
writeMessageReference(messRef1);
writeMessageReference(messRef2);
writeMessageReference(messRef3);
std::cerr << '\n';
MessageSeverity* messPoin1 = new MessageInformation;
MessageSeverity* messPoin2 = new MessageWarning;
MessageSeverity* messPoin3 = new MessageFatal;
writeMessagePointer(messPoin1);
writeMessagePointer(messPoin2);
writeMessagePointer(messPoin3);
std::cout << '\n';
}
Die statische Polymorphie basiert auf Templates. Lass mich das Programm mithilfe des Curiously Recurring Template Pattern (CRTP) refaktorieren.
Statische Polymorphie
Bevor ich das vorherige Programm dispatchDynamicPolymorphism.cpp
überarbeite, möchte ich den Kerngedanken von CRTP vorstellen: Eine Klasse Derived
leitet sich von einem Klassen-Template Base
ab, das seinerseits Derived
als Template-Argument besitzt.
template <typename T>
class Base
{
...
};
class Derived : public Base<Derived>
{
...
};
Die eigentliche Natur von CRTP sieht folgendermaßen aus:
// crtp.cpp
#include <iostream>
template <typename Derived>
struct Base{
void interface(){ // (2)
static_cast<Derived*>(this)->implementation();
}
void implementation(){ // (3)
std::cout << "Implementation Base" << std::endl;
}
};
struct Derived1: Base<Derived1>{
void implementation(){
std::cout << "Implementation Derived1" << std::endl;
}
};
struct Derived2: Base<Derived2>{
void implementation(){
std::cout << "Implementation Derived2" << std::endl;
}
};
struct Derived3: Base<Derived3>{}; // (4)
template <typename T> // (1)
void execute(T& base){
base.interface();
}
int main(){
std::cout << '\n';
Derived1 d1;
execute(d1);
Derived2 d2;
execute(d2);
Derived3 d3;
execute(d3);
std::cout << '\n';
}
Ich verwende in dem Funktions-Template execute
(// 1) statische Polymorphie. Jede base
ruft die Methode base.interface
auf. Die Memberfunktion Base::interface
(// 2) ist der Schlüssel des CRTP-Idioms. Die Memberfunktion leitet an die Implementierung der abgeleiteten Klasse weiter: static_cast<Derived*>(this)->implementation()
. Das ist möglich, weil die Funktion erst beim Aufruf instanziiert wird. Zu diesem Zeitpunkt sind die abgeleiteten Klassen Derived1
, Derived2
und Derived3
vollständig definiert. Daher kann die Funktion Base::interface
die Implementierung der abgeleiteten Klassen verwenden. Interessant ist dabei die Memberfunktion Base::implementation
(// 3). Sie spielt die Rolle einer Defaultimplementierung für die statische Polymorphie der Klasse Derived3
(// 4).
Hier ist die Ausgabe des Programms:
![Mehr Details zur statischen und dynamischen Polymorphie](https://heise.cloudimg.io/width/324/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/3/5/0/1/5/9/crtp-3785cc7bffd53cb4.png)
Nun möchte ich den nächsten Schritt machen und das Programm dispatchDynamicPolymorphism.cpp
refaktorieren.
// dispatchStaticPolymorphism.cpp
#include <chrono>
#include <iostream>
auto start = std::chrono::steady_clock::now();
void writeElapsedTime(){
auto now = std::chrono::steady_clock::now();
std::chrono::duration<double> diff = now - start;
std::cerr << diff.count() << " sec. elapsed: ";
}
template <typename ConcreteMessage> // (1)
struct MessageSeverity{
void writeMessage(){ // (2)
static_cast<ConcreteMessage*>(this)->writeMessageImplementation();
}
void writeMessageImplementation() const {
std::cerr << "unexpected" << std::endl;
}
};
struct MessageInformation: MessageSeverity<MessageInformation>{
void writeMessageImplementation() const { // (3)
std::cerr << "information" << std::endl;
}
};
struct MessageWarning: MessageSeverity<MessageWarning>{
void writeMessageImplementation() const { // (4)
std::cerr << "warning" << std::endl;
}
};
struct MessageFatal: MessageSeverity<MessageFatal>{}; // (5)
template <typename T>
void writeMessage(T& messServer){
writeElapsedTime();
messServer.writeMessage(); // (6)
}
int main(){
std::cout << std::endl;
MessageInformation messInfo;
writeMessage(messInfo);
MessageWarning messWarn;
writeMessage(messWarn);
MessageFatal messFatal;
writeMessage(messFatal);
std::cout << std::endl;
}
In diesem Fall leiten sich alle konkreten Klassen (// 3, // 4 und // 5) von der Basisklasse MessageSeverity
ab. Die Memberfunktion writeMessage
ist die Schnittstelle, die an die konkreten Implementierungen writeMessageImplementation
weiterleitet. Um dies zu erreichen, wird das Objekt in die ConcreteMessage upgecastet: static_cast<ConcreteMessage*>(this)->writeMessageImplementation()
. Dies ist der statische Dispatch zur Compilezeit, der auch den Namen für diese Technik geprägt hat: statische Polymorphie.
Um ehrlich zu sein, habe ich einige Zeit gebraucht, um mich an die eigentümliche Syntax von CRTP zu gewöhnen, aber die Anwendung der statischen Polymorphie in // (6) ist recht einfach.
Zum Schluss möchte ich dynamische und statische Polymorphie kurz vergleichen:
Dynamische versus statische Polymorphie
Dynamische Polymorphie findet zur Laufzeit statt, statische Polymorphie zur Compilezeit. Dynamische Polymorphie erfordert in der Regel eine Zeigerindirektion zur Laufzeit (vergleiche den Beitrag "Demystifying virtual functions, Vtable, and VPTR in C++ [2]"), statische Polymorphie hat hingegen keinen Laufzeiteinfluss. Zugegeben, es gibt einen Grund, warum das Idiom Curiously Recurring Template Pattern (CRTP) den Begriff curious (seltsam) im Namen trägt: Für Anfänger ist das Idiom ziemlich schwer zu verstehen. Wann sollte es daher eingesetzt werden?
Zunächst einmal solltest du die Kosten für einen virtuellen Aufruf nicht überschätzen. In den meisten Fällen lassen sie sich ignorieren. Details zu den Performanzzahlen finden sich in dem hervorragenden Paper "Technical Report on C++ Performance [3]". Es ist zwar recht alt, enthält aber in Abschnitt 5.3.3 interessante Messungen zu den zusätzlichen Kosten virtueller Funktionsaufrufe. Wenn du dir immer noch Sorgen um die Performanz machst, gibt es nur ein Mittel: Messen. Versioniere deine Performanztests und wiederhole sie immer dann, wenn sich etwas an deiner Hardware, deinem Compiler oder der Compiler-Version geändert hat, denn derartige Änderungen lassen deine bisherigen Leistungszahlen ungültig werden.
Letztendlich wird der Code viel öfter gelesen als geschrieben. Daher solltest du die Techniken anwenden, mit denen dein Team am besten zurechtkommt.
Wie geht's weiter?
Mixins sind eine beliebte Technik in Python. Sie ermöglichen es dir, das Verhalten einer Klasse durch Mehrfachvererbung zu ändern. Dank CRTP gibt es Mixins auch in C++. Lies mehr darüber in meinem nächsten Artikel. ( [4])
URL dieses Artikels:
https://www.heise.de/-6526677
Links in diesem Artikel:
[1] https://www.heise.de/blog/Statische-und-dynamische-Polymorphie-6498701.html
[2] https://medium.com/@abhichavhan/demystifying-virtual-functions-vtable-and-vptr-in-c-bf56f11f7cc7
[3] http://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf
[4] mailto:rainer@grimm-jaud.de
Copyright © 2022 Heise Medien