Le support des templates et des generics dans le C++/CLI

Paru le 24 novembre 2006

Sur cette page

Introduction
Managed Template
Generic .NET
Mixer template et generics
Conclusion

Introduction

Depuis la sortie de Visual Studio 2005, le C++ s’est enrichi d’une nouvelle syntaxe : le C++/CLI. Cette version, inclut en fait tout ce que fait le compilateur C++ (code natif depuis Win32 et avant !) et ajoute des éléments de syntaxes des options de compilation et des bibliothèques permettant de travailler avec la runtime et le framework .NET.

Depuis 1998, date de la normalisation C++, le comité a normalisé les Templates comme moteur de généricité du langage. S’en est suivit de la définition des STL, (Standard Template Librairie : 50 fichiers .H normalisé ISO-ANSI).

Avec l’arrivée du Framework .NET v2.0, les « generics » sont disponibles pour toutes syntaxes .NET (dès lors que le compilateur le supporte). On dispose dans le Framework2.0 d’outils de collections génériques et d’une syntaxe permettant d’émettre du code générique.

Il devient intéressant de s’interroger sur le support des Templates et des generics dans le C++/CLI et laquelle de ces approches utiliser. Dans un second temps, il serait utile d’identifier les différences entre ces deux techniques de « généricité » du code (notons que seul le C++ propose deux approches pour la généricité du code vs les autres syntaxes .NET).

Managed Template

Les templates, vont permettre de définir des « gabarits » de code générique ; et ce n’est qu’au moment de la mise en œuvre de ces gabarits que du code apparaîtra dans le binaire issu de la compilation (ce qui permet d’inclure nombre de .H sans pour autant alourdir le binaire résultant ; cette approche s’est révélée très pratique avec la technologie COM sous la forme des ATL : Active Template Library ou dans la norme avec STL)

Commençons par vérifier le support des templates dans un code CLR. Nous prendrons comme option de compilation /CLR :pure, pour nous assurer qu’il n’y a pas de mixte code dans notre exemple ie pas d’appels à du code natif.

Première approche, définissons un template sur une fonction :

      template<class T="">
      T Fct1(T x)
      {
      return x*x;
      }
    

La compilation réussie, notons qu’il est tout à fait possible de créer des fonctions globale en C++/CLI.

En regardant le code IL généré, nous constatons, comme c’était prévisible, qu’à ce stade aucun code n’a été généré.

Bb738066.tgccli-1(fr-fr,MSDN.10).gif

Il faut bien sûr utiliser ce template quelque part dans le code, par exemple en ajoutant la ligne suivante :

    double y = Fct1<double>(8.2);
  

Le compilateur émet alors la fonction Fct1. On voit apparaître dans le code IL généré la fonction typée (ici sur un double).

Bb738066.tgccli-2(fr-fr,MSDN.10).gif

Conforme au comportement des templates « natifs », le code apparaît uniquement lorsqu’il est réellement utilisé ce qui garantit que, quelque soit les définitions présentent dans notre source, seul le code utile sera « embarqué » dans le binaire. Ce qui en principe permet de générer des binaires de petites tailles.

La norme C++, permet de spécialiser des templates, et également d’effectuer des spécialisations partielles, ce qui devient rapidement très utile pour gérer les cas ou types dit « d’exception ». Voyons cela sur un extrait de code.

Spécialisons notre Fct1 pour le type int et pour une référence sur un type d’objet managé :

        //Specialization : int
        template<> int Fct1(int x)
        {
        return x*x*x;
        }
        //Specialization : classe managée
        template<>Test^ Fct1(Test^ x)
        {
        return x;
        }
      

(Notez la syntaxe, conforme à la norme ISO-98, qui explicite via le template<> qu’il s’agit d’une spécialisation de template…)
Les appels correspondants sont naturellement ceux prenant soit un long soit un handle sur Test :

      int z = Fct1<int>(42); qui retournera donc le cube de x.
      Test t;
      Test tt = Fct1<Test^>(%t); qui retournera une copie de t.
    

Pour illustrer la spécialisation partielle, nous définissons un template sur fonction avec deux paramètres génériques :

      template<class T="", class="" T2="">
      T Fct11(T x, T2 y)
      {
      return x*y;
      }
    

Nous ne pouvons alors spécialiser le template que pour l’un des types, par exemple T2 en double:

      template<class T="">int Fct11(T x, double y)
      {
      return static_cast<int>(x*x*y);
      }
    

De même, il est tout à fait possible d’utiliser des « non type parameters », ie des valeurs lors de la définition d’un template, par exemple pour spécifier la taille maximale d’une stack.
Nous venons de travailler sur des fonctions. Qu’en est- il des classes managées ? Bonne surprise ! Il est en effet tout à fait possible de définir ce que l’on appelle des « managed template » sur des « ref class », autrement dit d’utiliser des gabarits de type sur des classes managées :

        template<class T="">
        ref class ManagedTest
        {
        public:
        T mx;
        T FctTest(){return mx;}
        void FctTest2(T t){}

        ManagedTest(){}
        ManagedTest(ManagedTest^){}
        };

      

Cette définition n’apparaît pas dans l’IL généré tant que nous ne l’instancions pas (conformément à ce que l’on attend). Prenons une instance à base de double : notez que le compilateur demande une déclaration explicite d’un constructeur de copie, ce qui est fait ici via une référence .NET (ManagedTest^)

        ManagedTest<double>mt;
        double x = mt.FctTest();
        mt.FctTest2(x);
      

Nous obtenons alors le code IL suivant :

Bb738066.tgccli-3(fr-fr,MSDN.10).gif

Au delà de la définition d’un template sur la portée d’une classe, il est tout à fait possible de définir des templates sur des méthodes, soit l’exemple suivant :

        ref class ManagedTest2
        {
        public:
        String^ mx;
        template<class T="">
        T FctTest(T t){return t*t;}

        ManagedTest2(){}
        ManagedTest2(ManagedTest2^){}
        };
      

Que l’on utilisera de la manière suivante :

        ManagedTest2 mt2;
        double xx = mt2.FctTest<double>(8.2);
      

Enfin, il est tout à fait possible de dériver une ref class « templatisée » dans une ref class. Par exemple :

        ref class Managed3 : ManagedTestTT<double,int>
        {
        public:
        double Test(){return mx;}
        };

      

De même que nous avons pu spécialiser le template d’une fonction, il est possible de spécialiser les templates sur une classe. Ce qui nous donne dans notre exemple pour le type long :

        template<>
        ref class ManagedTest<long>
        {
        public:
        long mx;
        long FctTest(){return mx;}
        void FctTest2(long t){}

        ManagedTest(){}
        ManagedTest(ManagedTest^){}

        };

      

Idem pour les spécialisations partielles, il est possible de déclarer un template de classe partiellement spécialisée. Mais avant, il faut déclarer un template avec deux types pour une ref class. Par exemple :

        template<class T="", class="" T2="">
        ref class ManagedTestTT
        {
        public:
        T mx;
        T2 my;
        T FctTest(){return mx;}
        T2 FctTest11(){return my;}

        void FctTest2(T t){}
        void FctTest2(T2 t){}

        ManagedTestTT(){}
        ManagedTestTT(ManagedTestTT^){}
        };

      

Pour faire une spécialisation partielle, par exemple sur le second type, on pourra écrire :

        template<class T="">
        ref class ManagedTestTT< T, long="">
        {
        public:
        T mx;
        long my;
        T FctTest(){return mx;}
        long FctTest11(){return my;}

        void FctTest2(T t){}
        void FctTest2(long t){}

        ManagedTestTT(){}
        ManagedTestTT(ManagedTestTT^){}
        };

      

Bien évidement le code IL ne sera généré que lorsqu’on utilisera ces templates :

        ManagedTestTT<double,int>mttt;
        double yy = mttt.FctTest();
        int r = mttt.FctTest11();
        mttt.FctTest2(8.2);
      

Et pour la spécialisation partielle :

        ManagedTestTT<double,long>mtttd;
        yy = mtttd.FctTest();
        long xx = mtttd.FctTest11();
        mtttd.FctTest2(8.2);
      

Notez sur la forme spécialisée partielle que l’intellisense comprends qu’il s’agit bien de la forme spécialisée avec un long :

Bb738066.tgccli-4(fr-fr,MSDN.10).gif

Nous venons de voir quelques unes des constructions à base de template managé. La question que l’on peut maintenant se poser est : peut-on utiliser ces définitions en dehors de l’assemblage où ils ont été déclarés ?
Créons pour ce faire un projet C++/CLI en prenant en référence l’assemblage issu de nos premiers exemples. Intellisense nous montre bien les classes et fonction basées sur des templates :

Bb738066.tgccli-5(fr-fr,MSDN.10).gif

Mais nous obtenons ce message à la compilation :

error C2065: 'ManagedTest' : undeclared identifier

L’erreur était prévisible et rappelle bien ce que l’on a avec les STL natives : il faut impérativement inclure les headers dans nos sources. Il en sera donc de même en C++/CLI : on ne peut pas exporter de template de ref class depuis des assemblages .NET.

Tous ces premiers essais, je le rappelle, ont été compilé en mode CLR :Safe, ie en ne manipulant que des types .NET. En compilant en /CLR, nous pourront combiner types natifs et types managés (.NET) ce qui nécessite un minimum de rigueur (nous en reparlerons plus tard).

En synthèse les templates C++ sont accessibles en C++/CLI, ie avec les types .NET. Les managed template se comportent conformément à la spécification ISO98. Ils admettent les spécialisations, y compris partielles. Le code de gabarit et celui généré ne sont pas exportables d’un assemblage. Cette généricité peut être qualifiée de « syntaxique » (ie au niveau de la syntaxe).

Je n’ai pas abordé l’aspect contrainte, mais en effet dans l’implémentation de notre Fct(), je retourne x*x, et il est légitime de se poser la question de ce qu’il va se passer si l’on utilise un type ne supportant pas l’opérateur * (par exemple une classe ne surchargeant pas *) ? C’est à cette question que nous allons répondre au regard du comportement des generics .NET face au même cas de figure.

Generic .NET

Le Framework .NET 2.0 (synchro avec VS2005) introduit une notion de généricité dans le code IL et rend accessible cette technologie aux syntaxes qui souhaitent le supporter. Le fait que ce soit la runtime .Net qui supporte la généricité, induit un comportement au moment de la compilation différent des templates C++. Effectuons les mêmes manipulations de code que précédemment avec un generic .NET . Dans un premier temps, appliquons un type generic à une fonction :

      generic<class T="">
      T Fctg(T x)
      {
      return x*x;
      }
    

Nous obtenons une erreur de compilation :

error C2296: '*' : illegal, left operand has type 'T'

Ca ne veut pas dire qu’on ne peut pas utiliser de generic, mais qu’il existe des contraintes fortes sur les types que nous passons ; ainsi le problème est sur le support de l’opérateur * (multiplier). Le compilateur, ne pouvant garantir le support de cet opérateur pour tous les types, refuse ce code. A noter que nous n’avons pas encore essayé d’utiliser le generic. En effet, la résolution du type à utiliser ne se fait pas à la compilation. Le compilateur doit émettre du code dont il est sûr à l’exécution. Quelque soit le type utilisé, cela fonctionnera.

Je vous propose de modifier le code afin qu’il compile. Nous ne prenons aucun risque en retournant simplement le paramètre sans traitement. A ce stade, nous voyons déjà du code IL généré :

Bb738066.tgccli-6(fr-fr,MSDN.10).gif

Revenons un instant sur le problème de contrainte de type. En fait la vérification du type pour les generics se fait au moment de la compilation et la substitution se fera au moment de l’exécution. Dans le cas des templates la vérification et la substitution se font à la compilation. Le compilateur peut vérifier que le type utilisé supporte bien les instructions codées dans le template (dans notre cas en utilisant des numériques, l’opérateur * ne pose pas de soucis). Notons bien que si les templates avaient à faire à un type incompatible avec l’opérateur *, nous aurions eu une erreur de compilation, par exemple :

      String^ s;
      String^ s2;
      s = Fct1<String^>
      (s2);
    

Génère la même erreur qu’avec les generics :

error C2296: '*' : illegal, left operand has type 'System::String ^'

Nous sommes sur l’une des différences majeures entre generic et template : la contrainte sur les types. Avec les templates, l’application des contraintes se fait au moment de la compilation et est totalement résolue au moment de l’exécution. Avec les generics elle est faite de manière générique à la compilation puisque la substitution se fera au moment de l’exécution sans vérification possible.

Allons un peu plus loin et tentons une spécialisation de notre fonction générique avec le type double :

    generic<> double Fctg(double x)
    {return x;}
  

Nous recevons une erreur très explicite:

error C2979: explicit specializations are not supported in generics

Nous abordons ici, une autre différence marquante entre template et generics : la spécialistaion n’est pas supportée.

Voyons maintenant comment travailler avec des classes génériques. De même que pour les templates, il est en effet possible de travailler avec les génerics au niveau classe et méthode ; par exemple les extraits de code suivants :

  generic<class T="">
  ref class ManagedG
  {
  public:
  T x;
  T Test1(){return x;}
  void Test2(T t){}

  ManagedG(){};
  ManagedG(T xx){x=xx;}
  ManagedG(ManagedG^){}
  };

Ou sous la forme de méthodes génériques:

  ref class ManagedG2
  {
  public:
  long x;
  generic<class T="">
  T Test1(){T t ;return t;}

  generic<class T="">
  void Test2(T t){}

  ManagedG2(){};
  ManagedG2(long xx){x=xx;}
  ManagedG2(ManagedG2^){}
  };

Les utilisations de ces définitions seront de la forme :

  ManagedG<double>mg;
  mg.x = 8.2;
  double x = mg.Test1();
  mg.Test2(x);

  ManagedG2 mg2;
  double y = mg2.Test1<double>();
  mg2.Test2<long>(42);

Notez que pour les templates définis au niveau des méthodes, le type utilisé peut changer de méthodes en méthodes alors que le code IL reste inchangé.

Bb738066.tgccli-7(fr-fr,MSDN.10).gif

Pour le moment dans ces codes génériques, on ne fait pas grand-chose ! Il est en effet difficile d’écrire du code dit générique, puisque par défaut le type T est vu comme un object. Il est possible de faire des cast, mais c’est bien évidement risqué et l’on doit prévoir des gestions d’exception. La difficulté devant l’absence de spécialisation est donc d’écrire des méthodes génériques simulant des spécialisations…qui n’en sont pas réellement dès lors qu’on utilise une technique de cast… C’est pour cela que nous avons la possibilité avec les generics .NET de gérer explicitement des contraintes sur les types. Cela se fait par un mécanisme d’implémentation d’interface. Imaginons l’interface suivante :

  public interface class ITraitable
  {
  virtual void Traite();
  };

On pourra via cette interface définir un generic qui contraint le type utilisé à implémenter cette interface :

  generic<class T="">
  where T : ITraitable
  ref class ListeTraitable : System::Collections::Generic::List<T>
  {

Dans cet exemple nous dérivons de List<> qui est une des classes generic du Framework 2.0 permettant de gérer des collections fortement typées (contrairement aux arraylist). L’IL généré est assez explicite quant au type à utiliser : T doit être de type ITraitable…

Bb738066.tgccli-8(fr-fr,MSDN.10).gif

Reste maintenant à tester cette contrainte. Essayons avec un long :

  ListeTraitable<long>lt;

L’erreur obtenue est cohérente avec ce que nous attendons :

  error C3214: 'long' : invalid type argument for generic parameter 'T' of generic 'ListeTraitable',
  does not meet constraint 'ITraitable ^'

Il faudra bien sûr passer un paramètre implémentant notre interface. Par exemple :

  ref class Personne:ITraitable
  {
  public:
  String^ nom;
  Personne(){}
  Personne(Personne^){}
  Personne(String^ n){nom = n;}

  virtual void Traite(){nom = nom->ToUpper();}
  };

On pourra alors créer une liste de personnes :

  ListeTraitable<Personne^>lt;

Revenons à notre class de liste. L’idée est d’y écrire une méthode générique. Dans notre exemple, nous voulons appeler Traite pour chaque item de la liste. Cela se fera tout naturellement sous la forme :

  generic<class T="">
  where T : ITraitable
  ref class ListeTraitable : System::Collections::Generic::List<T>
  {
  public:
  void TraiteTous()
  {
  for each(T t in this)
  t->Traite();
  }
  };

On peut utiliser la méthode Traite dès la compilation et sans cast « audacieux» puisque T implémente ITraitable ! L’appel est trivial :

  ListeTraitable<Personne^>lt;
  lt.TraiteTous();

Dernier point, et non des moindres, sur les generics : ils sont visibles en dehors de l’assemblage où ils sont définis. Il est en effet possible de créer un projet en C#, C++ ou VB et de prendre une référence sur les types génériques exposés. Dans notre exemple, nous regroupons les précédentes définitions dans un espace de nommage base_generic pour en faire une dll.

  base_generic::ManagedG<double>mg;
  mg.x = 8.2;
  double x = mg.Test1();

C’est grâce à ce mécanisme que les generics sont disponibles au niveau du Framework 2.0. Le namespace System ::Collection ::Generic exporte ainsi bon nombres de generics dédiés aux collections. Au regard de ces tests, on pourrait qualifier les generics .NET de technologie de généricité au runtime, avec contrôle à la compilation (en opposant ainsi les templates qui eux, nous l’avons vu, restent au niveau syntaxique ie compilation seulement).

Mixer template et generics

Il est tout à fait possible de mixer les deux techniques de généricité que nous venons de voir. Cette approche cumule en fait l’ensemble des contraintes des templates C++ et generic .NET. En partant des classes suivantes :

  generic<class T="">
  public ref class TypeGeneric
  {
  public:
  TypeGeneric (){}
  TypeGeneric (TypeGeneric^){}
  };

  template<class T="">
  public ref class TypeTemplate
  {
  public:
  T mx;
  TypeTemplate(){}
  TypeTemplate(TypeTemplate^){}
  };

Il est possible de déclarer des instances mixtes comme suit : (attention à ce stade tout peut devenir très abstrait dans votre code… et pas facile à suivre !)

  TypeGeneric<TypeTemplate<double>^>tgtt;
  TypeTemplate<TypeGeneric<double>>tttg;

Conclusion

Nous venons de le voir les templates C++ sont entièrement disponibles pour les développeurs C++/CLI: le terme managed prend ici tout son sens.

Avec le Framework 2.0, la généricité est couverte par les generics, eux aussi disponibles aux développeurs C++/CLI. Pour choisir entre ces deux approches de la généricté, il faudra prendre en compte les différences que nous avons observé : essentielement entre le support syntaxique ou au runtime et l’exportation de l’assemblage.

Retenons, que les managed templates offrent plus de souplesse en termes de contrainte, de spécialisation ; qu’ils sont explicités lors de la compilation et qu’ils génèrent un code optimum « à la demande ». En revanche, ils sont non exploitables en dehors de l’assemblage qui les définit. Les generics, quant à eux, explicitent les contraintes par une clause where dans la chaîne d’héritage, et offrent moins de souplesse dans l’écriture de code générique. Puisqu’ils ne sont explicités qu’au moment de la JIT Compilation, ils peuvent être exploités en dehors de leur assemblage de définition.