Template-Metaprogrammierung: Wie es funktioniert

Modernes C++ Rainer Grimm  –  23 Kommentare

Nach dem Beitrag über die Ursprünge der Template-Metaprogrammierung geht es darum, wie Template-Metaprogrammierung verwendet werden kann, um Typen zur Compilezeit zu verändern.

Template Metaprogrammierung: Wie es funktioniert

Das faktorielle Programm im letzten Artikel "Template Metaprogrammierung: Wie alles begann" war ein schönes Beispiel, aber nicht idiomatisch für Template Metaprogrammierung. Die Manipulation von Typen zur Compilezeit ist typisch für die Template-Metaprogrammierung.

Typmanipulation zur Compilezeit

Hier ist ein Beispiel, wie std::move konzeptionell umgesetzt ist:

static_cast<std::remove_reference<decltype(arg)>::type&&>(arg);

std::move nimmt sein Argument arg an, leitet seinen Typ ab (decltype(arg)), entfernt seine Referenz (std::remove_reverence) und wandelt sie in eine Rvalue-Referenz um (static_cast<...>::type&&>). Im Wesentlichen ist std::move eine Konvertierung zu einer Rvalue-Referenz. Jetzt kann die Move-Semantik zum Einsatz kommen.

Wie kann eine Funktion die Konstante aus ihrem Argument entfernen?

// removeConst.cpp

#include <iostream>
#include <type_traits>

template<typename T >
struct removeConst {
using type = T; // (1)
};

template<typename T >
struct removeConst<const T> {
using type = T; // (2)
};

int main() {

std::cout << std::boolalpha;
std::cout << std::is_same<int, removeConst<int>::type>::value
<< '\n'; // true
std::cout << std::is_same<int, removeConst<const int>::type>::value
<< '\n'; // true

}

Ich habe removeConst so implementiert, wie die Funktion wohl in der Typ-Traits Bibliothek implementiert ist. std::is_same aus der Typ-Traits Bibliothek hilft mir, zur Compilezeit zu entscheiden, ob die beiden Typen gleich sind. Im Falle von removeConst<int> greift das primäre oder allgemeine Klassen-Template; im Fall von removeConst<const int> kommt die partielle Spezialisierung für const T zum Einsatz. Entscheidend ist, dass beide Klassen-Templates den zugrunde liegenden Typ in (1) und (2) über den Alias-Typ zurückgeben. Wie versprochen, wird die Konstante des Arguments entfernt.

Es gibt noch weitere interessante Beobachtungen.

  • Template-Spezialisierung (teilweise oder vollständig) ist eine bedingte Ausführung zur Compilezeit. Genauer: Wenn ich removeConst mit einem nicht konstanten int verwende, wählt der Compiler das primäre oder allgemeine Template aus. Wenn ich einen konstanten int verwende, wählt der Compiler die partielle Spezialisierung für const T aus.
  • Der Ausdruck type = T dient als Rückgabewert, der in diesem Fall ein Typ ist.
  • Wer das Programm removeConst.cpp auf C++ Insights studiert, sieht, dass der Ausdruck std::is_same<int, removeConst<int>::type>::value auf den booleschen Wert std::integral_constant<bool, true>::value hinausläuft, der als true angezeigt wird.

Ich trete einen Schritt zurück, um über Template-Metaprogrammierung aus konzeptioneller Sicht schreiben.

Mehr Meta

Zur Ausführungszeit verwenden wir Daten und Funktionen. Zur Compilezeit verwenden wir Metadaten und -funktionen. Es ist naheliegend, dass dies Meta heißt, weil wir Metaprogrammierung betreiben.

Metadaten

Metadaten sind Werte, die Metafunktionen zur Compilezeit verwenden.

Es gibt drei Arten von Werten:

  • Typen wie int oder double
  • Nichttypen wie Integrale, Aufzählungszeichen, Zeiger, Referenzen, Fließkommazahlen mit C++20
  • Template wie std::vector oder std::deque

Mehr über die drei Arten von Werten steht in meinem vorherigen Artikel "Alias Templates und Template Parameter".

Metafunktionen

Metafunktionen sind Funktionen, die zur Compilezeit ausgeführt werden.

Zugegeben, das klingt seltsam: Typen werden in der Template-Metaprogrammierung verwendet, um Funktionen zu simulieren. Ausgehend von der Definition von Metafunktionen sind auch constexpr-Funktionen, die zur Compilezeit ausgeführt werden können, Metafunktionen. Die gleiche Argumentation gilt für consteval-Funktionen in C++20.

Hier sind zwei Metafunktionen.

template <int a , int b>
struct Product {
static int const value = a * b;
};

template<typename T >
struct removeConst<const T> {
using type = T;
};

Die erste Metafunktion Product gibt einen Wert zurück und die zweite Funktion removeConst einen Typ. Die Namen value und type sind Namenskonventionen für die Rückgabewerte. Wenn eine Metafunktion einen Wert zurückgibt, wird dieser value genannt; wenn sie einen Typ zurückgibt, wird dieser type genannt. Die Typ-Traits Bibliothek folgt genau dieser Namenskonvention.

Es ist recht aufschlussreich, Funktionen mit Metafunktionen zu vergleichen.

Funktionen versus Meta-Funktionen

Die folgende Funktion power und die Metafunktion Power berechnen pow(2, 10) zur Laufzeit und zur Compilezeit.

// power.cpp

#include <iostream>

int power(int m, int n) {
int r = 1;
for(int k = 1; k <= n; ++k) r *= m;
return r;
}

template<int m, int n>
struct Power {
static int const value = m * Power<m, n-1>::value;
};

template<int m>
struct Power<m, 0> {
static int const value = 1;
};

int main() {

std::cout << '\n';

std::cout << "power(2, 10)= " << power(2, 10) << '\n';
std::cout << "Power<2,10>::value= " << Power<2, 10>::value << '\n';

std::cout << '\n';
}

Dies ist der Hauptunterschied:

  • Argumente: Die Funktionsargumente kommen in den runden Klammern (( ... )) und die Meta-Funktionsargumente in den spitzen Klammern (< ...>) zum Einsatz. Diese Beobachtung gilt auch für die Definition der Funktion und der Metafunktion. Die Funktion verwendet runde Klammern und die Metafunktion spitze Klammern. Jedes Argument der Metafunktion erzeugt einen neuen Typ.
  • Rückgabewert: Die Funktion verwendet eine Rückgabeanweisung und die Metafunktion einen statischen ganzzahligen konstanten Wert.

Auf diesen Vergleich gehe ich in einem weiteren Artikel über constexpr- und consteval-Funktionen näher ein. Hier ist die Ausgabe des Programms.

Template Metaprogrammierung: Wie es funktioniert

power wird zur Laufzeit und Power zur Compilezeit ausgeführt, aber was passiert im folgenden Beispiel?

// powerHybrid.cpp

#include <iostream>

#include <iostream>

template<int n>
int Power(int m){
return m * Power<n-1>(m);
}

template<>
int Power<0>(int m){
return 1;
}

int main() {

std::cout << '\n';

std::cout << "Power<0>(10): " << Power<0>(20) << '\n';
std::cout << "Power<1>(10): " << Power<1>(10) << '\n';
std::cout << "Power<2>(10): " << Power<2>(10) << '\n';


std::cout << '\n';

}

Die Frage ist natürlich: Ist Power eine Funktion oder eine Metafunktion? Ich verspreche, die Antwort auf diese Frage gibt mehr Aufschluss.

Wie geht's weiter?

In meinem nächsten Artikel analysiere ich die Funktion/Metafunktion Power und stelle die Typ-Traits Bibliothek genauer vor. Die Typ-Traits-Bibliothek ist idiomatisch für die Compilezeitprogrammierung in C++.