Einführung in die Metaprogrammierung in C ++

Zurück 1 2 3 Page 3 Seite 3 von 3
  • Zustandsvariablen: Die Vorlagenparameter
  • Schleifenkonstrukte: Durch Rekursion
  • Wahl der Ausführungspfade: Durch Verwendung von bedingten Ausdrücken oder Spezialisierungen
  • Ganzzahlige Arithmetik

Wenn die Anzahl der rekursiven Instanziierungen und die Anzahl der zulässigen Statusvariablen unbegrenzt sind, reicht dies aus, um alles zu berechnen, was berechenbar ist. Es ist jedoch möglicherweise nicht bequem, dies mithilfe von Vorlagen zu tun. Da die Instanziierung von Vorlagen erhebliche Compilerressourcen erfordert, verlangsamt eine umfassende rekursive Instanziierung einen Compiler schnell oder erschöpft sogar die verfügbaren Ressourcen. Der C ++ - Standard empfiehlt, schreibt jedoch nicht vor, dass mindestens 1.024 Ebenen rekursiver Instanziierungen zulässig sind, was für die meisten (aber sicherlich nicht alle) Metaprogrammieraufgaben für Vorlagen ausreicht.

In der Praxis sollten Vorlagenmetaprogramme daher sparsam verwendet werden. Es gibt jedoch einige Situationen, in denen sie als Werkzeug zur Implementierung praktischer Vorlagen unersetzlich sind. Insbesondere können sie manchmal in den Innereien herkömmlicher Vorlagen versteckt sein, um die Leistung kritischer Algorithmusimplementierungen zu steigern.

Rekursive Instanziierung versus rekursive Vorlagenargumente

Betrachten Sie die folgende rekursive Vorlage:

template struct Doublify {}; template struct Trouble {using LongType = Doublify
   
    ;; }; template struct Trouble {using LongType = double; }; Trouble :: LongType autsch;
   

Die Verwendung von Trouble::LongTypenicht nur löst die rekursiven Instantiierung Trouble, Trouble, ..., Trouble, aber es instanziiert auch Doublifyimmer komplexe Typen über. Die Tabelle zeigt, wie schnell es wächst.

Das Wachstum von Trouble::LongType

 
Geben Sie Alias ​​ein Grundlegender Typ
Trouble::LongType double
Trouble::LongType Doublify
Trouble::LongType Doublify

   Doublify>

Trouble::LongType Doublify

     Doublify>,

  

     Doublify>>

Wie die Tabelle zeigt, Trouble::LongTypewächst die Komplexität der Typbeschreibung des Ausdrucks exponentiell mit N. Im Allgemeinen belastet eine solche Situation einen C ++ - Compiler noch mehr als rekursive Instanziierungen, die keine rekursiven Vorlagenargumente beinhalten. Eines der Probleme hierbei ist, dass ein Compiler eine Darstellung des verstümmelten Namens für den Typ beibehält. Dieser verstümmelte Name codiert in gewisser Weise die genaue Vorlagenspezialisierung, und frühe C ++ - Implementierungen verwendeten eine Codierung, die ungefähr proportional zur Länge der Vorlagen-ID ist. Diese Compiler verwendeten dann weit über 10.000 Zeichen für Trouble::LongType.

Neuere C ++ - Implementierungen berücksichtigen die Tatsache, dass verschachtelte Vorlagen-IDs in modernen C ++ - Programmen ziemlich häufig sind, und verwenden clevere Komprimierungstechniken, um das Wachstum der Namenscodierung erheblich zu reduzieren (z. B. einige hundert Zeichen für Trouble::LongType). Diese neueren Compiler vermeiden es auch, einen verstümmelten Namen zu generieren, wenn tatsächlich keiner benötigt wird, da für die Vorlageninstanz kein Code auf niedriger Ebene generiert wird. Wenn alle anderen Dinge gleich sind, ist es wahrscheinlich vorzuziehen, die rekursive Instanziierung so zu organisieren, dass Vorlagenargumente nicht auch rekursiv verschachtelt werden müssen.

Aufzählungswerte versus statische Konstanten

In den frühen Tagen von C ++ waren Aufzählungswerte der einzige Mechanismus, um „wahre Konstanten“ ( Konstantenausdrücke genannt ) als benannte Mitglieder in Klassendeklarationen zu erstellen . Mit ihnen können Sie beispielsweise ein Pow3Metaprogramm definieren , um Potenzen von 3 wie folgt zu berechnen:

meta / pow3enum.hpp // primäre Vorlage zur Berechnung von 3 zur N-ten Vorlagenstruktur Pow3 {enum {value = 3 * Pow3 :: value}; }; // vollständige Spezialisierung zum Beenden der Rekursionsvorlage struct Pow3 {enum {value = 1}; };

Mit der Standardisierung von C ++ 98 wurde das Konzept der statischen Konstanteninitialisierer in der Klasse eingeführt, sodass das Pow3-Metaprogramm wie folgt aussehen kann:

meta / pow3const.hpp // primäre Vorlage zur Berechnung von 3 zur N-ten Vorlagenstruktur Pow3 {static int const value = 3 * Pow3 :: value; }; // vollständige Spezialisierung zum Beenden der Rekursionsvorlage struct Pow3 {static int const value = 1; };

Diese Version hat jedoch einen Nachteil: Statische Konstanten sind l-Werte. Also, wenn Sie eine Erklärung wie haben

void foo (int const &);

und Sie übergeben es das Ergebnis eines Metaprogramms:

foo (Pow3 :: Wert);

Ein Compiler muss die Adresse von übergeben. Pow3::valueDies zwingt den Compiler, die Definition für das statische Element zu instanziieren und zuzuweisen. Infolgedessen ist die Berechnung nicht mehr auf einen reinen "Kompilierungszeit" -Effekt beschränkt.

Aufzählungswerte sind keine l-Werte (dh sie haben keine Adresse). Wenn Sie sie als Referenz übergeben, wird kein statischer Speicher verwendet. Es ist fast genau so, als hätten Sie den berechneten Wert als Literal übergeben.

In C ++ 11 wurden jedoch constexprstatische Datenelemente eingeführt , die nicht auf integrale Typen beschränkt sind. Sie lösen das oben angesprochene Adressproblem nicht, aber trotz dieses Mangels sind sie heute ein üblicher Weg, um Ergebnisse von Metaprogrammen zu erzielen. Sie haben den Vorteil, dass sie einen korrekten Typ haben (im Gegensatz zu einem künstlichen Aufzählungstyp), und dieser Typ kann abgeleitet werden, wenn das statische Element mit dem automatischen Typspezifizierer deklariert wird. C ++ 17 fügte statische Inline-Datenelemente hinzu, die das oben angesprochene Adressproblem lösen und mit denen verwendet werden können constexpr.

Metaprogrammiergeschichte

Das früheste dokumentierte Beispiel eines Metaprogramms war Erwin Unruh, der damals Siemens im C ++ - Standardisierungsausschuss vertrat. Er bemerkte die rechnerische Vollständigkeit des Vorlageninstanziierungsprozesses und demonstrierte seinen Standpunkt durch die Entwicklung des ersten Metaprogramms. Er benutzte den Metaware-Compiler und brachte ihn dazu, Fehlermeldungen auszugeben, die aufeinanderfolgende Primzahlen enthalten würden. Hier ist der Code, der 1994 auf einer Sitzung des C ++ - Komitees verteilt wurde (so geändert, dass er jetzt auf standardkonformen Compilern kompiliert wird):

meta / unruh.cpp // Primzahlberechnung // (mit Genehmigung des Originals von 1994 von Erwin Unruh geändert) Vorlage
   
     struct is_prime {enum ((p% i) && is_prime2? p: 0), i-1> :: pri); }; Template struct is_prime {enum {pri = 1}; }; Template struct is_prime {enum {pri = 1}; }; Vorlage
    
      struct D { D(void*); }; template
     
       struct CondNull { static int const value = i; }; template struct CondNull { static void* value; }; void* CondNull::value = 0; template
      
        struct Prime_print {
       

// primary template for loop to print prime numbers Prime_print a; enum { pri = is_prime::pri }; void f() { D d = CondNull::value;

// 1 is an error, 0 is fine a.f(); } }; template struct Prime_print {

// full specialization to end the loop enum {pri=0}; void f() { D d = 0; }; }; #ifndef LAST #define LAST 18 #endif int main() { Prime_print a; a.f(); }

If you compile this program, the compiler will print error messages when, in Prime_print::f(), the initialization of d fails. This happens when the initial value is 1 because there is only a constructor for void*, and only 0 has a valid conversion to void*. For example, on one compiler, we get (among several other messages) the following errors:

unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’

Note: As error handling in compilers differs, some compilers might stop after printing the first error message.

The concept of C++ template metaprogramming as a serious programming tool was first made popular (and somewhat formalized) by Todd Veldhuizen in his paper “Using C++ Template Metaprograms.” Veldhuizen’s work on Blitz++ (a numeric array library for C++) also introduced many refinements and extensions to metaprogramming (and to expression template techniques).

Both the first edition of this book and Andrei Alexandrescu’s Modern C++ Design contributed to an explosion of C++ libraries exploiting template-based metaprogramming by cataloging some of the basic techniques that are still in use today. The Boost project was instrumental in bringing order to this explosion. Early on, it introduced the MPL (metaprogramming library), which defined a consistent framework for type metaprogramming made popular also through David Abrahams and Aleksey Gurtovoy’s book C++ Template Metaprogramming.

Additional important advances have been made by Louis Dionne in making metaprogramming syntactically more accessible, particularly through his Boost.Hana library. Dionne, along with Andrew Sutton, Herb Sutter, David Vandevoorde, and others are now spearheading efforts in the standardization committee to give metaprogramming first-class support in the language. An important basis for that work is the exploration of what program properties should be available through reflection; Matúš Chochlík, Axel Naumann, and David Sankel are principal contributors in that area.

John J. Barton and Lee R. Nackman illustrated how to keep track of dimensional units when performing computations. The SIunits library was a more comprehensive library for dealing with physical units developed by Walter Brown. The std::chrono component in the standard library only deals with time and dates, and was contributed by Howard Hinnant.