zurück zum Artikel

Template-Metaprogrammierung: Wie es funktioniert

Rainer Grimm

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.

Template Metaprogrammierung: Wie es funktioniert

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.

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.

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

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 sind Werte, die Metafunktionen zur Compilezeit verwenden.

Es gibt drei Arten von Werten:

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

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.

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:

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.

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