Template Metaprogrammierung: Hybride Programmierung

Hybride Programmierung ist kein offizieller Begriff, betont aber einen interessanten Aspekt von Templates : Den Unterschied zwischen Funktionsargumenten und Templateargumenten.

In Pocket speichern vorlesen Druckansicht 62 Kommentare lesen
Lesezeit: 7 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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:

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?

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.

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.

Wer das vorherige Programm powerHybrid.cpp 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.

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 ein std::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.

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 Randbedingung Power<0>(10) nicht zuschlägt.
  • Potenz<200>(10) verursacht einen int-Ü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

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.

In meinem nächsten Artikel schreibe ich über die Type-Traits-Bibliothek, die Template-Metaprogrammierung in schönen Gewande verkörpert. ()