C++/CLI en 10 min, partie 4: Syntaxe détaillée

Dans cette partie, on détaille la syntaxe d’autres éléments en C++/CLI.

nullptr

En C++/CLI, le pointeur nul est nullptr. Il correspond à 0 ou NULL en C++. On peut utiliser nullptr sur des types managés et sur des types non managés:

ManagedPoint ^point = nullptr;

typedefs

Comme en C++, il est possible de définir des alias et de les utiliser ensuite dans le reste du code.

Par exemple, on peut définir un alias de cette façon:

typedef unsigned int positiveNumber;

Les déclarations dans le code peuvent utiliser directement l’alias:

PositiveNumber a;

namespace

Les namespaces se déclarent de la même façon qu’en C++:

  • Pour indiquer l’utilisation d’un namespace particulier dans un fichier: using namespace System;
  • Pour définir une classe à l’intérieur d’un namespace:
    namespace Namespace1 
    { 
        Namespace Namespace2 
        { 
            // Définition de la classe 
        } 
    }
    
  • Pour utiliser des namespaces, on utiliser "::" entre les noms: Namespace1::Namespace2.

Héritage

En C++/CLI, l’héritage des objets managés est semblable à celui en C#.

Par exemple, il se déclare:

ref class DerivedClass : public BaseClass 
{ 
    // ... 
};

Si on ne précise rien:

ref class DerivedClass : BaseClass 
{ 
    // ... 
};

L’héritage est considéré comme public.
Il n’est pas possible de déclarer private BaseClass. Cette syntaxe est acceptée en C++ avec des objets non managés.

Le multihéritage n’est pas possible avec des objets managés (contrairement au C++).
Concernant les objets de type valeur (déclarés avec value class ou value struct):

  • Ils peuvent seulement hérités d’interfaces.
  • Ils ne peuvent pas hérités de classes.
  • Ils sont implicitement sealed c’est-à-dire qu’on ne peut pas en hériter.

Méthode virtuelle

On utilise le mot clé virtual pour déclarer une méthode virtuelle:

ref class BaseClass   
{  
    public:  
      virtual int GetInt()  
      { 
          return 10; 
      } 
};

Pour surcharger une méthode de la classe héritée, on utilise le mot clé override. En plus de ce mot clé il faut aussi repréciser le mot clé virtual:

ref class DerivedClass : public BaseClass 
{  
    public:  
      virtual int GetInt() override 
      { 
          return 5; 
      } 
};

On peut aussi utiliser le mot clé new pour cache la déinition de la méthode dans la classe dérivée:

ref class DerivedClass : public BaseClass 
{  
    public:  
      virtual int GetInt() new 
      { 
          return 5; 
      } 
};

Sealed

Comme en C#, on peut utiliser sealed dans la déclaration de la méthode pour empêcher qu’elle soit surchargée dans un classe dérivée:

ref class DerivedClass : public BaseClass 
{  
    public:  
      virtual int GetInt() sealed 
      { 
          return 15; 
      } 
};

Le mot clé peut aussi être utilisé au niveau de la déclaration d’une classe pour empêcher d’hériter d’une classe:

ref class InheritingClass sealed: public BaseClass 
{  
    // ... 
};
Ne pas confondre “abstract” et “abstract sealed”

Ne pas confondre le déclaration abstract qui sert à déclarer une classe abstraite et abstract sealed qui permet de déclarer une classe statique.

Méthode statique et classe statique

Une méthode statique se déclare dans une classe avec le mot clé static:

ref class NonStaticClass 
{  
    public: 
      static int GetIntStatically() 
      { 
          return 10; 
      } 
};

Une classe statique se déclare avec les mots clés abstract sealed. Tous les membres de la classe statique étant statique, doivent être déclarés avec le mot clé static:

ref class NonStaticClass abstract sealed 
{  
    public: 
      static int GetIntStatically() 
      { 
          return 10; 
      } 
};

Classe abstraite

Pour déclarer une classe abstraite, on utilise le mot clé abstract placé après le nom de la classe.

Par exemple:

public ref class AbstractBaseClass abstract 
{ 
    // ... 
};

Une méthode abstraite se déclare aussi en utilisant le mot clé abstract après la signature de la méthode:

public ref class AbstractBaseClass abstract 
{ 
    public: 
      virtual void AbstractMethod() abstract; 
};

Une méthode virtuelle pure peut aussi se déclarer avec =0 comme en C++:

public ref class AbstractBaseClass abstract 
{ 
    public: 
      virtual void AbstractMethod() =0; 
};
Classe abstraite implicitement

Si une classe n’implémente pas toutes les méthodes de la classe abstraite dont elle hérite ou toutes les méthodes d’une interface, elle est implicitement abstraite même si elle n’est pas déclarée avec le mot clé abstract.

Interface

Contrairement au C++, on peut déclarer des interfaces comme en C# en utilisant les mots clés interface class ou interface struct. Les 2 déclarations sont équivalentes car tous les membres d’une interface sont publics. Les notations interface class ou interface struct existent par cohérence avec ref class et ref struct, toutefois éviter d’utiliser “interface struct” permettra d”éviter des confusions.

On peut déclarer une interface en écrivant:

interface class IPoint 
{  
    public: 
      bool IsEqual(int x, int y); 
      int GetX(); 
      int GetY(); 
};

Quand une classe satisfait une interface, les méthodes doivent être déclarées avec le mot clé virtual:

ref class Point : public IPoint 
{  
    public: 
      virtual bool IsEqual(int x, int y) 
      {  
          return innerX == x && innerY == y; 
      }; 
 
      virtual int GetX() 
      {  
          return x; 
      }; 
 
      virtual int GetY() 
      {  
          return y; 
      }; 
};

Les objets de type valeur ne peuvent pas dériver de classes, en revanche ils peuvent dériver d’interfaces.

Les chaines de caractères

Les chaines de caractères désignent des objets de type différent en C++/CLI: les chaines de caractères managées et les chaines non managées, chacune ayant des caractéristiques différentes.

Chaines de caractères non managées

Plusieurs types correspondent à des chaines non managées:

  • Les chaines provenant du C: char* pour les chaines ANSI et wchar_t* pour les chaines Unicode.
  • Les chaines utilisant la bibliothèque C++ standard STL (standard template library): std::string pour les chaines ANSI et std::wstring pour les chaines Unicode.

Par exemple:

const char *nativeAnsiString = "Chaine native ANSI"; 
const wchar_t *nativeUnicodeString = L"Chaine native Unicode"; 
std::string nativeAnsiStlString = "Chaine native STL ANSI"; 
std::wstring nativeUnicodeStlString = L"Chaine native STL Unicode";

Les chaines Unicode doivent être préfixé avec "L":
L"Chaine Unicode".

Pour utiliser les chaines provenant de la STL il faut ajouter:

#include <string>;

On peut avoir plus de détails sur la différence entre ANSI et Unicode dans: Unicode en 5 min

Chaines de caractères managés

Ce type est le même que les chaines de caractères en C#, c’est un objet de type référence immutable. Immutable car toutes les affectations de nouvelles chaines de caractères créent un nouvel objet.

Les chaines managées sont allouées obligatoirement dans le tas managé et sont déclarées en utilisant un “handle”.

Par exemple:

using System::String;

String ^managedString = L"Chaine managée";

On peut utiliser un constructeur qui permet d’affecter des chaines de caractères natives:

const wchar_t *nativeUnicodeString = L"Chaine native Unicode"; 
String ^managedStringFromNative = gcnew String(nativeUnicodeString); 
 
std::wstring nativeUnicodeStlString = L"Chaine native STL Unicode"; 
String ^managedStringFromStl = gcnew String(nativeUnicodeStlString.c_str());

Conversion d’une chaine de caractères managée vers une chaine non managée
En utilisant la méthode:

static void ClrStringToStdString(String ^str, std::string &outStr) 
{ 
    IntPtr ansiStr = System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(str); 
    outStr = (const char*)ansiStr.ToPointer(); 
    System::Runtime::InteropServices::Marshal::FreeHGlobal(ansiStr); 
}

On peut utiliser les mêmes méthodes pour convertir les types primitifs en chaine ou inversement:

  • ToString(): pour convertir des nombres en chaine de caractères.
  • int::Parse() par exemple pour convertir une chaine en entier.

Casts

Le C++/CLI permet d’effectuer plusieurs types de “cast” pour effectuer des conversions de type. Aux “casts” C++ s’ajoutent le safe_cast spécifique au C++/CLI.

Les “cast” C++ sont:

  • static_cast<>: pour effectuer des changements de type pour des variables de même “famille”. Le plus souvent cet opérateur permet d’éviter d’avoir un warning de compilation lorsque le “cast” est implicite.
    Par exemple:

    int a = 10; 
    double b = static_cast<double>(a);
    

    Si on manipule des pointeurs, que A* pointe vers un objet de type A et que A est une partie d’un objet B. Effectuer un cast static_cast<B*> permet, sans effectuer de vérifications, d’ajuster l’adresse du pointeur de façon à pointer vers B.

  • const_cast<>: pour supprimer la qualification const sur une variable de type pointeur ou référence, par exemple:
    int i = 8;
    const int &iRef = i; 
    int &iRef2 = const_cast<int&>(iRef); // permet d'enlever la qualification const sur iRef pour qu'elle soit affectée à une référence non constante. 
    
  • dynamic_cast<>: qui permet d’effectuer des changements de type à l’exécution dans la hiérarchie d’héritage de classes. Ce “cast” peut être effectué sur des variables de type pointeur ou référence. Lorsque ce type de cast échoue, le résultat renvoyé est null.
    Par exemple, si on définit les classes:

    class A  
    { 
        public: 
          virtual void f() {} 
    }; 
     
    class B : public A  
    {};
    

    On peut effectuer le cast:

    B bVal; 
    B &bRef = bVal; 
    A &aRef = dynamic_cast<B&>(bVal);
    
  • reinterpret_cast<>: ce type de cast permet de retourner un pointeur comportant le même nombre d’octets en mémoire mais en changeant le type du pointeur. L’adresse du pointeur n’est pas modifiée, seul son type est modifié. Ce type de cast peut mener à des erreurs dans le cas où on effectue un cast entre des types n’occupant pas le même espace en mémoire.

Le C++/CLI ajoute un autre “cast”: safe_cast<> qui permet permet d’effectuer l’équivalent de dynamic_cast<> en lançant une InvalidCastException si le “cast” échoue.

Par exemple, si on considère les classes:

ref class A {}; 
ref class B : A {}; 

Le safe_cast<> peut être utilisé de cette façon:

B ^b = gcnew B(); 
A ^a = b; 
 
try 
{ 
    B ^bWithCast = safe_cast<B ^>(a); 
} 
catch (System::InvalidCastException ^e) 
{ 
    Console::WriteLine("Cast failed"); 
}

Equivalent à typeof()

On peut obtenir le type d’un objet en écrivant:

obj->GetType();

Pour avoir le type d’une classe avec une écriture équivalente à typeof() en C#, on utilise typeid:

ClassName::typeid

initonly

initonly est l’équivalent de readonly en C#, il rends obligatoire l’initialisation d’un membre dans le constructeur de la classe. Avec ce mot-clé, une initialisation après le constructeur provoque une erreur de compilation.

Par exemple, pour utilisation initonly:

initonly int innerX; 
Initonly Point ^point;

literal

Des constantes peuvent être déclarées comme en C++ avec static const dans le scope de la classe. Toutefois en C++/CLI, les constantes déclarées de cette façon ne sont pas reconnues à la compilation si l’objet de la classe est accédé avec #using statement.
Pour atteindre une constante définie dans une classe et en utilisant #using statement, il faut utiliser les mot-clés literal.

La déclaration est directe:

literal int innerX = 34;

pragma

L’utilisation de ce mot-clé permet d’indiquer des parties d’un fichier qui sont managés et d’autres parties qui seront non managées. Si l’option /clr n’est pas utilisée pour la compilation de l’assembly, le compilation ignore les déclarations #pragma.
#pragma doit être utilisé à l’extérieur du corps d’une fonction et après une directive #include.

Il est déconseillé d’utiliser ces mot-clés, il est préférable d’utiliser des fichiers séparés réservés à des objets natifs.

Tout ce qui est entre #pragma unmanaged et #pragma managed est interprété comme du code natif:

#include ... 
 
#pragma unmanaged 
// Code natif 
 
Int NativeFunction() 
{  
    // ... 
} 
 
#pragma managed 
// Code managé

On peut utiliser d’autres types de déclarations pour indiquer du code natif dans un fichier managé:

#pragma managed(push, off) 
// Code natif 
 
#pragma managed(pop) 
// Code managé

Tableaux

Il existe en C++/CLI, une structure de données équivalente aux tableaux en C++ qui permet de stocker des objets managés ou des objets non managés. Cette structure de données est array.
D’autres structures sont disponibles:

  • Des structures provenant du C++ comme les structures fournies par la Standard Template Library (STL): std::vector, std::list, std::map etc…
  • Des structures .NET: listes génériques List<T>, les dictionnaires Dictionary<K,V>, HashSet<T>, etc…

Tableau en C++

Les tableaux C++ classiques peuvent être utilisés, ces tableaux sont très différents des objets .NET: le nom de la variable renvoie au premier élément du tableau. Les autres éléments sont ensuite placés de façon contigue en mémoire à partir du premier élément. Si on souhaite accéder au 3e élément du tableau, connaissant la taille de chaque élément stocké, on applique un décalage à partir du premier élément du tableau pour savoir où il se trouve en mémoire.

Par exemple:
Pour déclarer un tableau de double dont la taille est fixe:

double doubleArray[10]; // ce tableau est alloué sur la pile 
Point *refObjectArray[7]; // tableau de pointeurs vers une classe

On peut initialiser directement le contenu d’un tableau:

int intArray[4] = {1, 2, 4, 8 }; // en précisant la taille 
int intArray[] = {1, 2, 4, 8 }; // sans préciser la taille

On peut accéder aux éléments du tableau de façon classique en utilisant l’index de l’élément:

int element = intArray[2];  
refObjectArray[3] = new Point(4, 9); // même si le tableau est alloué sur la pile, il faut penser à libérer chaque élément avec "delete"

Pour déclarer un tableau à 2 dimensions:

int array2d[4][2];

Les tableaux précédents ont des tailles fixes et connues à la compilation. On peut allouer de façon dynamique dans le tas.
Par exemple:

double *doubleDynArray = new double[10]; // tableau de double 
Point **pointDynArray = new Point*[10]; // tableau de pointeurs

On peut accéder aux éléments de la même façon avec l’index de l’élément:

double doubleValue = doubleDynArray[3]; 
Point *pointValue = pointDynArray[2];
Pour les tableaux alloués dans le tas, il faut utiliser delete[]

Sachant qu’on a alloué des objets dans le tas en utilisant new, il faut les libérer en utilisant delete[] et non delete:

delete[] doubleDynArray; 
delete[] pointDynArray;

Lorsque le tableau est alloué sur la pile, il n’est pas nécessaire d’utiliser delete.

Tableau managé array<T>

L’équivalent aux tableaux C++ est array<T> qui est une structure de données managéé dont la taille est fixe. Comme pour les tableaux C++, la taille d’une array<T> reste fixe et n’est pas augmentée automatiquement (comme les listes génériques par exemple).

Sachant que array<T> est un objet géré par le CLR, on peut y accéder en utilisant un “handle”.

Par exemple, pour initialiser un objet de type array<T>:

array<int> ^intArray; // tableau d'entier 
array<String^> ^stringArray; // tableau de string 
array<Point^> ^pointArray; // tableau de "Point" qui est un objet de type "ref class"

L’initialisation se fait en utilisant gcnew et en précisant la taille:

array<int> ^intArray = gcnew array<int>(5); 
array<String^> ^stringArray = gcnew array<String^>(5); 
array<Point^> ^pointArray = gcnew array<Point^>(3);

Comme tous les objets gérés par le CLR, il n’est pas nécessaire d’utiliser delete pour les libérer.

On peut aussi initialiser un array<T> plus directement:

array<int> ^intArray = gcnew array<int>(3) { 1, 2, 4, 8 }; // en précisant la taille 
array<int> ^intArray = gcnew array<int>() { 1, 2, 4, 8 }; // sans indiquer la taille 
array<int> ^intArray = { 1, 2, 4, 8 }; // Encore plus directement 
 
array<String ^> ^stringArray = gcnew array<String^>(3) { 
    gcnew String("first string"), 
    gcnew String("secund string"), 
    gcnew String("third string") };

L’accès aux objets se faire classiquement en utilisant les index:

intArray[1] = 6; 
pointArray[5] = gcnew Point(2, 8);

Pour déclarer un tableau en 2 dimensions:

array<int, 2> ^arrayIn2d = gcnew array<int, 2>(4, 3);

"2" car il s’agit d’un array à 2 dimensions; "4" car il possède 4 lignes et "3" car il possède 3 colonnes.

Pour initialiser directement ce type de tableau:

array<int, 2> ^arrayIn2d = gcnew array<int, 2>(3, 4) 
    { 
        { 2, 13, 65, 76 }, 
        { 5, 87, 29, 140 }, 
        { 8, 84, 97, 9885 } 
    }; 

On peut accéder à chaque élément de l’array<T> avec une déclaration un peu différent de celle en C++:

arrayIn2d[2, 1] = 5; 

Boucle “for each”

Comme en C#, on peut utiliser une boucle for each pour accéder aux éléments d’une array<T>:

for each (String ^s in stringArray) 
{ 
    String ^arrayElement = s; 
    // ... 
}

Plus généralement, comme en C#, for each est utilisable pour toutes les structures satisfaisant IEnumerator.

Copier une array dans une autre

Il est possible de copier une array dans une autre en utilisant System::Array::Copy:

array<int> ^firstArray = gcnew array<int>(4); 
array<int> ^secundArray = gcnew array<int>(3);

System::Array::Copy(firstArray, 1, secundArray, 0, 2); 

Permet de copier firstArray vers secundArray en commençant à l’index "1" de firstArray et en copiant à partir de l’index "0" de secundArray. 2 éléments seront copiés.

Autres structures .NET

La plupart des structures de données courantes en .NET sont accessibles en C++/CLI:

  • La liste générique: List<T> par exemple List<String^> ^stringList = gcnew List<String^>();
  • Le dictionnaire: Dictionary<K,V> par exemple: Dictionary<String^, Point^> ^dictionary = gcnew Dictionary<String^, Point^>();
  • Une structure FIFO: Queue<T>.
  • Une structure LIFO: Stack<T>.
  • Une liste de pairs clé/valeur ordonnée: SortedList<K,V>.

STL

Les structures de données fournis par la Standard Template Library (STL) sont aussi utilisables: vector, list, map, multimap, set, multiset, queue, deque, stack etc…

Propriétés

On peut déclarer des propriétés dans les classes comme en C#.

Propriétés scalaires

On peut définir directement des propriétés avec le mot clé property.
Par exemple:

ref class Point 
{  
    public: 
      Point(int x, int y, String ^name) 
      { 
          X = x; 
          Y = y; 
          Name = name; 
      } 
 
      property int X; 
      property int Y; 
      property String ^Name; 
};

Les 3 propriétés X, Y et Name possédent implicitement un “getter” et un “setter”.

On peut y accéder classiquement:

Point ^point = gcnew Point(2, 9, "mai point"); 
point->X = 65; 
point->Y = 4; 
point->Name = "new name";

On peut utiliser une implémentation plus explicite du “getter” et du “setter”.
Par exemple:

ref class Point 
{  
    private: 
      int innerX, innerY; 
      String ^name; 
 
 
    public: 
      Point(int x, int y, String ^name) 
      { 
          innerX = x; 
          innerY = y; 
          Name = name; 
      } 
 
      property int X 
      { 
        int get() { return innerX; } 
        void set(int x) { innerX = x; } 
      } 
 
      property int Y 
      { 
        int get() { return innerY; } 
        void set(int y) { innerY = y; } 
      } 
 
      property String ^Name 
      { 
        String ^get() { return name; } 
        void set(String ^n) { name = n; } 
      } 
};

Il est possible d’implémenter une propriété en lecture seule en omettant la déclaration du “setter”. Inversement on peut déclarer une propriété en écriture seule en omettant la déclaration du “getter”.

Propriétés et héritage

Comme les autres membres d’une classe, les propriétés peuvent être surchargées.

Par exemple, si on définit la classe suivante:

ref class NamedObject abstract 
{ 
    public: 
      virtual property String ^Name; 
};

Une classe fille peut surcharger seulement le “getter”:

ref class NamedPoint : NamedObject 
{ 
    public: 
      virtual property String ^Name 
      { 
        String ^get() override  
        { 
          return "unnamed"; 
        } 
      } 
};

Propriétés indexées

Les propriétés indexées permettent d’accéder à un élément au moyen d’un index. L’élément n’est pas forcément dans une structure de données indexée puisqu’on peut implémenter librement le “setter” et le “getter”.

Par exemple, une propriété indexée peut être déclarée simplement avec:

property double IndexedValues[long];

Une implémentation plus explicite du “getter” et “setter” est possible:

property double IndexedValues[long] 
{ 
    double get(long index)  
    { 
      // ...  
    } 
 
    void set(long index, double value)  
    { 
      // ... 
    } 
}

Les exceptions

Les exceptions en C++/CLI sont proches de celles en C#:

  • Une exception C++/CLI est un objet de type référence qui dérive System::Exception.
  • Les exceptions s’utilisent dans des blocs try...catch...finally. finally ayant la même fonction qu’en C# c’est-à-dire exécuté du code après le bloc try...catch dans le cas d’une exception ou non.
  • Sachant qu’il est possible de lancer des exceptions managées en C++/CLI, elles peuvent être attrapées directement dans du code C#.
  • Il est possible de gérer 3 types d’exceptions: les exceptions managées C++/CLI, les exceptions C++ et les Microsoft Windows Structured Exception Handling (SEH) (plus de détails sur les exceptions SEH dans Gestion des “Corrupted State Exceptions” par le CLR.

Try…catch

Par exemple, un bloc try...catch est semblable à du code C#:

try 
{ 
    // Code où une exception peut être lancée 
} 
catch (System::InvalidCastException ^e) 
{ 
    // Traitement InvalidCastException 
} 
catch(System::ArithmeticException ^aex) 
{ 
    // Traitement ArithmeticException 
} 
catch(System::DivideByZeroException ^dex) 
{ 
    // DivideByZeroException 
}

Try…catch…finally

Une clause finally permet d’exécuter du code quelque soit ce qui se passe:

ManagedPoint ^firstPoint = gcnew ManagedPoint(23, 87); 
ManagedPoint ^secundPoint = gcnew ManagedPoint(0, 0); 
 
try 
{ 
    point->GetDistance(secundPoint); 
} 
catch (System::ArgumentException ^e) 
{ 
    Console.WriteLine(e->Message); 
} 
finally 
{ 
    delete firstPoint; 
    delete secundPoint; 
}

Throw

Pour lancer une exception:

throw gcnew System::ArgumentException("Argument null"); 

Définir un type d’exception

Il suffit de dériver de System::Exception comme en C#.

Par exemple:

ref class CustomException : System::Exception 
{ 
    public: 
      int errNo; 
 
      CustomException(String ^msg, int num) : Exception(msg), errNo(num) {} 
};

Delegates

Les déclarations des “delegates” sont proches de celles en C#. Comme un C#, ils permettent de définir la signature d’une fonction.

Par exemple, pour définit un delegate:

delegate int SquareDelegate(int number); 

Pour créer un delegate lié à une fonction statique d’une classe:

ref class SquareCalculator 
{ 
    public: 
      static int GetSquare(int x)  
      {  
          return x*x;  
      } 
};

On lie le “delegate” à la fonction statique Square::GetSquare():

SquareDelegate ^square = gcnew SquareDelegate(&SquareCalculator::GetSquare); 
// Ou 
SquareDelegate ^otherSquare = gcnew SquareDelegate(nullptr, &SquareCalculator::GetSquare); 

Pour exécuter le “delegate”:

int valueSquare = square(5); 

Pour lier un “delegate” à une fonction non statique, il faut ajouter l’instance de la classe à la déclaration du delegate.
Si on déclare le delegate suivant:

delegate int GetPointIndexDelegate(int x, int y); 

Et la classe suivante:

ref class PointSet 
{ 
    private: 
      array<Point^> points; 
 
    public: 
      PointSet() 
      {  
          points = gcnew array<Point^>()  
          {  
              gcnew Point(5, 8), 
              gcnew Point(2, 9), 
              gcnew Point(1, 3), 
          };  
      } 
 
      int GetPointIndex(int x, int y)  
      {  
          for(int i=0; i<3; i++) 
          { 
             Point ^p = points[i]; 
             if (p->X == x && p->Y == y) 
             { 
                return i; 
             } 
          } 
 
          return nullptr; 
      } 
};

On peut créer un “delegate” lié à une fonction non statique par:

PointSet ^pointSet = gcnew Point; 
GetPointIndexDelegate ^getPointIndex = gcnew GetPointIndexDelegate(pointSet, &PointSet::GetPointIndex);  

Pour exécuter la fonction en utilisant le “delegate”:

int index = GetPointIndex->Invoke(5, 8); 

Evènements

L’utilisation des évènements en C++/CLI est très semblable à ce qu’on peut retrouver en C#. On peut déclarer des évènements, s’y abonner puis déclencher l’exécution des fonctions abonnées.

Comme en C#, la création d’un évènement se fait à partir d’un “delegate”.
Par exemple:

delegate void ClickHandler(int, int); // déclaratin du delegate 
event ClickHandler ^OnClick; // déclaration de l'évènement 

Dans une classe:

ref class Point 
{ 
    private: 
      int innerX, innerY; 
 
 
      void RaiseOnCoordinateChanged() 
      { 
        if (OnCoordinateChanged != nullptr) 
        { 
          OnCoordinateChanged(innerX, innerY); // déclenchement de l'évènement 
        } 
      } 
 
    public: 
      Point(int x, int y) 
      { 
        innerX = x; 
        innerY = y; 
      } 
 
      void SetX(int newX) 
      { 
        innerX = newX; 
        RaiseOnCoordinateChanged(); 
      } 
 
      void SetY(int newY) 
      { 
        innerY = newY; 
        RaiseOnCoordinateChanged(); 
      } 
 
      delegate void CoordinateChangedHandler(int, int); 
 
      event CoordinateChangedHandler ^OnCoordinateChanged; 
};

Pour s’abonner à un évènement:

Point->OnCoordinateChanged += gcnew CoordinateChangedHandler(this, &eventHandler); 

Par exemple:

ref class PointSubscriber 
{ 
    private: 
      void PointChanged(int newX, int newY) 
      { 
        // ... 
      } 
 
    public: 
      PointSubscriber(Point ^point) 
      { 
        point->OnCoordinateChanged += gcnew CoordinateChangedHandler(this, &PointChanged);  
      } 
};

Pour se désaboner:

Point->OnCoordinateChanged -= gcnew CoordinateChangedHandler(this, &eventHandler); 

On peut utiliser une implémentation plus explicite lorsque certaines opérations sont effectuées sur un évènement:

  • add: quand une souscription est effectuée,
  • remove: quand une souscription est supprimée,
  • raise: quand l’évènement est déclenché.

Par exemple, en reprenant l’exemple précédent:

ref class Point 
{ 
    private: 
      int innerX, innerY; 
 
      void RaiseOnCoordinateChanged() 
      { 
        if (OnCoordinateChanged != nullptr) 
        { 
          OnCoordinateChanged(innerX, innerY); 
        } 
      } 
 
      event CoordinateChangedHandler ^innerOnCoordinateChanged; 
 
    public: 
      Point(int x, int y) 
      { 
        innerX = x; 
        innerY = y; 
      } 
 
      void SetX(int newX) 
      { 
        innerX = newX; 
        RaiseOnCoordinateChanged(); 
      } 
 
      void SetY(int newY) 
      { 
        innerY = newY; 
        RaiseOnCoordinateChanged(); 
      } 
 
      delegate void CoordinateChangedHandler(int, int); 
 
      event CoordinateChangedHandler ^OnCoordinateChanged 
      { 
        void add(CoordinateChangedHandler ^handler)  
        { 
          innerOnCoordinateChanged += handler; 
        } 
 
        void remove(CoordinateChangedHandler ^handler)  
        { 
           innerOnCoordinateChanged -= handler; 
        } 
 
        void raise(Object ^sender, PageDumpedEventArgs ^ea)  
        { 
          RaiseOnCoordinateChanged(); 
        } 
      } 
};

raise est implicitement protected, on ne peut pas l’atteindre à l’extérieur de la classe.

Une autre implémentation possible:

private: 
  static initonly Object ^pointChanged = gcnew Object(); 
 
public: 
  event CoordinateChangedHandler ^OnCoordinateChanged 
  { 
      void add(CoordinateChangedHandler ^handler)  
      { 
        Component::Events->AddHandler(pointChanged, handler);  
      } 
 
      void remove(CoordinateChangedHandler ^handler)  
      { 
        Component::Events->RemoveHandler(pointChanged, handler);  
      } 
 
      void raise(int x, int y)  
      { 
        CoordinateChangedHandler ^handler = (CoordinateChangedHandler^)Component::Events[pointChanged]; 
        if (handler != nullptr) 
          handler(x, y); 
      }  
  }

Templates et generics

Les “templates” et les “generics” sont des notions semblables et sont toutes les deux supportées par le C++/CLI. Les templates sont des types instanciés à la compilation alors que les “generics” restent génériques jusqu’à l’exécution et sont instanciés par le CLR. Les “generics” permettent de définir des contraintes sur le type des paramètres. Ce type de fonctionnalité n’est pas supporté par les “templates”.

Une classe “template” C++ peut être définie de cette façon:

template<typename xType, typename yType> class NativePoint 
{ 
    public: 
      NativePoint(xType x, yType y) 
      { 
        innerX = x; 
        innerY = y; 
      } 
   
      void SetX(xType x) 
      { 
        innerX = x; 
      } 
 
 
      void SetY(yType y) 
      { 
        innerY = y; 
      } 
 
    private: 
      xType innerX; 
      yType innerY; 
};

Un “generic” équivalent peut être défini de cette façon (le “generic” C++/CLI est la même notion que le “generic” C#):

generic<typename xType, typename yType> ref class GenericPoint 
{ 
    public: 
      GenericPoint(xType x, yType y) 
      { 
        innerX = x; 
        innerY = y; 
      } 
 
      void SetX(xType x) 
      { 
        innerX = x; 
      } 
 
      void SetY(yType y) 
      { 
        innerY = y; 
      } 
 
    private: 
      xType innerX; 
      yType innerY; 
};

Contrainte

Les “generics” peuvent utilisés des contraintes comme en C#.

Par exemple, si on définit l’interface suivante:

interface class INamedObject 
{ 
  void SetName(String ^name); 
};

On définit un objet qui satisfait la classe:

ref class Point : INamedObject 
{ 
    public: 
      void SetName(String ^name) 
      { 
        objectName = name; 
      } 
 
 
    private: 
      String ^objectName; 
};

On peut utiliser un “generic” avec une contrainte pour imposer que le type du “generic” satisfait l’interface:

generic<typename T> where T:INamedObject ref class NamedObjectWrapper 
{ 
    public: 
      void SetName(String ^name) 
      { 
        innerObject = Activator::CreateInstance<T>(); 
        innerObject->SetName(name); 
        delete safe_cast<Object^>(innerObject); 
      } 
 
      void DeleteInnerObject() 
      { 
        delete safe_cast<Object^>(innerObject);
      } 
 
    private: 
      T innerObject; 
};

On peut remarquer l’utilisation de Activator::CreateInstance<T>() pour créer une instance de l’objet à la place de gcnew. De même on utilise delete safe_cast<Object^>(innerObject) pour libérer l’espace alloué pour l’objet à la place d’un simple delete.

Utilisation des templates dans des assemblies mixtes

L’utilisation de templates C++ dans des assemblies mixtes peut mener à des appels managés-non managés involontairement ce qui peut dégrader les performances. Un template C++ compilé dans du code natif et ses membres sont compilés dans du code natif. Si du code utilise ce template et que le code est compilé dans du code managé, les membres du template seront compilés dans du code managé. Le template est donc compilé dans 2 variantes:

  • 1 variante dans du code non managé
  • 1 variante dans du code managé.

Suivant les utilisations du template, le linker choisira d’utiliser l’une ou l’autre des 2 variantes:

  • Pour un appel provenant de code non managé, il utilisera la version non managée
  • Pour un appel provenant de code managé, il utilisera la version managée.
Pour aller plus loin…

Références

Leave a Reply