Zum Inhalt springen

C++-Metaprogrammierung

aus Wikipedia, der freien Enzyklopädie
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 22. Oktober 2015 um 15:38 Uhr durch (Diskussion | Beiträge) (1 Weblink geändert). Sie kann sich erheblich von der aktuellen Version unterscheiden.

C++-Metaprogrammierung bezeichnet die Technik der Metaprogrammierung innerhalb der Programmiersprache C++, also eine Technik, um in C++ Programmcode von anderem Programmcode generieren zu lassen. Dabei kommen vor allem Templates zum Einsatz (in dem Fall spricht man auch von Templatemetaprogrammierung), aber auch Metaprogrammierung mit Hilfe konstanter Ausdrücke sowie mittels sogenannter Präprozessor-Makros.

Funktionsweise

Bei der Templatemetaprogrammierung macht man sich zunutze, dass Templates während des Kompilierens ausgewertet werden. So kann man Code schreiben, der zur Übersetzungszeit ausgewertet wird und erst den eigentlichen Code generiert. Dabei verlängert sich zwar die Dauer des Kompilierens, aufgrund durch die Metaprogrammierung optimierte Programmpfade kann es aber zu einer Verkürzung der Laufzeit kommen.

Die Templatemetaprogrammierung ist eine Programmiertechnik, die für C++ intensiv erforscht und ausentwickelt wurde. So gibt es zum Beispiel eine Implementierung eines Lisp-Derivats[1] oder mit Spirit einen mit Hilfe von C++-Templates realisierten Parsergenerator.[2]

Krzysztof Czarnecki und Ulrich W. Eisenecker gelten als die Vordenker dieser Technik. Herausragende Arbeiten zur C++-Metaprogrammierung stammen von Andrei Alexandrescu, die er besonders mit seinem Buch Modernes C++ Design – Generische Programmierung und Entwurfsmuster angewendet bekannt machte.

Die Templatemetaprogrammierung in C++ ist Turing-vollständig, was bedeutet, dass jeder Algorithmus durch Template-Metaprogrammierung umgesetzt werden kann. Gleichzeitig folgt daraus, dass es keinen korrekten C++-Compiler geben kann, der jedes C++ Programm übersetzt, beziehungsweise nicht mehr allgemein entscheidbar ist, ob ein C++ Programm korrekt ist.

In der Templatemetaprogrammierung gibt es keine veränderlichen Variablen, d. h. einmal mit einem bestimmten Wert initialisierte Elemente behalten ihren Wert für immer. Interessanterweise ist eine Konsequenz daraus, dass C++-Template-Metaprogramme generell – anders als C++-Laufzeitprogramme – eine Form der rein funktionalen Programmierung darstellen. Der Kontrollfluss erfolgt in Templatemetaprogrammen deshalb mit Hilfe von Rekursion.

Beispiele

Potenzberechnung mit Hilfe von Templates

Das folgende Beispiel berechnet die Potenz für positive Exponenten:

template<int B, unsigned int E>
struct potenz {
  enum { value = B * potenz<B, E - 1>::value };
};

template<int B>
struct potenz<B, 0> {
  enum { value = 1 };
};

const int P = potenz<10, 3>::value;

Erläuterung: Das Template potenz instanziiert sich selbst rekursiv, wobei der Exponent mit jedem Rekursionsschritt um 1 reduziert wird. Das Template besitzt eine sogenannte Spezialisierung für den Fall, dass der Exponent 0 ist und liefert in dem Fall das Ergebnis 1 zurück, diese Spezialisierung nimmt die Rolle der Abbruchbedingung der Rekursion ein.

Also lässt sich die Struktur des Codes als Pseudocode

P(B, E) := B * P(B, E-1)
P(B, 0) := 1

beschreiben.

Potenzberechnung mit Hilfe konstanter Ausdrücke

Das folgende Beispiel berechnet ebenfalls die Potenz, in diesem Fall mit Hilfe verallgemeinerter konstanter Ausdrücke:

constexpr int potenz(int basis, unsigned int exp) {
  return (exp==0)? 1 : basis * potenz(basis, exp-1);
}
const int P = potenz(10, 3);

Erläuterung: Aufrufe einfacher Funktionen können zur Übersetzungszeit durchgeführt und in konstanten Ausdrücken verwendet werden. Die aufzurufende Funktion muss dafür mit constexpr versehen sein. Dies ist eine Neuerung, die in C++11, der Revision der internationalen ISO-Norm von C++ aus dem Jahr 2011, eingeführt wurde.

Verwendete Konstrukte

Klassentemplates nehmen in der C++-Templatemetaprogrammierung die Rolle von Funktionen zur Laufzeit ein. Sie können Typen und konstante Werte, einschließlich Verweise auf Funktionen, als Parameter entgegennehmen und mittels eines typedef einen Typ oder mittels eines enum oder einer Konstanten einen Wert speichern, der damit die Rolle eines Rückgabetyps einnimmt. Spezialisierungen von Templates entsprechen Verzweigungen und ermöglichen auf diese Weise die Steuerung des Programmflusses.

Ausgehend davon lassen sich komplexe Funktionen und Datenstrukturen implementieren, die zur Übersetzungszeit ausgewertet werden. Solche können verwendet werden, um etwa Klassenstrukturen zu erzeugen und damit etwa die Umsetzung gewisser Entwurfsmuster zu vereinfachen, oder um Funktionen zu synthetisieren.

Bibliotheken wie Boost oder Loki implementieren bestimmte grundlegende Konstrukte, die solche Metaprogrammierung erleichtern, etwa if- oder fold-ähnliche Konstrukte zur Übersetzungszeit oder etwa Datenstrukturen.

Typlisten

Sogenannte Typlisten stellen ein einfaches Beispiel für eine mittels Templates zur Übersetzungszeit definierte Datenstruktur dar. Eine typische Implementierung sieht wie folgt aus:

struct NullType;
template<typename H, class T>
struct TypeList
{
  typedef H Head;
  typedef T Tail;
};
typedef TypeList< Type1, TypeList< Type2, TypeList< Type3, NullType > > > MyList;

Typlisten können mittels Templates verarbeitet werden, dafür muss jedoch das Ende durch einen 'Null-Typ' gekennzeichnet werden.

Es ist sehr aufwändig, eine Funktionalität auf Basis von Typlisten zu entwickeln. Mit entsprechenden Grundfunktionen ist jedoch eine elegante Nutzung möglich. Die Problematik in der Verwendung ergibt sich daraus, dass die Möglichkeiten zur Metaprogrammierung erst nachträglich entdeckt wurden und für sie keinerlei speziell entworfenen Sprachkonstrukte existieren.

Verarbeitung von Typlisten

In C++ gibt es keine einfache Zugriffsmöglichkeit auf die Elemente von Typlisten. Soll eine Typliste verarbeitet werden, so muss jede Iteration in einem separaten Funktionsaufruf (mit Tail als Template-Parameter) oder über die Instanziierung eines Klassentemplates für jede Iteration. Typischerweise terminiert die Abarbeitung durch eine Spezialisierung für den Null-Typ.

Variadische Templates

In C++11 haben variadische Templates, also Templates mit einer beliebigen Parameterzahl, Einzug gehalten. Eine solche Funktionalität lässt sich zwar bereits mit Typlisten realisieren, wie etwa in Loki und Boost, die in die Programmiersprache integrierte Unterstützung variadischer Templates bietet aber den Vorteil wesentlich kürzerer Übersetzungszeiten, da das Verarbeiten von Typlisten mit sehr hohem Aufwand verbunden ist. Anstelle der Parameter wählt man hierbei einen einzigen Tupel-Parameter. Die Abarbeitung muss auch mit variadischen Templates über rekursive Instanziierung ablaufen, sodass strukturell starke Ähnlichkeit besteht.

Tupel

Ein elementares Beispiel für die Erzeugung von Datenstrukturen mittels Metaprogrammierung stellen Tupel dar. Diese lassen sich mit variadischen Templates (oder vormals Typlisten) realisieren. Hierfür legt man üblicherweise ein Klassen-Template an, das beliebig viele Typen als Parameter übernimmt und für jeden Typ ein Feld dieses Typs enthält. In C++ muss dies wiederum auf rekursive Art und Weise geschehen. Beispielsweise kann ein Tupel von einem Tupel mit einem Element weniger erben. Anschließend lassen sich Operationen wie Indexzugriffe implementieren, die während der Übersetzung stattfinden müssen, oder aber Operationen wie Vergleiche und Hashes synthetisieren, die zur Laufzeit auf den Tupeln angewandt werden können.

Vor- und Nachteile der Templatemetaprogrammierung

  • Abwägung zwischen Übersetzungszeit und Ausführungszeit: Da der gesamte Template-Quelltext während der Übersetzung verarbeitet, ausgewertet und eingesetzt wird, dauert die Übersetzung insgesamt länger, während der ausführbare Code dadurch an Effizienz gewinnen kann. Obwohl dieser Zusatzaufwand im Allgemeinen sehr gering ausfällt, kann er auf große Projekte oder Projekte, in denen intensiv Templates eingesetzt werden, großen Einfluss auf die Dauer der Übersetzung haben.
  • Generische Programmierung: Templatemetaprogrammierung ermöglicht eine höhere Abstraktion. Daher kann Templatemetaprogrammierung zu kürzerem Quelltext und besserer Wartbarkeit führen.
  • Lesbarkeit: Verglichen mit konventioneller C++-Programmierung wirken Syntax und Schreibweisen der Templatemetaprogrammierung zunächst ungewohnt. Fortgeschrittene oder sogar die meiste nicht-triviale Templatemetaprogrammierung kann daher schwer zu verstehen sein. Dadurch können Metaprogramme von Programmierern, die in Templatemetaprogrammierung unerfahren sind, schwer zu pflegen sein, insbesondere entspricht die rein funktionale Struktur nicht der üblichen Struktur von C++. Letzteres hängt allerdings auch davon ab, wie die Templatemetaprogrammierung im speziellen Fall umgesetzt wurde.
  • Schlechte Unterstützung durch Entwicklungswerkzeuge: In den bestehenden Entwicklungswerkzeugen ist es nicht möglich, die Metagenerierung schrittweise zu verfolgen. Aufgrund fehlender Sprachmittel ist es bislang auch schwierig, sinnvolle Fehlermeldungen für die Metaprogrammierung zu erzeugen. Die meisten Compiler geben Fehlermeldungen aus, aus denen sich nur schwer auf den eigentlichen Fehler schließen lässt.

Literatur

Einzelnachweise

  1. Metalisp (Memento vom 4. Februar 2003 im Internet Archive)
  2. Boost Spirit