C++23: Syntactic Sugar with Deducing This

Dank Deducing This das in C++ häufig verwendete, aber schwer zu verstehende Curiously Recurring Template Pattern deutlich einfacher anzuwenden.

In Pocket speichern vorlesen Druckansicht 39 Kommentare lesen
Distance,Education,Online,Learning,Concept.,Robot,Teacher,,Abstract,Classroom,Interior

(Bild: Besjunior/Shutterstock.com)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Das Curiously Recurring Template Pattern (CRTP) ist ein häufig verwendetes Idiom in C++. Es ist ähnlich schwer zu verstehen wie das klassische Entwurfsmuster Viistor, das ich in meinem letzten Artikel vorgestellt habe: "C++23: Deducing This erstellt explizite Zeiger". Dank Deducing This können wir das C und R aus der Abkürzung entfernen.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Das Akronym CRTP steht für das C++ Idiom Curiously Recurring Template Pattern und bezeichnet eine Technik in C++, bei der eine Klasse Derived von einem Klassen-Template Base abgeleitet ist. Der entscheidende Punkt ist, dass Base Derived als Template-Argument besitzt.

template <typename T>
class Base{
    ...
};

class Derived : public Base<Derived>{
    ...
};

CRTP wird typischerweise verwendet, um statischen Polymorphie zu implementieren. Diese findet im Gegensatz zur dynamischen Polymorphie zur Compile-Zeit statt und benötigt keine teure Zeiger-Indirektion.

C++98

Das folgende Programm crtp.cpp stellt eine idiomatische Implementierung von CRTP vor, die auf C++98 basiert.

// crtp.cpp

#include <iostream>

template <typename Derived>
struct Base{                                        
  void interface(){                                 // (2)
    static_cast<Derived*>(this)->implementation();
  }
  void implementation(){                            // (3)
    std::cout << "Implementation Base" << '\n';
  }
};

struct Derived1: Base<Derived1>{
  void implementation(){
    std::cout << "Implementation Derived1" << '\n';
  }
};

struct Derived2: Base<Derived2>{
  void implementation(){
    std::cout << "Implementation Derived2" << '\n';
  }
};

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';
  
}

Das Funktions-Template execute (1) verwendet statische Polymorphie. Die Memberfunktion Base::interface (2) ist der Schlüssel des CRTP-Idioms. Die Member-Funktion leitet den Aufruf 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 Default-Implementierung für die statische Polymorphie der Klasse Derived3 (4). Die folgende Abbildung zeigt die statische Polymorphie in Aktion.

C++23

Dank des expliziten Objektparameters lässt sich das C und das R aus dem Akronym CRTP entfernen.

Das Programm deducingThisCRTP.cpp stellt die C++23-basierte Implementierung von CRTP vor.

// deducingThisCRTP.cpp

#include <iostream>

struct Base{                                            // (1)
  template <typename Self>
  void interface(this Self&& self){
    self.implementation();
  }
  void implementation(){
    std::cout << "Implementation Base" << '\n';
  }
};

struct Derived1: Base{
  void implementation(){
    std::cout << "Implementation Derived1" << '\n';
  }
};

struct Derived2: Base{
  void implementation(){
    std::cout << "Implementation Derived2" << '\n';
  }
};

struct Derived3: Base{};

template <typename T>
void execute(T& base){
    base.interface();                                 // (2)
}

int main(){
  
  std::cout << '\n';
  
  Derived1 d1;                                        // (3)
  execute(d1);
    
  Derived2 d2;                                        // (4)
  execute(d2);
  
  Derived3 d3;                                        // (5)
  execute(d3);
  
  std::cout << '\n';
  
}

Die Parameter des Explicit-Objekts ermöglichen es, den abgeleiteten Typ abzuleiten und ihn perfekt weiterzuleiten (1). Für den konkreten Typ in (2) kommt Derived1 (3), Derived2 (4) und Derived3 (5) zum Einsatz. Folgerichtig wird die entsprechende virtuelle Funktion implementation aufgerufen: std::forward<Self>(self).implementation(). Mit dem aktuellen Microsoft-Compiler lässt sich das Programm bereits ausführen.

Ich habe einen Kommentar zu meinem letzten deutschen Beitrag erhalten, dass ich die anschaulichsten Anwendungen von Deducing This vergessen habe: Rekursive Lambdas. Ehrlich gesagt, bin ich mir nicht so sicher, ob dies der beste Einsatz von Deducing This ist, denn die meisten Programmierer haben Probleme mit der Rekursion. Zweitens bin ich kein Fan von komplizierten Lambdas. Lambdas sollten prägnant und selbstdokumentierend sein.

Nun stelle ich verschiedene Implementierungen einer rekursiv definierten Fakultät-Funktion vor. Danach kann jeder für sich entscheiden, welche Version am einfachsten zu lesen ist.

Jede Funktion berechnet die Fakultät von 10: 3628800.

C++98

In C++98 hat man zwei Möglichkeiten: Entweder man verwendet Template-Metaprogrammierung mit rekursiver Instanziierung oder einen Funktionsaufruf. Das Template-Metaprogramm wird zur Compiletime ausgeführt.

// factorial_cpp98.cpp

#include <iostream>

template <unsigned int N>                                                                 
struct Factorial{
    static int const value = N * Factorial<N-1>::value;
};



template <>                                                                     
struct Factorial<0>{
    static int const value = 1;
};

int factorial(unsigned int n){
    return n > 0 ? n * factorial(n - 1): 1;
}

int main(){
    
    std::cout << '\n';
    
    std::cout << "Factorial<10>::value: " 
      << Factorial<10>::value << '\n'; 
    std::cout << "factorial(10)         " 
      << factorial(10) << '\n';
    
    std::cout << '\n';

}

In the case of the template metaprogram, a full template specialization for the values 2 to 10 is created: https://cppinsights.io/s/b7a2cbd6.

C++11

In C++11 kann die faktorielle Funktion constexpr sein und hat das Potenzial, zur Compiletime ausgeführt zu werden.

// factorial_cpp11.cpp

#include <iostream>

constexpr int factorial(unsigned int n){
    return n > 0 ? n * factorial(n - 1): 1;
}

int main(){
    
    std::cout << '\n';
    
    std::cout << "factorial(10)         " << factorial(10) << '\n';
    
    std::cout << '\n';

}

C++17

Dank constexp if wird abhängig davon unterschiedlicher Code erzeugt, ob N > 0 ist oder nicht. Wie bei dem Template-Metaprogramm in C++11 erzeugt der Compiler vollständig spezialisierte Templates für die Werte 2 bis 10: https://cppinsights.io/s/650b62f0.

// factorial_cpp17.cpp

#include <iostream>

template <unsigned int N>                                             
constexpr int factorial() {
    if constexpr (N > 0) 
        return N * factorial<N - 1>();
    else 
        return 1;
}

int main(){
    
    std::cout << '\n';
    
    std::cout << "factorial<10>() " << factorial<10>() << '\n';
    
    std::cout << '\n';

}

Eine constexpr-Funktion (C++11) hat das Potenzial zur Compiletime, aber eine consteval-Funktion (C++20) muss zur Compiletime ausgeführt werden.

C++20

// factorial_cpp20.cpp

#include <iostream>

consteval int factorial(unsigned int n){
    return n > 0 ? n * factorial(n - 1): 1;
}

int main(){
    
    std::cout << '\n';
    
    std::cout << "factorial(10)         " << factorial(10) << '\n';
    
    std::cout << '\n';

}

Schließlich bin ich in C++23 gelandet. In C++23 kann ein Lambda auf sich selbst verweisen. Das ermöglicht mir, ein rekursives Lambda zu implementieren.

C++23

// factorial_cpp23.cpp

#include <iostream>

auto factorial = [](this auto&& self, unsigned int n) -> int { 
                     return n > 0 ? n * self(n - 1): 1;
                 };

int main(){
    
    std::cout << '\n';
    
    std::cout << "factorial(10)         " << factorial(10) << '\n';
    
    std::cout << '\n';

}

Der MSVC-Compiler unterstützt diese Deducing This noch nicht vollständig. Ich muss daher den Rückgabetyp (-> int) des rekursiven Lambdas angeben. Laut dem Proposal P0847R7 ist dies nicht notwendig.

auto factorial = [](this auto&& self, unsigned int n) { 
                     return n > 0 ? n * self(n - 1): 1;
                 };

Hier ist die Ausgabe des Programms:

Jeder mag eine unterschiedliche Variante der Fakultät-Funktion bevorzugen. Mein Favorit ist die C++20-Version, die auf consteval basiert.

Die C++23-Kernsprache bietet mehr Features an als Deducing This. Genau um diese Features geht es in meinem nächsten Artikel. (rme)