Template-Metaprogrammierung: Wie es funktioniert
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.
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.

Das faktorielle Programm im letzten Artikel "Template Metaprogrammierung: Wie alles begann [1]" 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 [2]
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 [3] 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 konstantenint
verwende, wählt der Compiler das primäre oder allgemeine Template aus. Wenn ich einen konstantenint
verwende, wählt der Compiler die partielle Spezialisierung fürconst T
aus. - Der Ausdruck
type = T
dient als Rückgabewert, der in diesem Fall ein Typ ist. - Wer das Programm removeConst.cpp [4] auf C++ Insights studiert, sieht, dass der Ausdruck
std::is_same<int, removeConst<int>::type>::value
auf den booleschen Wertstd::integral_constant<bool, true>::value
hinausläuft, der alstrue
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
oderdouble
- Nichttypen wie Integrale, Aufzählungszeichen, Zeiger, Referenzen, Fließkommazahlen mit C++20
- Template wie
std::vector
oderstd::deque
Mehr über die drei Arten von Werten steht in meinem vorherigen Artikel "Alias Templates und Template Parameter [5]".
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 [6] 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.

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 [7] genauer vor. Die Typ-Traits-Bibliothek ist idiomatisch für die Compilezeitprogrammierung in C++.
( [8])
URL dieses Artikels:
https://www.heise.de/-6237233
Links in diesem Artikel:
[1] https://heise.de/-6233576
[2] https://en.cppreference.com/w/cpp/utility/move
[3] https://en.cppreference.com/w/cpp/header/type_traits
[4] https://cppinsights.io/s/c9b121d0
[5] https://heise.de/-6062972
[6] https://en.cppreference.com/w/cpp/header/type_traits
[7] https://en.cppreference.com/w/cpp/header/type_traits
[8] mailto:rainer@grimm-jaud.de
Copyright © 2021 Heise Medien