Mixins
Das Curiously Recurring Template Pattern lässt sich nicht nur für statische Polymorphie nutzen, sondern auch für Mixins.
Das Curiously Recurring Template Pattern lässt sich nicht nur für statische Polymorphie nutzen, sondern auch für Mixins.
In meinem letzten Artikel "Mehr Details zur statischen und dynamischen Polymorphi [1]e" habe ich das Curiously Recurring Template Pattern (CRTP) verwendet, um statische Polymorphie zu implementieren. Ein weiterer typischer Anwendungsfall für CRTP sind Mixins.
![Mixins](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/3/5/6/0/1/9/crtp-f905574e318fb06c.png)
Mixins sind eine beliebte Methode beim Entwurf von Klassen, um diese mit neuer Funktionalität zu erweitern. Es ist in Python eine häufig verwendete Technik, das Verhalten einer Klasse durch Mehrfachvererbung zu anzupassen. Im Gegensatz zu C++ ist es in Python erlaubt, mehr als eine Definition einer Methode in einer Klassenhierarchie zu besitzen. Python verwendet einfach die Methode, die in der Method Resolution Order [2] (MRO) an erster Stelle steht.
Mixins lassen sich in C++ mit CRTP implementieren. Ein bekanntes Beispiel ist die Klasse std::enable_shared_from_this.
Das folgende Beispiel zeigt std::enable_shared_from_this
und damit CRTP in der Anwendung.
// enableShared.cpp
#include <iostream>
#include <memory>
class ShareMe: public std::enable_shared_from_this<ShareMe> { // (1)
public:
std::shared_ptr<ShareMe> getShared(){
return shared_from_this(); // (2)
}
};
int main(){
std::cout << '\n';
std::shared_ptr<ShareMe> shareMe(new ShareMe);
std::shared_ptr<ShareMe> shareMe1= shareMe->getShared(); // (3)
{
auto shareMe2(shareMe1);
std::cout << "shareMe.use_count(): " << shareMe.use_count() << '\n';
}
std::cout << "shareMe.use_count(): " << shareMe.use_count() << '\n';
shareMe1.reset();
std::cout << "shareMe.use_count(): " << shareMe.use_count() << '\n';
std::cout << '\n';
}
Mit der Klasse std::enable_shared_from_this
lassen sich Objekte erstellen, die eine std::shared_ptr
auf sich selbst zurückgeben. Dazu muss die Klasse MySharedClass
public von std::enable_shared_from_this
abgeleitet werden (1). Jetzt besitzt die Klasse MySharedClass
eine Member-Funktion shared_from_this
(2), mit der sich std::shared_ptr
zu ihren Objekten erstellen lassen. Der Aufruf shareMe->getShared()
(3) erzeugt einen neuen Smart Pointer. Die Memberfunktion getShared
verwendet intern die Funktion shared_from_this
(2). Der folgende Screenshot zeigt die Erstellung des Shared Pointer.
![Mixins](https://heise.cloudimg.io/width/397/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/3/5/6/0/1/9/enabledShared-d3024155691f25c5.png)
Mehr über Smart Pointer findet sich in meinem früheren Artikel Smart Pointer [3].
Um die Arbeitsweise von Mixin-Klassen in C++ zu zeigen, stelle ich einen typischen Anwendungsfall vor.
Erweitern einer Klasse mit allen Vergleichsoperatoren
Als Beispiel dient die Implementierung aller sechs Vergleichsoperatoren für einen Datentyp. (Mit C++20 kann der Compiler sie natürlich automatisch generieren: C++20: Der Drei-Wege-Vergleichsoperator [4]). Bislang existiert für unser Beispiel nur der Kleiner-Operator (<). Alle anderen fünf Vergleichsoperatoren (<=, >, >=
==
und !=
) lassen sich aber mithilfe des Kleiner-Operators umsetzen. Dank dieser Idee und CRTP, sind viel weniger Tastaturanschläge notwendig.
// mixins.cpp
#include <iostream>
#include <string>
template <typename Derived>
struct Relational {
friend bool operator > (Derived const& op1, Derived const& op2){
return op2 < op1;
}
friend bool operator == (Derived const& op1, Derived const& op2){
return !(op1 < op2) && !(op2 < op1);
}
friend bool operator != (Derived const& op1, Derived const& op2){
return (op1 < op2) || (op2 < op1);
}
friend bool operator <= (Derived const& op1, Derived const& op2){
return (op1 < op2) || (op1 == op2);
}
friend bool operator >= (Derived const& op1, Derived const& op2){
return (op1 > op2) || (op1 == op2);
}
};
class Apple: public Relational<Apple>{
public:
explicit Apple(int s): size{s}{};
friend bool operator < (Apple const& a1, Apple const& a2){ // (1)
return a1.size < a2.size;
}
private:
int size;
};
class Man: public Relational<Man>{ // (3)
public:
explicit Man(const std::string& n): name{n}{}
friend bool operator < (Man const& m1, Man const& m2){ // (2)
return m1.name < m2.name;
}
private:
std::string name;
};
int main(){
std::cout << std::boolalpha << '\n';
Apple apple1{5};
Apple apple2{10};
std::cout << "apple1 < apple2: " << (apple1 < apple2) << '\n';
std::cout << "apple1 > apple2: " << (apple1 > apple2) << '\n';
std::cout << "apple1 == apple2: " << (apple1 == apple2) << '\n';
std::cout << "apple1 != apple2: " << (apple1 != apple2) << '\n';
std::cout << "apple1 <= apple2: " << (apple1 <= apple2) << '\n';
std::cout << "apple1 >= apple2: " << (apple1 >= apple2) << '\n';
std::cout << '\n';
Man man1{"grimm"};
Man man2{"jaud"};
std::cout << "man1 < man2: " << (man1 < man2) << '\n';
std::cout << "man1 > man2: " << (man1 > man2) << '\n';
std::cout << "man1 == man2: " << (man1 == man2) << '\n';
std::cout << "man1 != man2: " << (man1 != man2) << '\n';
std::cout << "man1 <= man2: " << (man1 <= man2) << '\n';
std::cout << "man1 >= man2: " << (man1 >= man2) << '\n';
std::cout << '\n';
}
Ich habe für den Apple
und den Man
den Kleiner-Operator implementiert (Zeilen 1 und 2). Der Einfachheit halber verwende ich in meiner Argumentation nur die Klasse Man
. Man
ist eine public Ableitung der Klasse Relational<Man>
(Zeile 3), die CRTP verwendet. Die Klasse Relational
unterstützt die fünf fehlenden Vergleichsoperatoren (<=, <,>=, ==
und !=
). Die fünf Vergleichsoperatoren werden auf den Kleiner-Operator von Man
abgebildet (Zeile 2).
![Mixins](https://heise.cloudimg.io/width/401/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/3/5/6/0/1/9/mixins-4c1625d9e4bb78e0.png)
Ehrlich gesagt, gefällt mir der Name Mixins für dieses Idiom sehr gut. Die Klasse Relational
mischt die restlichen Vergleichsoperatoren in die Klasse Man
.
Die Besonderheit von CRTP ist, dass eine Klasse Derived
von einer Klassenvorlage Base
abgeleitet wird und Base Derived
als Templateargument besitzt:
class Derived : public Base<Derived> // (1)
{
...
};
class Derivedwrong : public Base<Derived> // (2)
{
...
};
Wie kann man sicherstellen, dass man nicht versehentlich wie in Zeile 2 die falsche Klasse DervivedWrong
von Base<Derived>
ableitet?
Geprüftes CRTP
Der Trick ist ganz einfach: Der Konstruktor von Base sollte privat sein.
// crtpCheck.cpp
#include <iostream>
template <typename Derived>
struct Base{
void interface(){
static_cast<Derived*>(this)->implementation();
}
private:
Base() = default; // (2)
friend Derived; // (2)
};
struct Derived1: Base<Derived1>{
void implementation(){
std::cout << "Implementation Derived1" << '\n';
}
};
struct Derived2: Base<Derived1>{ // (3)
void implementation(){
std::cout << "Implementation Derived1" << '\n';
}
};
template <typename T>
void execute(T& base){
base.interface();
}
int main(){
std::cout << '\n';
Derived1 d1;
execute(d1);
Derived2 d2;
execute(d2);
std::cout << '\n';
}
Base
hat einen privaten Default-Konstruktor (1). Nur die Klasse Base
selbst oder sein Freund Derived
(2) kann ihn aufrufen. Folglich schlägt der Aufruf Derived2 d2
(3) fehl, weil Derived2
von Base<Derived1>
abgeleitet ist und damit kein Freund von Base ist. Hier ist die Fehlermeldung des GCC:
![Mixins](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/3/5/6/0/1/9/crtpCheck-d5efc43152a555ad.png)
Wie geht's weiter?
Das Curiously Recurring Template Pattern (CRTP) ist zwar sehr mächtig, aber nicht leicht zu verstehen. Das Gleiche gilt für Expression Templates. Damit lassen sich überflüssige temporäre Objekte vermeiden. ( [5])
URL dieses Artikels:
https://www.heise.de/-6535761
Links in diesem Artikel:
[1] https://heise.de/-6526677
[2] https://docs.python.org/3/glossary.html#term-method-resolution-order
[3] https://www.grimm-jaud.de/index.php/blog/tag/smart-pointer
[4] https://www.heise.de/developer/artikel/C-20-Der-Drei-Weg-Vergleichsoperator-4782690.html
[5] mailto:rainer@grimm-jaud.de
Copyright © 2022 Heise Medien