Template Metaprogrammierung: Hybride Programmierung
Hybride Programmierung ist kein offizieller Begriff, betont aber einen interessanten Aspekt von Templates : Den Unterschied zwischen Funktionsargumenten und Templateargumenten.
- Rainer Grimm
Hybride Programmierung ist kein offizieller Begriff. Ich habe ihn erfunden, um einen interessanten Aspekt von Templates zu betonen: Den Unterschied zwischen Funktionsargumenten und Templateargumenten.
Meinen letzten Artikel "Template-Metaprogrammierung: Wie es funktioniert" beendete ich mit einem Rätsel. Zur Erinnerung, hier ist das Rätsel:
Das Rätsel
Die Funktionen power
und Power berechnen pow(2, 10). power
wird zur Laufzeit ausgeführt und Power
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';
}
Mehr Details zu beiden Funktionen finden sich in meinem vorherigen Artikel "Template-Metaprogrammierung - Wie es funktioniert.
So weit, so gut, aber was passiert in folgendem Beispiel?
// powerHybrid.cpp
#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';
}
Wie erwartet, erledigt Power
seine Aufgabe zuverlässig.
Hier ist das Rätsel in Kurzform: Ist Power
eine Funktion oder eine Metafunktion?
Hybride Programmierung
Die Aufrufe Power<0>(10)
, Power<1>(10)
und Power<2>(10)
verwenden spitze und runde Klammern und potenzieren 10 mit 0, 1 und 2. Das heißt, 0, 1 und 2 sind Compilezeit-Argumente und 10 ist ein Laufzeit-Argument. Anders ausgedrückt heißt dies: Potenz
ist gleichzeitig eine Funktion und eine Metafunktion. Ich möchte auf diesen Punkt gerne genauer eingehen.
Power zur Laufzeit
Zunächst kann ich Power
für 2 instanziieren, ihr den Namen Power2of
geben und sie in einer for-Schleife verwenden.
// powerHybridRuntime.cpp
#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';
auto Power2of = Power<2>;
for (int i = 0; i <= 20; ++i) {
std::cout << "Power2of(" << i << ")= "
<< Power2of(i) << '\n';
}
std::cout << '\n';
}
Power2of
ermöglicht es, die Quadrate von 0 ... 20 zur Laufzeit zu berechnen.
Natürlich kann man Power nicht mit verschiedenen Template-Argumenten in der for-Schleife aufrufen. Die Instanziierung eines Templates erfordert einen konstanten Ausdruck. Um es kurz zu machen: Die folgende Anwendung von Power führt zu einem Compilierfehler, der besagt, dass "the value of 'i' is not usable in a constant expression".
for (int i = 0; i <= 20; ++i) {
std::cout << "Power<" << i << ">(2)= " << Power<i>(2) << '\n';
}
Es gibt einen sehr interessanten Unterschied zwischen einer Funktion und einer Metafunktion.
Power zur Compilezeit
Wer das vorherige Programm powerHybrid.cp
p in C++ Insights untersucht, sieht, dass jeder Einsatz von Power
mit einem anderen Template-Argument einen neuen Typ erzeugt.
Das bedeutet, dass der Aufruf von Power<2>(10
) die rekursive Template-Instanziierung für Power<1>(10)
und Power<0>(10)
bewirkt. Hier ist die Ausgabe von C++ Insights.
Um meine Beobachtung zusammenzufassen: Jede Template-Instanziierung erzeugt einen neuen Typ.
Neue Typen erstellen
Wer ein Template wie Power
, std::vector
oder std::array
verwendet, kann es mit zwei Arten von Argumenten aufrufen: Funktionsargumente und Template-Argumente. Die Funktionsargumente stehen in runden Klammern (( ... )
) und die Template-Argumente stehen in spitzen Klammern (<...>
). Mit den Template-Argumenten werden neue Typen erstellt. Oder andersherum formuliert. Man kann Templates auf zwei Arten parametrisieren: zur Compilezeit mit spitzen Klammern (<...>
) und zur Laufzeit mit runden Klammern (( ... )
).
auto res1 = Power<2>(10); // (1)
auto res2 = Power<2>(11); // (2)
auto rest3 = Power<3>(10); // (3)
std::vector<int> myVec1(10); // (1)
std::vector<int> myVec2(10, 5); // (2)
std::vector<double> myDouble(5); // (3)
std::array<int, 3> myArray1{ 1, 2, 3}; // (1)
std::array<int, 3> myArray2{ 1, 2, 3}; // (2)
std::array<double, 3> myArray3{ 1.1, 2.2, 3.3}; // (3)
- (1) erstellt eine neue Power-Instanz, einen
std::vector
der Länge 10 oder einstd::array
mit drei Elementen - (2) verwendet die bereits erstellten Typen aus den vorherigen Zeilen (1) wieder
- (3) erstellt einen neuen Typ
Ein paar meiner deutschen Leser haben mich bereits darauf hingewiesen: Meine Metafunktion Power
hat eine große Schwachstelle.
Die große Schwachstelle
Wenn ich Power mit einer negativen oder einer zu großen Zahl instanziiere, kommt es zu undefiniertem Verhalten.
Power<-1>(10)
verursacht eine unendliche Template-Instanziierung, weil die RandbedingungPower<0>(10)
nicht zuschlägt.Potenz<200>(10)
verursacht einenint
-Überlauf.
Das erste Problem kann durch die Verwendung eines static_assert
innerhalb der Power
-Templates behoben werden: static_assert(n >= 0, "exponent must be >= 0");
. Für das zweite Problem gibt es keine einfache Lösung.
// powerHybridRuntimeOverflow.cpp
#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';
auto Power10of = Power<10>;
for (int i = 0; i <= 20; ++i) {
std::cout << "Power10of(" << i << ")= "
<< Power10of(i) << '\n';
}
std::cout << '\n';
}
Der Überlauf beginnt mit Power10of
(9). pow(9, 10) ist 3.486.784.40
Was ich noch sagen wollte ...
Am Ende dieser drei Artikel "Template Metaprogrammierung - Wie alles begann", "Template-Metaprogrammierung: Wie es funktioniert" über Template Metaprogramming möchte ich einen Haftungsausschluss aussprechen. Ich möchte nicht, dass jemand zur Compilezeit mit Templates programmiert. Die meiste Zeit ist constexpr
(C++11) oder consteval
(C++20) die deutlich bessere Wahl.
Ich habe die Template-Metaprogrammierung aber aus zwei Gründen erklärt.
- Die Template-Metaprogrammierung hilft dir, Templates und den Prozess der Template-Instanziierung besser zu verstehen.
- Die Type-Traits-Bibliothek wendet die Idee der Template-Metaprogrammierung an und nutzt deren Konventionen.
Wie geht's weiter?
In meinem nächsten Artikel schreibe ich über die Type-Traits-Bibliothek, die Template-Metaprogrammierung in schönen Gewande verkörpert. ()