Mixins

Das Curiously Recurring Template Pattern lässt sich nicht nur für statische Polymorphie nutzen, sondern auch für Mixins.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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 Polymorphie" habe ich das Curiously Recurring Template Pattern (CRTP) verwendet, um statische Polymorphie zu implementieren. Ein weiterer typischer Anwendungsfall für CRTP sind Mixins.

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

Mehr über Smart Pointer findet sich in meinem früheren Artikel Smart Pointer.

Um die Arbeitsweise von Mixin-Klassen in C++ zu zeigen, stelle ich einen typischen Anwendungsfall vor.

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

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?

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:

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