Introduction à la métaprogrammation en C ++

Précédent 1 2 3 Page 3 Page 3 sur 3
  • Variables d'état: les paramètres du modèle
  • Constructions en boucle: par récursivité
  • Choix des chemins d'exécution: en utilisant des expressions conditionnelles ou des spécialisations
  • Arithmétique entière

S'il n'y a pas de limites à la quantité d'instanciations récursives et au nombre de variables d'état autorisées, cela suffit pour calculer tout ce qui est calculable. Cependant, il peut ne pas être pratique de le faire à l'aide de modèles. En outre, comme l'instanciation de modèle nécessite des ressources de compilateur importantes, une instanciation récursive étendue ralentit rapidement un compilateur ou même épuise les ressources disponibles. La norme C ++ recommande, mais n'impose pas que 1 024 niveaux d'instanciations récursives soient autorisés au minimum, ce qui est suffisant pour la plupart (mais certainement pas toutes) les tâches de métaprogrammation de modèles.

Ainsi, dans la pratique, les métaprogrammes modèles doivent être utilisés avec parcimonie. Il existe cependant quelques situations où ils sont irremplaçables en tant qu'outil pour implémenter des modèles pratiques. En particulier, ils peuvent parfois être cachés dans les entrailles de modèles plus conventionnels pour tirer plus de performances des implémentations d'algorithmes critiques.

Instanciation récursive et arguments de modèle récursifs

Considérez le modèle récursif suivant:

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

L'utilisation de Trouble::LongTypenon seulement déclenche l'instanciation récursive Trouble, Trouble..., Troublemais il instancie également Doublifysur les types de plus en plus complexes. Le tableau montre à quelle vitesse il se développe.

La croissance de Trouble::LongType

 
Tapez Alias Type sous-jacent
Trouble::LongType double
Trouble::LongType Doublify
Trouble::LongType Doublify

   Doublify>

Trouble::LongType Doublify

     Doublify>,

  

     Doublify>>

Comme le montre le tableau, la complexité de la description de type de l'expression Trouble::LongTypeaugmente de façon exponentielle avec N. En général, une telle situation met davantage l'accent sur un compilateur C ++ que les instanciations récursives qui n'impliquent pas d'arguments de modèle récursifs. L'un des problèmes ici est qu'un compilateur conserve une représentation du nom mutilé du type. Ce nom déformé encode la spécialisation exacte du modèle d'une certaine manière, et les premières implémentations C ++ utilisaient un codage qui est à peu près proportionnel à la longueur de l'identifiant du modèle. Ces compilateurs utilisaient alors bien plus de 10 000 caractères pour Trouble::LongType.

Les nouvelles implémentations C ++ prennent en compte le fait que les identifiants de modèle imbriqués sont assez courants dans les programmes C ++ modernes et utilisent des techniques de compression intelligentes pour réduire considérablement la croissance du codage des noms (par exemple, quelques centaines de caractères pour Trouble::LongType). Ces nouveaux compilateurs évitent également de générer un nom mutilé si aucun n'est réellement nécessaire car aucun code de bas niveau n'est réellement généré pour l'instance de modèle. Cependant, toutes choses étant égales par ailleurs, il est probablement préférable d'organiser l'instanciation récursive de telle sorte que les arguments de modèle n'aient pas besoin d'être imbriqués de manière récursive.

Valeurs d'énumération et constantes statiques

Au début du C ++, les valeurs d'énumération étaient le seul mécanisme permettant de créer de «vraies constantes» (appelées expressions constantes ) en tant que membres nommés dans les déclarations de classe. Avec eux, vous pouvez, par exemple, définir un Pow3métaprogramme pour calculer des puissances de 3 comme suit:

meta / pow3enum.hpp // modèle principal pour calculer 3 au Nième modèle struct Pow3 {enum {value = 3 * Pow3 :: value}; }; // spécialisation complète pour terminer le modèle de récursivité struct Pow3 {enum {value = 1}; };

La standardisation de C ++ 98 a introduit le concept d'initialiseurs de constantes statiques en classe, de sorte que le métaprogramme Pow3 puisse ressembler à ceci:

meta / pow3const.hpp // modèle principal pour calculer 3 au Nième modèle struct Pow3 {static int const value = 3 * Pow3 :: value; }; // spécialisation complète pour terminer le modèle de récursivité struct Pow3 {static int const value = 1; };

Cependant, cette version présente un inconvénient: les membres des constantes statiques sont des lvalues. Donc, si vous avez une déclaration telle que

void foo (int const &);

et vous lui passez le résultat d'un métaprogramme:

toto (Pow3 :: valeur);

un compilateur doit passer l' adresse de Pow3::value, et cela force le compilateur à instancier et à allouer la définition pour le membre statique. De ce fait, le calcul n'est plus limité à un pur effet «à la compilation».

Les valeurs d'énumération ne sont pas des valeurs l (c'est-à-dire qu'elles n'ont pas d'adresse). Ainsi, lorsque vous les passez par référence, aucune mémoire statique n'est utilisée. C'est presque exactement comme si vous passiez la valeur calculée comme un littéral.

C ++ 11, cependant, a introduit constexprdes membres de données statiques, et ceux-ci ne sont pas limités aux types intégraux. Ils ne résolvent pas le problème d'adresse soulevé ci-dessus, mais malgré cette lacune, ils constituent désormais un moyen courant de produire des résultats de métaprogrammes. Ils ont l'avantage d'avoir un type correct (par opposition à un type enum artificiel), et ce type peut être déduit lorsque le membre statique est déclaré avec le spécificateur de type automatique. C ++ 17 a ajouté des membres de données statiques en ligne, qui résolvent le problème d'adresse soulevé ci-dessus et peuvent être utilisés avec constexpr.

Historique de la métaprogrammation

Le premier exemple documenté de métaprogramme a été d'Erwin Unruh, alors représentant Siemens au comité de normalisation C ++. Il a noté l'exhaustivité du calcul du processus d'instanciation du modèle et a démontré son point en développant le premier métaprogramme. Il a utilisé le compilateur Metaware et l'a amené à émettre des messages d'erreur qui contiendraient des nombres premiers successifs. Voici le code qui a été diffusé lors d'une réunion du comité C ++ en 1994 (modifié pour qu'il se compile désormais sur des compilateurs conformes aux normes):

meta / unruh.cpp // calcul des nombres premiers // (modifié avec la permission de l'original de 1994 par 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}; }; modèle
    
      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.