Introduzione alla metaprogrammazione in C ++

Precedente 1 2 3 Pagina 3 Pagina 3 di 3
  • Variabili di stato: i parametri del modello
  • Costrutti di ciclo: attraverso la ricorsione
  • Scelta dei percorsi di esecuzione: utilizzando espressioni condizionali o specializzazioni
  • Aritmetica intera

Se non ci sono limiti alla quantità di istanze ricorsive e al numero di variabili di stato consentite, ciò è sufficiente per calcolare tutto ciò che è calcolabile. Tuttavia, potrebbe non essere conveniente farlo utilizzando i modelli. Inoltre, poiché la creazione di istanze dei modelli richiede notevoli risorse del compilatore, un'estesa istanza ricorsiva rallenta rapidamente un compilatore o addirittura esaurisce le risorse disponibili. Lo standard C ++ raccomanda ma non impone che siano consentiti almeno 1.024 livelli di istanze ricorsive, il che è sufficiente per la maggior parte (ma certamente non tutte) le attività di metaprogrammazione dei modelli.

Pertanto, in pratica, i metaprogrammi modello dovrebbero essere usati con parsimonia. Ci sono alcune situazioni, tuttavia, in cui sono insostituibili come strumento per implementare modelli convenienti. In particolare, a volte possono essere nascosti nelle viscere di modelli più convenzionali per ottenere maggiori prestazioni dalle implementazioni di algoritmi critici.

Istanziazione ricorsiva contro argomenti modello ricorsivi

Considera il seguente modello ricorsivo:

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

L'uso di Trouble::LongTypenon solo fa scattare l'istanza ricorsiva di Trouble, Trouble, ..., Trouble, ma si crea un'istanza anche Doublifysu tipi di sempre più complesse. La tabella illustra la velocità con cui cresce.

La crescita di Trouble::LongType

 
Digita Alias Tipo di sottostante
Trouble::LongType double
Trouble::LongType Doublify
Trouble::LongType Doublify

   Doublify>

Trouble::LongType Doublify

     Doublify>,

  

     Doublify>>

Come mostra la tabella, la complessità della descrizione del tipo dell'espressione Trouble::LongTypecresce esponenzialmente con N. In generale, una situazione del genere sottolinea un compilatore C ++ ancor più di quanto non facciano istanze ricorsive che non coinvolgono argomenti ricorsivi del modello. Uno dei problemi qui è che un compilatore mantiene una rappresentazione del nome alterato per il tipo. Questo nome alterato codifica in qualche modo l'esatta specializzazione del modello e le prime implementazioni C ++ usavano una codifica che è approssimativamente proporzionale alla lunghezza dell'id del modello. Questi compilatori hanno quindi utilizzato ben oltre 10.000 caratteri per Trouble::LongType.

Le nuove implementazioni C ++ tengono conto del fatto che i template-id annidati sono abbastanza comuni nei moderni programmi C ++ e utilizzano tecniche di compressione intelligenti per ridurre considerevolmente la crescita nella codifica dei nomi (ad esempio, poche centinaia di caratteri per Trouble::LongType). Questi compilatori più recenti evitano anche di generare un nome alterato se non è effettivamente necessario perché non viene effettivamente generato alcun codice di basso livello per l'istanza del modello. Tuttavia, a parità di altre condizioni, è probabilmente preferibile organizzare l'istanza ricorsiva in modo tale che gli argomenti del modello non debbano essere annidati anche ricorsivamente.

Valori di enumerazione rispetto a costanti statiche

Agli albori del C ++, i valori di enumerazione erano l'unico meccanismo per creare "costanti vere" (chiamate espressioni costanti ) come membri denominati nelle dichiarazioni di classe. Con loro, potresti, ad esempio, definire un Pow3metaprogramma per calcolare potenze di 3 come segue:

meta / pow3enum.hpp // template principale per calcolare 3 nell'Nth template struct Pow3 {enum {value = 3 * Pow3 :: value}; }; // specializzazione completa per terminare il modello di ricorsione struct Pow3 {enum {value = 1}; };

La standardizzazione di C ++ 98 ha introdotto il concetto di inizializzatori di costanti statiche in classe, in modo che il metaprogramma Pow3 potesse apparire come segue:

meta / pow3const.hpp // template primario per calcolare 3 sull'Nth template struct Pow3 {static int const value = 3 * Pow3 :: value; }; // specializzazione completa per terminare il modello di ricorsione struct Pow3 {static int const value = 1; };

Tuttavia, c'è uno svantaggio con questa versione: i membri costanti statici sono i valori. Quindi, se hai una dichiarazione come

void foo (int const &);

e gli passi il risultato di un metaprogramma:

foo (Pow3 :: valore);

un compilatore deve passare l' indirizzo di Pow3::valuee questo forza il compilatore a creare un'istanza e allocare la definizione per il membro statico. Di conseguenza, il calcolo non è più limitato a un puro effetto "in fase di compilazione".

Enumeration values aren’t lvalues (that is, they don’t have an address). So, when you pass them by reference, no static memory is used. It’s almost exactly as if you passed the computed value as a literal.

C++ 11, however, introduced constexpr static data members, and those are not limited to integral types. They do not solve the address issue raised above, but in spite of that shortcoming they are now a common way to produce results of metaprograms. They have the advantage of having a correct type (as opposed to an artificial enum type), and that type can be deduced when the static member is declared with the auto type specifier. C++ 17 added inline static data members, which do solve the address issue raised above, and can be used with constexpr.

Metaprogramming history

The earliest documented example of a metaprogram was by Erwin Unruh, then representing Siemens on the C++ standardization committee. He noted the computational completeness of the template instantiation process and demonstrated his point by developing the first metaprogram. He used the Metaware compiler and coaxed it into issuing error messages that would contain successive prime numbers. Here is the code that was circulated at a C++ committee meeting in 1994 (modified so that it now compiles on standard conforming compilers):

meta/unruh.cpp // prime number computation // (modified with permission from original from 1994 by Erwin Unruh) template
   
     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}; }; template
    
      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.