Vorlagen-Metaprogrammierung – Wikipedia

before-content-x4

Vorlagen-Metaprogrammierung ((TMP) ist eine Metaprogrammiertechnik, bei der Vorlagen von einem Compiler verwendet werden, um temporären Quellcode zu generieren, der vom Compiler mit dem Rest des Quellcodes zusammengeführt und dann kompiliert wird. Die Ausgabe dieser Vorlagen umfasst Konstanten zur Kompilierungszeit, Datenstrukturen und vollständige Funktionen. Die Verwendung von Vorlagen kann als Polymorphismus zur Kompilierungszeit betrachtet werden. Die Technik wird von einer Reihe von Sprachen verwendet, von denen die bekannteste C ++, aber auch Curl, D und XL ist.

Die Metaprogrammierung von Vorlagen wurde gewissermaßen versehentlich entdeckt.[1][2]

Einige andere Sprachen unterstützen ähnliche, wenn nicht sogar leistungsfähigere Funktionen zur Kompilierungszeit (z. B. Lisp-Makros), die jedoch nicht in den Geltungsbereich dieses Artikels fallen.

Komponenten der Vorlagen-Metaprogrammierung[edit]

Die Verwendung von Vorlagen als Metaprogrammiertechnik erfordert zwei unterschiedliche Operationen: Eine Vorlage muss definiert und eine definierte Vorlage muss instanziiert werden. Die Vorlagendefinition beschreibt die generische Form des generierten Quellcodes, und die Instanziierung bewirkt, dass aus dem generischen Formular in der Vorlage ein bestimmter Satz von Quellcode generiert wird.

Die Metaprogrammierung von Vorlagen ist Turing-vollständig, was bedeutet, dass jede Berechnung, die von einem Computerprogramm ausgedrückt werden kann, in irgendeiner Form von einem Metaprogramm für Vorlagen berechnet werden kann.[3]

Vorlagen unterscheiden sich von Makros. Ein Makro ist ein Code, der zur Kompilierungszeit ausgeführt wird und entweder eine Textmanipulation des zu kompilierenden Codes durchführt (z. B. C ++ – Makros) oder den vom Compiler erzeugten abstrakten Syntaxbaum (z. B. Rust- oder Lisp-Makros). Textmakros sind insbesondere unabhängiger von der Syntax der zu manipulierenden Sprache, da sie lediglich den speicherinternen Text des Quellcodes unmittelbar vor der Kompilierung ändern.

Vorlagen-Metaprogramme haben keine veränderlichen Variablen – das heißt, keine Variable kann nach der Initialisierung den Wert ändern. Daher kann die Vorlagen-Metaprogrammierung als eine Form der funktionalen Programmierung angesehen werden. Tatsächlich implementieren viele Vorlagenimplementierungen die Flusssteuerung nur durch Rekursion, wie im folgenden Beispiel gezeigt.

Verwenden der Vorlagen-Metaprogrammierung[edit]

Obwohl sich die Syntax der Vorlagen-Metaprogrammierung normalerweise stark von der Programmiersprache unterscheidet, mit der sie verwendet wird, hat sie praktische Anwendungen. Einige häufige Gründe für die Verwendung von Vorlagen sind die Implementierung einer generischen Programmierung (Vermeidung von Codeabschnitten, die bis auf einige geringfügige Abweichungen ähnlich sind) oder die Durchführung einer automatischen Optimierung der Kompilierungszeit, z. B. einmal zur Kompilierungszeit und nicht jedes Mal, wenn das Programm ausgeführt wird. Zum Beispiel, indem der Compiler Schleifen entrollt, um Sprünge und Schleifenzählungsdekremente zu eliminieren, wenn das Programm ausgeführt wird.

Generierung von Klassen zur Kompilierungszeit[edit]

Was genau “Programmieren zur Kompilierungszeit” bedeutet, kann anhand eines Beispiels einer Fakultätsfunktion veranschaulicht werden, die in C ++ ohne Vorlage mithilfe der Rekursion wie folgt geschrieben werden kann:

unsigned int factorial(unsigned int n) {
	return n == 0 ? 1 : n * factorial(n - 1); 
}

// Usage examples:
// factorial(0) would yield 1;
// factorial(4) would yield 24.

Der obige Code wird zur Laufzeit ausgeführt, um den Fakultätswert der Literale 4 und 0 zu bestimmen. Durch Verwendung der Template-Metaprogrammierung und der Template-Spezialisierung als Endbedingung für die Rekursion können die im Programm verwendeten Fakultäten – ohne Berücksichtigung aller nicht verwendeten Fakultäten – zur Kompilierungszeit durch diesen Code berechnet werden:

template <unsigned int n>
struct factorial {
	enum { value = n * factorial<n - 1>::value };
};

template <>
struct factorial<0> {
	enum { value = 1 };
};

// Usage examples:
// factorial<0>::value would yield 1;
// factorial<4>::value would yield 24.

Der obige Code berechnet den Fakultätswert der Literale 4 und 0 zur Kompilierungszeit und verwendet die Ergebnisse so, als wären sie vorberechnete Konstanten. Um Vorlagen auf diese Weise verwenden zu können, muss der Compiler den Wert seiner Parameter zur Kompilierungszeit kennen, was die natürliche Voraussetzung hat, dass Fakultät:: value kann nur verwendet werden, wenn X zur Kompilierungszeit bekannt ist. Mit anderen Worten, X muss ein konstantes Literal oder ein konstanter Ausdruck sein.

In C ++ 11 und C ++ 20 wurden constexpr und consteval eingeführt, damit der Compiler Code ausführen kann. Mit constexpr und consteval kann die übliche rekursive faktorielle Definition mit der Syntax ohne Vorlagen verwendet werden.[4]

Codeoptimierung zur Kompilierungszeit[edit]

Das obige Fakultätsbeispiel ist ein Beispiel für die Codeoptimierung zur Kompilierungszeit, da alle vom Programm verwendeten Fakultäten vorkompiliert und bei der Kompilierung als numerische Konstanten eingefügt werden, wodurch sowohl Laufzeitaufwand als auch Speicherbedarf gespart werden. Es ist jedoch eine relativ geringfügige Optimierung.

Als weiteres, bedeutenderes Beispiel für das Abrollen von Schleifen zur Kompilierungszeit kann die Vorlagen-Metaprogrammierung verwendet werden, um Längen-n Vektorklassen (wo n ist zur Kompilierungszeit bekannt). Der Vorteil gegenüber einer traditionelleren Längen Vektor ist, dass die Schleifen abgewickelt werden können, was zu einem sehr optimierten Code führt. Betrachten Sie als Beispiel den Additionsoperator. Eine Länge-n Vektoraddition könnte geschrieben werden als

template <int length>
Vector<length>& Vector<length>::operator+=(const Vector<length>& rhs) 
{
    for (int i = 0; i < length; ++i)
        value[i] += rhs.value[i];
    return *this;
}

Wenn der Compiler die oben definierte Funktionsvorlage instanziiert, kann der folgende Code erzeugt werden:[citation needed]

template <>
Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs) 
{
    value[0] += rhs.value[0];
    value[1] += rhs.value[1];
    return *this;
}

Das Optimierungsprogramm des Compilers sollte in der Lage sein, das zu entrollen for Schleife, weil der Template-Parameter length ist eine Konstante zur Kompilierungszeit.

Seien Sie jedoch vorsichtig und vorsichtig, da dies zu einem Aufblähen des Codes führen kann, da für jedes ‘N’ (Vektorgröße), mit dem Sie instanziieren, ein separater, nicht gerollter Code generiert wird.

Statischer Polymorphismus[edit]

Polymorphismus ist eine übliche Standardprogrammierfunktion, bei der abgeleitete Objekte als Instanzen ihres Basisobjekts verwendet werden können, die Methoden der abgeleiteten Objekte jedoch wie in diesem Code aufgerufen werden

class Base
{
public:
    virtual void method() { std::cout << "Base"; }
    virtual ~Base() {}
};

class Derived : public Base
{
public:
    virtual void method() { std::cout << "Derived"; }
};

int main()
{
    Base *pBase = new Derived;
    pBase->method(); //outputs "Derived"
    delete pBase;
    return 0;
}

wo alle Anrufungen von virtual Methoden werden diejenigen der am meisten abgeleiteten Klasse sein. Dies dynamisch polymorph Das Verhalten wird (normalerweise) durch die Erstellung virtueller Nachschlagetabellen für Klassen mit virtuellen Methoden erzielt. Diese Tabellen werden zur Laufzeit durchlaufen, um die aufzurufende Methode zu identifizieren. So, Laufzeitpolymorphismus Dies ist zwangsläufig mit einem Ausführungsaufwand verbunden (obwohl bei modernen Architekturen der Aufwand gering ist).

In vielen Fällen ist das erforderliche polymorphe Verhalten jedoch unveränderlich und kann zur Kompilierungszeit bestimmt werden. Dann kann das Curiously Recurring Template Pattern (CRTP) verwendet werden, um dies zu erreichen statischer PolymorphismusDies ist eine Nachahmung des Polymorphismus im Programmcode, wird jedoch zur Kompilierungszeit aufgelöst und beseitigt somit die Suche nach virtuellen Tabellen zur Laufzeit. Zum Beispiel:

template <class Derived>
struct base
{
    void interface()
    {
         // ...
         static_cast<Derived*>(this)->implementation();
         // ...
    }
};

struct derived : base<derived>
{
     void implementation()
     {
         // ...
     }
};

Hier nutzt die Basisklassenvorlage die Tatsache, dass Elementfunktionskörper erst nach ihren Deklarationen instanziiert werden, und verwendet Mitglieder der abgeleiteten Klasse innerhalb ihrer eigenen Elementfunktionen über die Verwendung von a static_cast, also beim Zusammenstellen eine Objektzusammensetzung mit polymorphen Eigenschaften erzeugen. Als Beispiel für die reale Verwendung wird das CRTP in der Boost-Iterator-Bibliothek verwendet.[5]

Eine andere ähnliche Verwendung ist der “Barton-Nackman-Trick”, der manchmal als “eingeschränkte Vorlagenerweiterung” bezeichnet wird, bei dem allgemeine Funktionen in einer Basisklasse platziert werden können, die nicht als Vertrag, sondern als notwendige Komponente zur Durchsetzung von konformem Verhalten bei gleichzeitiger Minimierung verwendet wird Code-Redundanz.

Generierung statischer Tabellen[edit]

Der Vorteil statischer Tabellen besteht darin, dass “teure” Berechnungen durch eine einfache Array-Indizierungsoperation ersetzt werden (Beispiele siehe Nachschlagetabelle). In C ++ gibt es mehrere Möglichkeiten, eine statische Tabelle zur Kompilierungszeit zu generieren. Die folgende Auflistung zeigt ein Beispiel für die Erstellung einer sehr einfachen Tabelle mithilfe rekursiver Strukturen und variabler Vorlagen. Der Tisch hat eine Größe von zehn. Jeder Wert ist das Quadrat des Index.

#include 
#include 

constexpr int TABLE_SIZE = 10;

/**
 * Variadic template for a recursive helper struct.
 */
template<int INDEX = 0, int ...D>
struct Helper : Helper<INDEX + 1, D..., INDEX * INDEX> { };

/**
 * Specialization of the template to end the recursion when the table size reaches TABLE_SIZE.
 */
template<int ...D>
struct Helper<TABLE_SIZE, D...> {
  static constexpr std::array<int, TABLE_SIZE> table = { D... };
};

constexpr std::array<int, TABLE_SIZE> table = Helper<>::table;

enum  {
  FOUR = table[2] // compile time use
};

int main() {
  for(int i=0; i < TABLE_SIZE; i++) {
    std::cout << table[i]  << std::endl; // run time use
  }
  std::cout << "FOUR: " << FOUR << std::endl;
}

Die Idee dahinter ist, dass der struct Helper rekursiv von einer Struktur mit einem weiteren Vorlagenargument erbt (in diesem Beispiel berechnet als INDEX * INDEX), bis die Spezialisierung der Vorlage die Rekursion bei einer Größe von 10 Elementen beendet. Die Spezialisierung verwendet einfach die Liste der variablen Argumente als Elemente für das Array. Der Compiler erzeugt Code ähnlich dem folgenden (entnommen aus clang, das mit -Xclang -ast-print -fsyntax-only aufgerufen wird).

template <int INDEX = 0, int ...D> struct Helper : Helper<INDEX + 1, D..., INDEX * INDEX> {
};
template<> struct Helper<0, <>> : Helper<0 + 1, 0 * 0> {
};
template<> struct Helper<1, <0>> : Helper<1 + 1, 0, 1 * 1> {
};
template<> struct Helper<2, <0, 1>> : Helper<2 + 1, 0, 1, 2 * 2> {
};
template<> struct Helper<3, <0, 1, 4>> : Helper<3 + 1, 0, 1, 4, 3 * 3> {
};
template<> struct Helper<4, <0, 1, 4, 9>> : Helper<4 + 1, 0, 1, 4, 9, 4 * 4> {
};
template<> struct Helper<5, <0, 1, 4, 9, 16>> : Helper<5 + 1, 0, 1, 4, 9, 16, 5 * 5> {
};
template<> struct Helper<6, <0, 1, 4, 9, 16, 25>> : Helper<6 + 1, 0, 1, 4, 9, 16, 25, 6 * 6> {
};
template<> struct Helper<7, <0, 1, 4, 9, 16, 25, 36>> : Helper<7 + 1, 0, 1, 4, 9, 16, 25, 36, 7 * 7> {
};
template<> struct Helper<8, <0, 1, 4, 9, 16, 25, 36, 49>> : Helper<8 + 1, 0, 1, 4, 9, 16, 25, 36, 49, 8 * 8> {
};
template<> struct Helper<9, <0, 1, 4, 9, 16, 25, 36, 49, 64>> : Helper<9 + 1, 0, 1, 4, 9, 16, 25, 36, 49, 64, 9 * 9> {
};
template<> struct Helper<10, <0, 1, 4, 9, 16, 25, 36, 49, 64, 81>> {
  static constexpr std::array<int, TABLE_SIZE> table = {0, 1, 4, 9, 16, 25, 36, 49, 64, 81};
};

Seit C ++ 17 kann dies besser gelesen werden als:

 
#include 
#include 

constexpr int TABLE_SIZE = 10;

constexpr std::array<int, TABLE_SIZE> table = [] { // OR: constexpr auto table
  std::array<int, TABLE_SIZE> A = {};
  for (unsigned i = 0; i < TABLE_SIZE; i++) {
    A[i] = i * i;
  }
  return A;
}();

enum  {
  FOUR = table[2] // compile time use
};

int main() {
  for(int i=0; i < TABLE_SIZE; i++) {
    std::cout << table[i]  << std::endl; // run time use
  }
  std::cout << "FOUR: " << FOUR << std::endl;
}

Um ein komplexeres Beispiel zu zeigen, wurde der Code in der folgenden Liste um einen Helfer für die Wertberechnung (zur Vorbereitung komplizierterer Berechnungen), einen tabellenspezifischen Offset und ein Vorlagenargument für den Typ der Tabellenwerte (z. B. uint8_t,) erweitert. uint16_t, …).

                                                                
#include 
#include 

constexpr int TABLE_SIZE = 20;
constexpr int OFFSET = 12;

/**
 * Template to calculate a single table entry
 */
template <typename VALUETYPE, VALUETYPE OFFSET, VALUETYPE INDEX>
struct ValueHelper {
  static constexpr VALUETYPE value = OFFSET + INDEX * INDEX;
};

/**
 * Variadic template for a recursive helper struct.
 */
template<typename VALUETYPE, VALUETYPE OFFSET, int N = 0, VALUETYPE ...D>
struct Helper : Helper<VALUETYPE, OFFSET, N+1, D..., ValueHelper<VALUETYPE, OFFSET, N>::value> { };

/**
 * Specialization of the template to end the recursion when the table size reaches TABLE_SIZE.
 */
template<typename VALUETYPE, VALUETYPE OFFSET, VALUETYPE ...D>
struct Helper<VALUETYPE, OFFSET, TABLE_SIZE, D...> {
  static constexpr std::array<VALUETYPE, TABLE_SIZE> table = { D... };
};

constexpr std::array<uint16_t, TABLE_SIZE> table = Helper<uint16_t, OFFSET>::table;

int main() {
  for(int i = 0; i < TABLE_SIZE; i++) {
    std::cout << table[i] << std::endl;
  }
}

Was mit C ++ 17 wie folgt geschrieben werden könnte:

#include 
#include 

constexpr int TABLE_SIZE = 20;
constexpr int OFFSET = 12;

template<typename VALUETYPE, VALUETYPE OFFSET>
constexpr std::array<VALUETYPE, TABLE_SIZE> table = [] { // OR: constexpr auto table
  std::array<VALUETYPE, TABLE_SIZE> A = {};
  for (unsigned i = 0; i < TABLE_SIZE; i++) {
    A[i] = OFFSET + i * i;
  }
  return A;
}();

int main() {
  for(int i = 0; i < TABLE_SIZE; i++) {
    std::cout << table<uint16_t, OFFSET>[i] << std::endl;
  }
}

Vor- und Nachteile der Vorlagen-Metaprogrammierung[edit]

Kompromiss zwischen Kompilierungszeit und Ausführungszeit
Wenn viel Template-Metaprogrammierung verwendet wird.
Generische Programmierung
Durch die Metaprogrammierung von Vorlagen kann sich der Programmierer auf die Architektur konzentrieren und die Generierung aller für den Clientcode erforderlichen Implementierungen an den Compiler delegieren. Somit kann die Vorlagen-Metaprogrammierung wirklich generischen Code erzielen, was die Minimierung des Codes und eine bessere Wartbarkeit erleichtert[citation needed].
Lesbarkeit
In Bezug auf C ++ sind die Syntax und die Redewendungen der Vorlagen-Metaprogrammierung im Vergleich zur herkömmlichen C ++ – Programmierung esoterisch, und Vorlagen-Metaprogramme können sehr schwer zu verstehen sein.[6][7]

Siehe auch[edit]

Verweise[edit]

  1. ^ Scott Meyers (12. Mai 2005). Effektives C ++: 55 Spezifische Möglichkeiten zur Verbesserung Ihrer Programme und Designs. Pearson Ausbildung. ISBN 978-0-13-270206-5.
  2. ^ Sehen Geschichte von TMP auf Wikibooks
  3. ^ Veldhuizen, Todd L. “C ++ – Vorlagen sind vollständig”. CiteSeerX 10.1.1.14.3670.
  4. ^ http://www.cprogramming.com/c++11/c++11-compile-time-processing-with-constexpr.html
  5. ^ http://www.boost.org/libs/iterator/doc/iterator_facade.html
  6. ^ Czarnecki, K.; O’Donnell, J.; Striegnitz, J.; Taha, Walid Mohamed (2004). “DSL-Implementierung in Metaocaml, Template-Haskell und C ++” (PDF). Universität Waterloo, Universität Glasgow, Forschungszentrum Julich, Rice University. Die Metaprogrammierung von C ++ – Vorlagen weist eine Reihe von Einschränkungen auf, darunter Portabilitätsprobleme aufgrund von Compilereinschränkungen (obwohl sich diese in den letzten Jahren erheblich verbessert haben), mangelnde Debugging-Unterstützung oder E / A während der Instanziierung von Vorlagen, lange Kompilierungszeiten, lange und unverständliche Fehler, schlechte Lesbarkeit des Codes und schlechte Fehlerberichterstattung.
  7. ^ Sheard, Tim; Jones, Simon Peyton (2002). “Template Meta-Programmierung für Haskell” (PDF). ACM 1-58113-415-0 / 01/0009. Robinsons provokatives Papier identifiziert C ++ – Vorlagen als einen großen, wenn auch zufälligen Erfolg des C ++ – Sprachdesigns. Trotz des extrem barocken Charakters der Meta-Programmierung von Vorlagen werden Vorlagen auf faszinierende Weise verwendet, die über die wildesten Träume der Sprachdesigner hinausgehen. Angesichts der Tatsache, dass Vorlagen funktionale Programme sind, ist es vielleicht überraschend, dass funktionale Programmierer den Erfolg von C ++ nur langsam nutzen konnten
  • Eisenecker, Ulrich W. (2000). Generative Programmierung: Methoden, Werkzeuge und Anwendungen. Addison-Wesley. ISBN 0-201-30977-7.
  • Alexandrescu, Andrei (2003). Modernes C ++ – Design: Generische Programmierung und angewandte Entwurfsmuster. Addison-Wesley. ISBN 3-8266-1347-3.
  • Abrahams, David; Gurtovoy, Aleksey (Januar 2005). Metaprogrammierung von C ++ – Vorlagen: Konzepte, Tools und Techniken von Boost and Beyond. Addison-Wesley. ISBN 0-321-22725-5.
  • Vandevoorde, David; Josuttis, Nicolai M. (2003). C ++ – Vorlagen: Das vollständige Handbuch. Addison-Wesley. ISBN 0-201-73484-2.
  • Clavel, Manuel (2000-10-16). Reflexion in der Umschreibungslogik: Metallogische Grundlagen und Metaprogrammieranwendungen. ISBN 1-57586-238-7.

Externe Links[edit]

after-content-x4