C++ Core Guidelines: Programmierung zur Compilezeit

Weiter geht es mit der Einführung zur Programmierung zur Compilezeit. Der letzte Artikel begann mit Template-Metaprogrammierung, deren Erklärung nun zum Abschluss kommt.

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

Weiter geht es mit meiner Einführung zur Programmierung zur Compilezeit. Der letzte Artikel begann mit Template-Metaprogrammierung. Genau hier knüpfe ich an und schließe die Template-Metaprogrammierung ab.

Bevor ich in die Untiefen abtauche, hier ist das große Bild:

Mit den Regeln der C++ Core Guidelines ging meine Einführung zur Programmierung zur Compilezeit los.

Wir befinden uns immer noch auf der untersten Ebene des Dreiecks.

Im letzten Artikel "C++ Core Guidelines: Regel für Template-Metaprogrammierung", stellte ich ein einfaches Template-Metaprogramm vor, das die Konstanz seiner Argumente entfernte:

template<typename T>             // (2)
struct removeConst{
typedef T type; // (5)
};

template<typename T> // (4)
struct removeConst<const T> {
typedef T type; // (5)
};


int main(){

std::is_same<int, removeConst<int>::type>::value; // true (1)
std::is_same<int, removeConst<const int>::type>::value; // true (3)

}

Welche weiteren Erkenntnisse können wir aus dem Programm ziehen?

  • Template-Spezialisierung, sei sie teilweise oder vollständig, ist eine Compilezeit if. Hier möchte ich gerne genauer sein: Wenn ich removeConst mit einer nicht konstanten int (Zeile 1) aufrufe, wendet der Compiler das primäre oder allgemeine Template (Zeile 2) an. Wenn ich eine Konstante int (Zeile 3) verwende, wendet der Compiler die partielle Spezialisierung für const T (Zeile 4) an.
  • Der Ausdruck typedef T type (Zeile 5) dient als Rückgabewert, der in diesem Fall ein Datentyp ist.

Zur Laufzeit kommen Daten und Funktionen zum Einsatz. Zur Compilezeit kommen Metadaten und Metafunktionen zum Einsatz. Das war einfach, denn Programmierung zur Compilezeit nennt sich Metaprogrammierung. Doch was sind genau Metadaten und Metafunktionen? Hier ist eine erste Definition:

  • Metadaten: Datentypen, Nichtdatentypen und Templates, die zur Compilezeit verwendet werden.
  • Metafunktionen: Funktionen, die zur Compilezeit ausgeführt werden.

Beide Begriffe muss ich natürlich genauer erläutern.

Metadaten

Es gibt drei Arten von Metadaten:

  • Datentypen wie int oder double
  • Nichtdatentypen (non-types) wie Ganzzahlen, Aufzähler, Zeiger oder Referenzen
  • Templates wie std::stack

In meinen einfachen Beispielen kommen nur Datentypen und Nicht-Datentypen zum Einsatz.

Metafunktionen

Das klingt erst mal seltsam: Datentypen werden in der Template-Metaprogrammierung verwendet, um Funktionen zu simulieren. Basierend auf meiner Definition von Metafunktionen können constexpr-Funktionen auch zur Compilezeit ausgeführt werden und sind damit auch Metafunktionen. Auf diesen Punkt werde ich in einem späteren Artikel eingehen.

Hier sind zwei Datentypen, die du bereits von meinem letzten Artikel "C++ Core Guidelines: Regel für Template-Metaprogrammierung" kennst: Factorial und RemoveConst:

template <> 
struct Factorial<1>{
static int const value = 1;
};

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

Die erste Metafunktion gibt einen Wert und die zweite einen Datentyp zurück. Die Namen value und type sind lediglich Namenskonventionen für den Rückgabetyp. Wenn eine Metafunktion einen Wert zurückgibt, wird dieser value genannt; falls sie einen Datentyp zurückgibt, type. Die Type-Traits-Bibliothek, auf die ich im nächsten Artikel eingehe, folgt genau dieser Namenskonvention.

Es ist ziemlich aufschlussreich, Funktionen mit Metafunktionen zu vergleichen.

Die folgende power und die Metafunktion Power berechnen 210 zur Laufzeit und zur Compilezeit:

// power.cpp

#include <iostream>

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

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

template<int m> // (2)
struct Power<m, 0>{ // (2)
static int const value = 1; // (4)
};

int main(){

std::cout << std::endl;

std::cout << "power(2, 10)= " << power(2, 10) << std::endl; // (A)
std::cout << "Power<2,10>::value= " << Power<2, 10>::value << std::endl; // (B)

std::cout << std::endl;
}

Dies sind die offensichtlichsten Unterschiede:

  • Argumente: Die Funktionsargumente landen direkt in den runden Klammern ("( ... )" in Zeile A)), die Argumente der Metafunktion sind in den eckigen Klammern ("< ...>" in Zeile B). Das Gleiche gilt für die Definition der Funktion und der Metafunktion. Die Funktion verwendet runde und die Metafunktion spitze Klammern.
  • Rückgabewert: Die Funktion verwendet eine Rückgabeanweisung (Zeile 3) und die Metafunktion die statische, konstante Ganzzahl.

Ich werde meinen Vergleich noch weiter fortsetzen, wenn ich über constexpr-Funktionen schreibe. Hier ist erst einmal die Ausgabe des Programms:

Das war einfach! Oder? power wird zur Laufzeit und Power wird zur Compilezeit ausgeführt. Aber was passiert hier?

// powerHybrid.cpp

#include <iostream>

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

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

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

int main(){

std::cout << std::endl;

std::cout << "power<10>(2): " << power<10>(2) << std::endl; // (1)

std::cout << std::endl;

auto power2 = power<2>; // (2)

for (int i = 0; i <= 10; ++i){
std::cout << "power2(" << i << ")= "
<< power2(i) << std::endl; // (3)
}

std::cout << std::endl;

}

Der Aufruf power<10>(2) in Zeile (1) verwendet spitze und runde Klammern und berechnet 210. Anders ausgedrückt, power ist eine Funktion und eine Metafunktion. Das heißt, 10 ist das Compilezeit- und 2 das Laufzeit-Argument. Jetzt kann ich ein Klassen-Template für 2 aufrufen und ihm den Namen power2 (Zeile 2) geben. CppInsight verrät mir, dass der Compiler das Klassen-Template für 2 instanziiert. Nur das Funktionsargument wird noch nicht gebunden:

Das Funktionsargument ist ein Laufzeitargument und kann daher in einer for-Schleife verwendet werden (Zeile 3):

In meinem nächsten Artikel springe ich Abstraktionsstufe höher in meinen Dreieck. Darin werde ich über die Type-Traits-Bibliothek schreiben. Template-Metaprogrammierung gibt es in C++ seit C++98, die Type-Traits-Bibliothek aber erst seit C++11. Ich denke, du ahnst es bereits. Die Type-Traits-Bibliothek stellt eine kultivierte Form der Template-Metaprogrammierung dar. ()