[Meta-prog] Les templates-Expressions

Les templates-Expressions [Meta-prog] - C++ - Programmation

Marsh Posté le 29-07-2003 à 09:16:14    

Voila, apres le premier post sur la MPT, je récidive avec cette fois quelque chose de plus burné : Les Expressions Templates.
 
Tout d'abord je tiens à signaler que ce post est une tentative d'adaptation des articles de Todd Veldhuizen, ze Boss en MPT, créateur de la librairie Blitz++ en autre.
 
Note : Cette explication fait partie de mes travaux de DEA :D
 
1. Principes
Considérons une fonction qui cacule l'intégrale d'une fonction mathématique.
Un moyen "classique" de l'implementer est d'utiliser un pointeur de fonction :
 

Code :
  1. double integrate( double (*f)(double), double inf, double sup )
  2. {
  3.     double r=0;
  4.     static const double dx = 0.001
  5.     for( double i=inf;i<sup;i+=dx;)  r += dx*f(i);
  6.     return r;
  7. }


 
Simple, efficace, pas de problème. SAUF que si cette fonction est une part critique de votre application, le déréférencement de pointeur va peut etre commencer à devenir tres tres handicapant.
 
Il serait agréable de pouvoir ecrire quelque chose comme :  
 

Code :
  1. Data<double> x;
  2. double r = integrate( 1+1/x,0,10);

 
 
et de voir se dérouler le code de l'intégration :
 

Code :
  1. double r = 0;
  2. for( double i=inf;i<sup;i+=dx;)  r += dx*(1+1/i);


 
Comment faire ?
 
2. Une Solution Elégante : Les Expressions templates
 
Lorsque l'on manipule ce genre d'expression, on peut considérer qu'elles sont l'application d'une grammaire restreinte, contenant juste des valeurs, des opérateurs et des expressions. A partir de la on peut reconstruire un ensemble de classe pour réécrir ces expressions :
 
Il nous faut d'abord une classe pour représenté les variables et les constantes
 

Code :
  1. class Variable
  2. {
  3.    public :
  4.    Variable() {}
  5.    double eval( double x ) const { return x; }
  6. };
  7. class Constante
  8. {
  9.    public :
  10.    Constante( const double& v) : val(v){}
  11.    double eval( double ) { return val; }
  12.    private :
  13.    double val;
  14. };

 
 
Ensuite, il nous font pouvoir coder les divers opérateurs d'une expression
arithmétique. Cantonnons nous au opérateurs bianires de bases : +,-,/,*.
Qu'est-ce qu'un operateur binaire : c'est une opération ET un noeud d'una rbre de syntaxe abstraite ayant deux fils. Ces deux aspects de l'opérateurs se retrouvent dans deux familles de classes :
 

Code :
  1. // classe représentant la caractére binaire d'un opérateur
  2. template<class L,class R,class OP>
  3. class BinaryNode
  4. {
  5.   public :
  6.   BinaryNode( const L& l, const R& r ) : left(l), right(r) {}
  7.   double eval( double x ) { return OP::eval( left.eval(x), right.eval(x));}
  8.   private:
  9.   L left;
  10.   R right;
  11. };
  12. // classes représentant les opérations des opérateurs
  13. struct func_plus
  14. {
  15.     static double eval( double x, double y) { return x+y; }
  16. };
  17. struct func_moins
  18. {
  19.     static double eval( double x, double y) { return x-y; }
  20. };
  21. struct func_fois
  22. {
  23.     static double eval( double x, double y) { return x*y; }
  24. };
  25. struct func_divise
  26. {
  27.     static double eval( double x, double y) { return x/y; }
  28. };


 
Il nous reste à coder une classe pour encapsuler une expression quelconque :
 

Code :
  1. template<class E>
  2. class Expression
  3. {
  4.   public :
  5.   Expression( const E& expr) : xpr(expr) {}
  6.   double eval( double x) { return xpr.eval(x); }
  7.    private :
  8.    E xpr;
  9. };


 
Voila, nous avaons toute nos briques. Comment alors reconstruire à partir de ces classes une expression arithmétique et l'évaluez en ligne ?
 
Trés simplement, nous allons redéfinir les opérateurs +,-,*,/ sur les types Variable, Constante et Expression et reconstruire au sein de ces opérateurs l'arbre de syntaxe de l'expression :
 

Code :
  1. // var+var
  2. inline Expression<BinaryNode<Variable,Variable,func_plus> >
  3. operator+( const Variable& v1, const Variable& v2 )
  4. {
  5.   typedef BinaryNode<Variable,Variable,func_plus> expr_t;
  6.   return Expression<expr_t>( expr_t(v1,v2) );
  7. }
  8. // (expr)+var
  9. template<class E>
  10. inline Expression<BinaryNode<Expression<E>,Variable,func_plus> >
  11. operator+( const Expression<E>& e, const Variable& v )
  12. {
  13.   typedef BinaryNode<Expression<E>,Variable,func_plus> expr_t;
  14.   return Expression<expr_t>( expr_t(e,v) );
  15. }
  16. // var+(expr)
  17. template<class E>
  18. inline Expression<BinaryNode<Variable,Expression<E>,func_plus> >
  19. operator+(  const Variable& v ,const Expression<E>& e)
  20. {
  21.   typedef BinaryNode<Variable,Expression<E>,func_plus> expr_t;
  22.   return Expression<expr_t>( expr_t(v,e) );
  23. }
  24. // expr+expr
  25. template<class E1,class E2>
  26. inline Expression<BinaryNode<Expression<E1>,Expression<E2>,func_plus> >
  27. operator+(  const Expression<E>& e1 ,const Expression<E>& e2)
  28. {
  29.   typedef BinaryNode<Expression<E1>,Expression<E2>,func_plus> expr_t;
  30.   return Expression<expr_t>( expr_t(e1,e2) );
  31. }


 
Voila, rajoutez a cela les opérateurs -,*,/, la surcharge pour gérer les constantes numériques :
 

Code :
  1. // const+expr
  2. // les autres versions sont nécéssaire evidemment
  3. template<class E>
  4. inline Expression<BinaryNode<Constante,Expression<E>,func_fois> >
  5. operator*( const double& c, const Expression<E>& e)
  6. {
  7.   typedef BinaryNode<Constante,Expression<E>,func_fois>  expr_t;
  8.   return Expression<expr_t>( expr_t(Constante(c),e) );
  9. }


 
En écrivant du code comme :
 

Code :
  1. template<class E>
  2. double integrate( Expression<E> e, double inf, double sup)
  3. {
  4.     double r=0;
  5.     static const double dx = 0.001;
  6.     for( double i=inf;i<sup;i+=dx;)  r += dx*e.eval(i);
  7. }


 
Nous générons par instanciation récursives des templates le code suivant :
 

Code :
  1. template<class E>
  2. double integrate( Expression<E> e, double inf, double sup)
  3. {
  4.     double r=0;
  5.     static const double dx = 0.001;
  6.     for( double i=inf;i<sup;i+=dx;)  r += dx*(1+1/i);
  7. }


 
C'est gaagné :D
cette  technique permet aussi via quelques modifications mineurs de
générer du code arithmétique vectoriel ou matriciel inline trés performant, éliminant toutes recopies d'objets temporaires et augmentant les performances d'un facteurs 3 ou 4, faisant du C++ un langage aussi rapide que le C.
 
On peut aussi ecrire des classes pour générer du code inline de parser/lexer à la YACC, des filtres de flux, du TI rapide (oops la c'est mon job :p) bref, des infinités d'applications.
 
EDIt : typo/ortho :p


Message édité par Joel F le 29-07-2003 à 10:19:30
Reply

Marsh Posté le 29-07-2003 à 09:16:14   

Reply

Marsh Posté le 29-07-2003 à 14:01:57    

Bon un exemple concret, un peu brutale mais bon.
le but du jeu est d'utiliser les E.T vu ci dessus pour générer du
code arithmétiques sur des classes du types Vecteur ou matrices.
Style :
 

Code :
  1. std::vector<float> a(10),b(10),c(10);
  2. a = 2*a+b*ln(c);

 
 
et généré un code sans recopie ni objets temporaires, stadire :
 

Code :
  1. std::vector<float> a(10),b(10),c(10);
  2. for(int i=0;i<10;++i)
  3. a[i] = 2*a[i]+b[i]*ln(c[i]);

 
 
ce qu'AUCUN COMPILATEUR NE SAIT FAIRE AUTOMATIQUEMENT je precise ...
 
Voila le code, il utilise le même style de code que précédemment :
 

Code :
  1. class vecDouble
  2. {
  3.     public
  4.     vecDouble( int taille=16) : data(NULL), size(taille) { allocate();}
  5.     virtual ~vecDouble() {deallocate();}
  6.     double& operator[](int i) { return data[i]; }
  7.     double operator[](int i) const { return data[i]; }
  8.     double* begin() { return data; }
  9.     template<class E>
  10.     operator=( const Expression<E>& xpr )
  11.     {
  12.         for( int i=0;i<size;i++)
  13.         {
  14.             data[i] = xpr.eval(i);
  15.         }
  16.        return *this;
  17.     }
  18.     private :
  19.     double* data;
  20.     long size;
  21.     void allocate() { if(data) deallocate(); data = new double[size]; }
  22.     void deallocate() { if(data) delete[] data; }
  23. };
  24. class Variable
  25. {
  26.   public :
  27.   Variable( double* it) : val(it) {}
  28.   double eval( int i ) const { return val[i]; }
  29.    private :
  30.    double* val;
  31. };
  32. class Constante
  33. {
  34.   public :
  35.   Constante( const double& v) : val(v){}
  36.   double eval( int ) { return val; }
  37.   private :
  38.   double val;
  39. };
  40. // classe représentant la caractére binaire d'un opérateur  
  41. template<class L,class R,class OP>
  42. class BinaryNode
  43. {
  44.  public :
  45.  BinaryNode( const L& l, const R& r ) : left(l), right(r) {}
  46.  double eval( int i ) { return OP::eval( left.eval(i), right.eval(i));}
  47.  private:
  48.  L left;
  49.  R right;
  50. };
  51. template<class E>
  52. class Expression
  53. {
  54.  public :
  55.  Expression( const E& expr) : xpr(expr) {}
  56.  double eval( int i) { return xpr.eval(i); }
  57.   private :
  58.   E xpr;
  59. };
  60. // var+var  
  61. inline Expression<BinaryNode<Variable,Variable,func_plus> >
  62. operator+( const vecDouble& v1, const vecDouble& v2 )
  63. {
  64.  typedef BinaryNode<Variable,Variable,func_plus> expr_t;
  65.  return Expression<expr_t>( expr_t(Variable(v1.begin()), Variable (v2.begin())) );
  66. }
  67. // etc ....

 
 
 
Voila quelquechose de + concret peut etre :p
gain de perf garanti pour des tres garande expressions :D

Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed