IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

C++/CLI quoi de neuf ?

Le C++/CLI est un langage à part parmi les langages .NET, il est le seul à permettre le mélange de code avec le langage dont il est issu le : C++.

Cette fonctionnalité est importante, car elle permet la récupération de code C++ ou le rafraichissement de programmes développés avec les MFC.

En effet depuis Visual 2005 il est très facile de mélanger du code C++/CLI avec un programme MFC, ce dernier bénéficiant alors de l'enrichissement de l'interface utilisateur avec la plateforme .net.

Un autre aspect, le C++/CLI est le langage de l'interopérabilité.

Avec le développement d'assembly il permet la récupération de code C++ encapsulé dans un wrapper de classe, permettant ainsi l'utilisation de code natif c++ dans les autres langages .net.

En dehors de ces aspects, les évolutions du langage C++ mises en œuvre dans le C++/CLI sont très intéressantes de par les apports syntaxiques ou les nouvelles fonctionnalités.

La plus visible étant le ramasse-miette mémoire au niveau des objets managés.

À travers cet article je vais décrire les différences et les apports par rapport au C++ standard.

Il est donc important pour pouvoir lire ces pages d'avoir une connaissance préalable du langage C++.

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Les types de données

Tous les types de données sont initialisés à zéro, même les types classiques C++ puisqu'ils sont des alias d'objet du framework .net.

Type natif C++

Correspondance dans le Framework .Net

wchar_t

System::Char caractères 16 bits Unicode

unsigned char

System::Byte

signed char

System::SByte

double, long double

System::Double

float

System::Single

int, signed int, long,signed long

System::Int32

__int64 , signed __int64

System::Int64

short, signed short

System::Int16

bool

System::Boolean

Tous les objets héritent de System::Objet

Méthodes communes :

Object()

création d'une nouvelle instance d'un objet

Equals()

comparaison de deux instances pour voir si elles sont égales

GetHashCode()

renvoie un hascode

GetType()

le type de donnée d'un objet

ReferenceEquals()

vérifie si deux instances d'un objet sont les mêmes

ToString()

renvoie une chaine qui représente l'objet.

I-A. Types de données utilisateur

I-A-1. Type énuméré

enum class ou enum struct : nommage constant.

On peut utiliser System::Enum ou enum du c++

Exemple :

 
Sélectionnez
enum  class SerialParity { None,Odd,Even};
SerialParity Parity;
Parity=SerialParity::Odd;

I-A-2. struct et value class

value struct :

value class : pareil sauf que par défaut les données de value struct sont publiques alors celles de value class sont privées.

value struct et value class ne sont pas managées, elles ne bénéficient pas du ramasse-miette mémoire.
value class et value struct héritent de la classe .net System::ValueType.

Elle permet que toutes les valeurs soient déposables sur la pile.

Elles peuvent hériter uniquement d'interface.

Tenter d'hériter de value struct ou value class provoquera une erreur à la compilation.

Ces deux types sont à réserver pour des types de données simples (pod).

La surcharge de l'opérateur d'affectation n'est pas autorisée, la copie des données se fait donc membre à membre.

I-A-3. Type référence

Les types de données référence sont des types que le programmeur développe et sont accessibles par des handles , ils sont stockés dans le tas managé.

Tous les types référence en C++/CLI sont garbage collector.

C++/CLI possède quatre types référence utilisateur définissables : arrays, classes, interface, delegates.

Ces quatre types ont une chose en commun : pour créer une instance ils requièrent l'opérateur gcnew.
gcnew utilise le symbole ^ qui est le pendant de * avec new du C++.

I-A-3-a. Les Tableaux

Les arrays sont similaires aux tableaux en C++, ils utilisent la CRT tas memory.

Ils récupèrent la place perdue (garbage collector) et gèrent une dimension.

Ils peuvent gérer tous les types hérités de System::Object celle-ci étant la classe de base des objets du framework.

La déclaration d'un array nécessite un handle qui sera alloué dans le tas managé par gcnew.

Syntaxe générale :

 
Sélectionnez
array<datatype>^ arrayVarName ;
 
Sélectionnez
//exemple de creation d'un array avec appel du constructeur :
using namespace stdcli ::language ;
array<double>^ ArrayDouble= gcnew array<double>(5);//5 double
array<String^>^ Arraystrings= gcnew array<String^>(10);

Il est possible de gérer des données non managées dans un array à condition que le type de données soit de type pointeur.

 
Sélectionnez
class NoManage{} ;
array< NoManage *>^ pNoManage = gcnew array< NoManage *>(10) ;
for(int i=0;i<pNoManage ->Length;i++)
pNoManage [i]=new NoManage();   
for(int i=0;i<pNoManage->Length;i++)
delete pNoManage[i];

Il est possible d'initialiser un tableau en même temps que la déclaration :

 
Sélectionnez
array<String^>^ ArrayString= gcnew array<String^>{"1","2","3","4"};

Les tableaux multidimension se déclarent comme pour les tableaux de templates.

Il suffit d'ajouter le rang derrière le type de données, le rang est compris entre 1 et 32 éléments, une erreur sera générée pour les autres valeurs.

Le rang ne peut être une variable.

 
Sélectionnez
array<int,1>^ TwoInts= gcnew array<int>(2);//2 entiers
array<int,2>^ TwoIntsx3= gcnew array<int,2>(2,3);// 2 X 3
for(int n=0;n<TwoIntsx3->GetLength(0);n++)
{
        for(int i=0;i<TwoIntsx3->GetLength(1);i++)
                TwoIntsx3[n,i]=((n*5)+i);
}

Les tableaux ainsi déclarés ont une taille uniforme sur les dimensions, un tableau à deux dimensions sera rectangulaire.

Il est possible d'avoir un tableau qui n'a pas la même longueur dans les autres dimensions pour cela on utilisera la syntaxe connue en C++.

Syntaxe générale :

 
Sélectionnez
array< array<datatype>^ >^
 
Sélectionnez
array<array<int>^ >^ varMulti= gcnew array<array<int>^ >(2);
for(int i=0;i<varMulti->Length;i++)
varMulti[i] = gcnew array <int>(10+(i*3));      // 2 tableaux un de 10 et un de 13 elements

L'accès à une variable est réalisé en utilisant l'opérateur [] comme en C++.

La nouveauté c'est lorsque le tableau est à plusieurs dimensions on écrira :

 
Sélectionnez
variable[index1,index2,....]
I-A-3-b. Interface

Une interface est une collection de méthodes et de propriétés sans définitions.

L'interface n'a pas d'implémentation pour ses méthodes et ses propriétés.

I-A-3-c. Delegates et Events

Delegate est un type de référence qui fonctionne comme un pointeur de fonction, qui peut être lié à une instance ou une méthode statique d'une classe C++/CLI Delegate peut être utilisé quand une méthode à besoin d'un appel dynamique et est utilisée comme callback de fonction pour intercepter les événements avec les applications .net.

Un event est une implémentation spécialisée d'un delegate.

Un event permet à une classe de déclencher l'exécution d'une méthode trouvée dans une autre classe, sans rien connaitre sur cette classe.

Ces éléments seront développés dans les pages qui suivent.

I-A-4. Boxing et Unboxing

Technique permettant de convertir des types valeurs en types références.

Unboxing étant l'inverse.

La pile est la forme de défaut de stockage pour les types de valeur du framework.

Dans cette forme un type valeur ne peut accéder à ses méthodes comme ToString(),parce que le type de valeur doit être au format d'objet (référence).

Pour remédier à ce problème, le type de valeur est implicitement (automatiquement) boxed quand la méthode ToString est appelée.

Avec .net 2.0 tous les types valeurs sont implicitement boxed.

Exemple :

 
Sélectionnez
value class Point
{
public 
int x,y ;
Point(int x,int y) :x(x),y(y){}
} ;
Point p1(1,2) ;
Object ^o =p1 ;         // la conversion est implicite.
Point ^hp=p1;           
Object ^o1 = 1024; // boxing implicite

L'objet créé est une copie du type valeur, donc toutes les modifications faites dans l'objet boxed ne seront pas répercutées dans l'objet d'origine.

Unboxing un type référence en un type valeur requière un cast explicite

 
Sélectionnez
Point p2=(Point)o ;
Point p1=(Point)hp ;

I-A-5. Types modificateurs et qualificateurs

Trois modificateurs et un qualificateur sont disponibles en C++/CLI, ils donnent un plus d'information sur la variable définie qui le précède.

auto

Permet de spécifier que la portée de la variable est le bloc où elle est définie.

Ce modificateur est optionnel.

 
Sélectionnez
auto int normalinteger ;

const

Identique au C++, une constante ne peut être modifiée même par pointeur interposé.

 
Sélectionnez
const Int32 intergerconstant=32 ;

Le C++/CLI ne supporte pas const sur les méthodes membres des types de données managés.

Exemple :

 
Sélectionnez
bool GetFlag() const {return true ;}

N'est pas permis avec une classe déclarée value ou ref (value struct ou ref class) Ce n'est pas supporté sur une interface…

extern

Identique au C++.

static

identique au C++.

I-A-6. Conversion de type

Quand la variable de réception est plus petite que la variable d'origine sur une opération d'affectation, il y a une conversion de type pouvant conduire à une perte d'information.

Exemple :

 
Sélectionnez
UInt16  n=45000 ;
Byte c=n ; // c vaut 175 et pas 45000.

Le compilateur vous signalera cet état par un warning. Pour enlever le warning il faudra utiliser un cast explicite. On utilisera :

 
Sélectionnez
safe_cast< type-de-donnée-pour-conversion >(expression).

Ou le bon vieux cast du C:
(type-de-donnée-pour-conversion) expression.

 
Sélectionnez
char b= safe_cast<char>(a);
// ou
char b=(char )a;

Littéral

Les nombres numériques peuvent être exprimés comme en c++, en octal, entier, hexadécimal, décimal, exponentiel.

La nouveauté c'est qu'un littéral numérique peut être considéré comme un objet.

Le nombre devant être entouré par des parenthèses, ce qui permettra d'utiliser la méthode ToString().

 
Sélectionnez
Console ::WriteLine((12356).ToString()) ;
Console ::WriteLine((0xFF).ToString());

Littéral booleen

Il y a deux littéraux pour le type bool : true et false.

Les deux sont aussi considérés comme des objets…

 
Sélectionnez
Console ::WriteLine(true.ToString());
Console ::WriteLine(false.ToString());

I-A-7. Les String

Les chaines de caractères sont entourées de doubles quotes comme en C/C++, une chaine créée avec le préfixe L sera traitée comme de l'Unicode.

 
Sélectionnez
String ^ s1= " \x61 " ; //a 
String^ s2="\x612"; // ne vaut pas a2, mais la valeur hexa de la séquence escape Unicode 612

II. Éléments spécifiques de syntaxe :

II-A. Adresse de référence et opérateur d'indirection

Opérateur

Action

&

adresse de

%

référence

*

indirection

L'opérateur adresse de (&) part sa nature d'être un manipulateur de pointeurs, doit être, et est une opération peu sûre (unsafe).

L'opérateur référence a été introduit par nécessité dans le C++/CLI, c'est une conséquence d'un manque syntaxique pour un opérateur sûr pour référencer un handle.

Cet opérateur fournit donc une nette différence syntaxique pour les objets managés et le code non protégé qui utilise l'opérateur adresse de (&).

 
Sélectionnez
int intvar=10 ;
int  %intref =intvar ;
Console ::WriteLine(intref) ;
Intref=20 ;
Console ::WriteLine(intref) ;

II-B. Référence et opérateur d'indirection en action

 
Sélectionnez
ref class Indirectionclass
{
public :
int n ;
Indirectionclass(int x){n=x;}

} ;

Indirectionclass LocalObj(10) ;
Indirectionclass  ^p ;

p =%LocalObj ; // place une référence de LocalObj dans le handle p
Console ::WriteLine(p->n) ; // renvoie 10

int  %i=LocalObj.n ; // LocalObj.n a une référence, attention ne fonctionne pas sur une propriété ( property int n )

i=50;
Console ::WriteLine(p->n) ; // renvoie 50

int ^y= gcnew int(10); // attention c'est bien un int initialisé a 10 et pas un tableau de 10 int..
Console ::WriteLine(y) ;// 10
*y=500 ; // nouvelle valeur par indirection 
Console ::WriteLine(*y) ;

Ce dernier code ne sera pas autorisé si on utilise l'option /clr :safe option.

II-C. Construction des boucles

Quatre types de boucles sont disponibles: while,do while,for,for each.

for each habituellement réservés au Visual basic et au C#, permet l'itération à travers une collection dérivée de l'interface Ienumerable.

Les tableaux en sont un exemple :

 
Sélectionnez
array <int>^ arraynumbers= gcnew array<int>{1,2,3} ;
for each (int i in arraynumbers)
{
Console ::WriteLine(i) ;
}

Pendant l'itération il n'est pas possible de rajouter ou supprimer une occurrence de la collection.
Essayer lèvera une exception.

II-D. Les fonctions

II-D-1. Passer un argument à une fonction

Il y a deux manières de passer un argument à une fonction par valeur et par référence.

Par rapport au C++ nous avons une différence avec l'ajout d'un nouvel opérateur %

Rappel : par valeur, une copie de la variable est passée à la fonction, la modification de celle-ci dans la fonction n'affecte pas la variable d'origine.

Pour que la variable d'origine soit modifiée, nous avons deux manières de procéder.

La première est de passer un handle par valeur, mais le problème c'est que la valeur d'origine ne sera toujours pas modifiée, il faudra adopter une syntaxe plus compliquée parce qu'il faudra déréférencer le handle :

 
Sélectionnez
void function(int ^v)
{
      *v= *v + 100 ;
} 
int  ^nvar=5 ;
function(nvar) ; // nvar vaut 105.

La seconde approche est le passage des arguments par référence.

Rappel : les variables passées par référence ne sont pas copiées, la fonction accède à un alias de l'argument, finalement on peut dire que la fonction manipule l'argument directement.

 
Sélectionnez
void function(int %v)
{
    v= v + 100 ;
} 
int  nvar=5 ;
function(nvar) ; // nvar vaut 105.

II-D-2. Renvoyer une valeur depuis une fonction

On retrouve les mêmes pièges qu'en C et C++ sur le retour de l'adresse d'une variable déclarée localement dans une fonction.

Exemple

Il faut faire attention lorsque l'on retourne un handle depuis une fonction.

Il ne faudra jamais faire ceci :

 
Sélectionnez
ref class MyClass{} ;

MyClass ^ function()
{
MyClass var ;
return %var ; 
}

La variable var est déclarée localement à la fonction et n'existe plus en sortant de la fonction…

Par contre on pourra par exemple retourner un handle qui est passé en argument ou alors un handle qui est créé avec l'opérateur gcnew dans la fonction.

 
Sélectionnez
ref class MyClass{} ;

MyClass ^ function(MyClass ^ var)
{
return var ; // ok 
}

ou :

 
Sélectionnez
MyClass ^ function()
{
            MyClass ^ var= gcnew MyClass(); 
return var ; // ok 
}

II-D-3. Retourner une référence

Même remarque que pour le handle : il ne faut jamais retourner une référence d'une variable déclarée localement dans une fonction.

 
Sélectionnez
ref class MyClass{} ;

MyClass % function()
{
MyClass var ;
return var ; 
}

La variable « var » est déclarée localement à la fonction et n'existe plus en sortant de la fonction…

À la place on pourra retourner un argument passé par référence à la fonction, ou un pointeur sur une référence créée dans la fonction avec l'opérateur gcnew.

 
Sélectionnez
ref class MyClass{} ;

MyClass % function(MyClass % var)
{
return var ; // ok 
}

ou :

 
Sélectionnez
MyClass % function()
{
            MyClass ^ var= gcnew MyClass(); 
return *var ; // ok 
}

III. Programmation objet

III-A. Généralités

Pour les programmeurs issus du C++, le mot clef « ref » n'est pas passé inaperçu, C'est le plus grand et le plus important changement par rapport à un programme C++.

L'utilisation du mot « ref » dit au compilateur que la classe doit être un objet référence sur le tas managé.

Le C++/CLI pour être en accord avec le C++, place par défaut les objets sur le tas CRT et non sur le tas managé.

Si on veut que la classe soit managée, il faudra placer « ref » devant la classe.

III-B. Avantages et inconvénients

En contrepartie une classe managée apporte les avantages suivants.

  • Ramasse-miette mémoire (garbage collector).
  • Héritage depuis n'importe quelle classe de base du framework .net qui n'est pas scellée ou stoppée (sealed), si aucune classe de base n'est spécifiée l'héritage sera automatiquement fait sur la classe System::Object.
  • Possibilité d'utiliser les ref class avec les collections et tableaux du framework.
  • Héritage multiple depuis des interfaces managées.
  • Habilité à contenir des propriétés.
  • Habilité à contenir des pointeurs vers des classes non managées.

Les moins

  • Héritage simple.

    • L'héritage depuis une classe non managée n'est pas permis.
  • Pour les ref class :

    • ne peut être un parent d'un type non managé ;
    • ne peut contenir une surcharge de l'opérateur gcnew ou delete ;
    • doit utiliser un héritage public ;
    • ne peut être utilisée avec l'opérateur sizeof ou offsetof ;
    • les manipulations arithmétiques sur les handles d'une classe ref ne sont pas permises ;
    • pas de surcharge d'une méthode avec des arguments par défaut ;
    • ne peut être un parent d'un type non managé ;
    • ne peut contenir une surcharge de l'opérateur gcnew ou delete ;
    • doit utiliser un héritage public ;
    • ne peut être utilisée avec l'opérateur sizeof ou offsetof ;
    • les manipulations arithmétiques sur les handles d'une classe ref ne sont pas permises ;
    • pas de surcharge d'une méthode avec des arguments par défaut.

III-C. Les constructeurs

Un constructeur est appelé quand une nouvelle instance d'une ref classe est créée, celle-ci est allouée sur le tas managé qui est maintenu par la CLR.

Une différence de taille par rapport aux classes non managées, les variables sont initialisées à zéro avant l'appel effectif du constructeur.

Les initialisations spécifiques seront donc à faire dans le constructeur.

III-C-1. Le constructeur de copie

Le constructeur de copie existe aussi en C++/CLI, la seule différence est l'utilisation de l'opérateur « % » à la place de « & » .

III-C-2. Le constructeur static

C'est une nouveauté par rapport au C++, il est possible de définir un constructeur statique dans une ref class pour initialiser les variables statiques de la classe.

Si des initialisations de variables statiques ont été déjà spécifiées dans la classe leur contenu sera effacé.

III-D. Initialisations des données membres statiques

Une ref classe accepte les fonctions membres statiques comme en c++, ainsi que les données membres static.

Celles-ci devront être initialisées dans la classe.

 
Sélectionnez
ref class sampleclass
{
static int nvar=3 ;
} ;

III-E. Destructeurs

Les destructeurs ont deux objectifs, la libération des ressources allouées par l'opérateur new ou gcnew, et l'éventuelle libération de ressources système.

En ce qui concerne les données allouées par gcnew, elles peuvent être libérées par l'appel à l'opérateur delete comme en C++.

Il ne faut pas oublier que le job du CLR est à même de détecter lorsqu'un objet n'est plus utilisé et de le libérer, de ce fait le meilleur choix est de le laisser faire son boulot et d'omettre les delete pour les allocations d'objets managés.

III-F. Finaliseur

Le C++/Cli dispose d'un autre destructeur appelé finaliseur. Celui-ci est appelé par la CLR quand il détecte que l'objet n'est plus utilisé.

La CLR avant son appel vérifie si l'opérateur delete n'a pas déjà été appelé, si c'est le cas il ne perd pas de temps à appeler le finaliseur…

Le finaliseur a la même syntaxe que le destructeur classique si ce n'est qu'il utilise le symbole « ! » à la place du « ~ », son accès est protégé.

 
Sélectionnez
protected :
  !MyClass() {}

En résumé, dans le destructeur classique on pourra :

  • libérer toutes les ressources managées et non managées et la mémoire.

Pour le finaliseur :

  • on libérera uniquement les ressources non managées et la mémoire.

Dans la pratique on écrira une méthode pour libérer la mémoire sur les ressources non managées, celle-ci devra être appelée dans les deux destructeurs…

III-G. Méthodes virtuelles

Il y a deux méthodes pour substituer/redéfinir (overriding) une fonction virtuelle : une implicite ou une explicite par nommage.

On peut cacher la fonction virtuelle et en démarrer une nouvelle, ou tout simplement stopper la séquence virtuelle de partout.

III-G-1. Substitution implicite d'une fonction virtuelle

Pour une surcharge d'une méthode, celle-ci doit disposer du même nom que celle de la classe de base qui inclut le préfixe virtual.

Le nom de la méthode et les arguments doivent être identiques.

Le type de retour peut ne pas être identique, mais il doit dériver du même type que celui de la classe de base.

La nouveauté, il faudra aussi indiquer le mot clef override après les paramètres.
Exemple :

 
Sélectionnez
virtual void function() override

Le compilateur se chargera assez bien de vous le rappeler en cas d'omission.

 
Sélectionnez
ref class Tools 
{
public:
        virtual void InitTools()
        {
                Console::WriteLine("Tools:InitTools");
        }
};
ref class Brush : public Tools
{
        public:
        virtual void InitTools() override
        {       
                Console::WriteLine("Brush:InitTools");
        }
};
int main(array<System::String ^> ^args)
{    
        Tools ^p1= gcnew Tools;
        p1->InitTools();                 //Tools:InitTools
        Tools ^p2= gcnew Brush;
        p2->InitTools();                // Brush:InitTools
    return 0;
}

III-G-2. Cacher la virtualité

Lorsque la classe parent définit une fonction virtuelle, ce n'est généralement pas sans raison.

Si l'on souhaite annuler la propagation de la virtualité d'une méthode dans les classes filles, on ajoutera le mot clef « new » après les arguments.

Exemple :

virtual void function() new

 
Sélectionnez
ref class Brush : public Tools
{
        public:
        virtual void InitTools() new
        {       
                Console::WriteLine("Brush:InitTools");
        }
};
int main(array<System::String ^> ^args)
{    
        Tools ^p1= gcnew Tools;
        p1->InitTools(); //Tools:InitTools
        Tools ^p2= gcnew Brush;
        p2->InitTools(); // Tools:InitTools
    return 0;
}

III-G-3. Substitution par nommage explicite d'une méthode virtuelle

La substitution explicite nommée permet d'assigner un nom de méthode différent de la fonction virtuelle d'origine.

La nouvelle fonction sera déclarée virtuelle et on lui assignera le nom de la méthode virtuelle qu'elle doit substituer.

 
Sélectionnez
ref class Tools 
{
public:
        virtual void InitTools()
        {
                Console::WriteLine("Tools:InitTools");
        }
};
ref class Brush : public Tools
{
        public:
        virtual void CreateBrush() = Tools::InitTools
        {       
                Tools::InitTools();
                Console::WriteLine("Brush:CreateBrush");
        }
};
int main(array<System::String ^> ^args)
{    
    Tools ^p1= gcnew Tools;
    p1->InitTools(); //Tools:InitTools
    Tools ^p2= gcnew Brush;
    p2->InitTools();//Brush:CreateBrush
    return 0;
}

Un peu plus compliqué, il est possible d'exprimer deux surcharges différentes pour une seule méthode virtuelle.

 
Sélectionnez
ref class Tools 
{
public:
        virtual void InitTools()
        {
                Console::WriteLine("Tools:InitTools");
        }
};

ref class ContextBar: public Tools
{
public:
        virtual void InitTools() new
        {
                Console::WriteLine("ContextBar:InitTools");
        }
};
ref class ToolBar : public ContextBar
{
public:
        virtual void InitTools() override = Tools::InitTools
        {
                Console::WriteLine("ToolBar:InitTools");
        }
};


int main(array<System::String ^> ^args)
{    
        Tools ^p1= gcnew ContextBar;
        p1->InitTools();
        ContextBar ^p2= gcnew ContextBar;
        p2->InitTools(); //ContextBar:InitTools

        Tools ^p3= gcnew ContextBar;
        p3->InitTools(); //Tools:InitTools

        ToolBar ^p4= gcnew ToolBar;
        p4->InitTools(); //ToolBar:InitTools
        Tools ^p5=gcnew ToolBar;
        p5->InitTools(); //ToolBar:InitTools

ContextBar ^p6=gcnew ToolBar;
        p6->InitTools(); //ToolBar:InitTools
    return 0;
}

On peut obtenir le même résultat en spécifiant la liste des surcharges, La seule contrainte étant de spécifier un autre nom pour la fonction de surcharge.

 
Sélectionnez
ref class Tools 
{
public:
        virtual void InitTools() 
        {
                Console::WriteLine("Tools:InitTools");
        }
};
ref class ContextBar: public Tools
{
public:
        virtual void InitTools() new
        {
                Console::WriteLine("ContextBar:InitTools");
        }
};
ref class ToolBar : public ContextBar
{
public:
        virtual void InitToolBar() = Tools::InitTools,ContextBar::InitTools
        {
                Console::WriteLine("ToolBar:InitTools");
        }
};

III-G-4. Méthode virtuelle pure

Comme en C++ il est possible de définir une méthode virtuelle pure pour forcer la définition de cette méthode dans les classes filles.

Une méthode virtuelle pure dans une ref classe class interdit d'instancier cette classe.

La classe doit donc être héritée pour être utilisée…

Dernier point une méthode virtuelle pure ne peut être cachée avec l'opérateur « new ».

Rappel syntaxique :

virtual void PureFunction() = 0 ;

III-H. Méthode surchargée

Pour les habitués du C++ les méthodes surchargées n'ont plus de secrets, et l'utilisation des arguments par défaut est un moyen simple d'y parvenir.

Malheureusement en C++/CLI les ref classes ne supportent pas les arguments par défaut !

La seule solution qui nous reste est de faire différentes méthodes surchargées du même nom .
Pas terrible…

III-H-1. Surcharge d'opérateurs managés

La ref class du C++/CLI supporte la surcharge des opérateurs comme en C++. Mais avec une syntaxe un peu différente.

Les opérateurs surchargés managés doivent être déclarés static, la conséquence étant qu'il faudra pour :

  • les opérateurs binaires passer les deux arguments dans la fonction ;
  • pour les unaires, passer le paramètre de gauche à la fonction.

Cette syntaxe est particulière si on désire garder la compatibilité en développement multilangages.

Sinon la syntaxe traditionnelle reste disponible.

À travers cette possibilité on voit bien que C++/CLI se positionne en tant que langage d'interopérabilité.

Exemple d'opérateur surchargé :

 
Sélectionnez
void operator *=(const MyClass ^ righth); // syntaxe classique.
static void operator *=(const MyClass ^ lefth, const MyClass ^ righth); // syntaxe managée

III-H-2. Surcharge d'opérateurs unaires

Les opérateurs unaires sont des opérateurs qui ne prennent qu'un argument : !,~,++,--,-,*,%,&.

On pourra les écrire selon deux formes, une traditionnelle avec l'argument constant, l'autre dite mutable.

Attention: les opérateurs *,%,& sont unaires et binaires suivant le contexte.

'*' correspond à l'opérateur de multiplication (binaire), mais aussi à l'opérateur d'indirection (unaire).

De même que & désigne trois choses différentes, il est utilisé pour la spécification de référence, il désigne aussi l'opérateur logique ET bit à bit, et il représente l'opérateur « adresse de » l'ambiguïté sera levée suivant le contexte d'utilisation.

 
Sélectionnez
static MyClass ^operator -(const MyClass ^lefth) // opérateur changement de signe en - 
{
    MyClass ^ ret=gcnew MyClass();
    ret->i=-(lefth->i);
return ret;
}

La forme mutable :

 
Sélectionnez
static MyClass ^operator -( MyClass ^lefth)
{
    lefth->i=-(lefth->i);
return lefth;
}

La forme « const » est toutefois préférable.

III-H-3. Surcharge d'opérateurs binaires

Les opérateurs binaires sont des opérateurs qui prennent deux arguments.

 
Sélectionnez
ref class MyClass
{
public:
        MyClass(int x){m_nx=x;}
        MyClass(){}
        static bool operator ==(const MyClass ^lefth,const MyClass ^righth)
        {
                return lefth->m_nx==righth->m_nx;
        }
        static bool operator ==(const MyClass ^lefth,int n)
        {
                return lefth->m_nx==n;
        }
        static bool operator !=(const MyClass ^lefth,const MyClass ^righth)
        {
                return lefth->m_nx!=righth->m_nx;
        }

        static MyClass ^ operator -=(const MyClass ^lefth,const MyClass ^righth)
        {
                MyClass ^ret=gcnew MyClass;
                ret->m_nx=(lefth->m_nx-righth->m_nx);
                return ret;
        }

        static void operator -=(MyClass ^lefth,int n)
        {
                lefth->m_nx-=n;         
        }
        void operator *=(const int n)   // syntaxe classique.
        {
                m_nx*=n;
        }
        int m_nx;
};
int main(array<System::String ^> ^args)
{
        MyClass ^obj= gcnew MyClass(10);
        MyClass ^obj2= gcnew MyClass;
        Console ::WriteLine("obj==obj2 "+(obj==obj2).ToString()) ;
        Console ::WriteLine("obj==10 "+(obj==10).ToString()) ;
        obj2->m_nx=10;
        Console ::WriteLine("obj!=obj2 "+(obj!=obj2).ToString());
        obj2->m_nx=5;
        MyClass ^ obj3=obj2-=obj;
        obj-=8;
        obj*=2;
        Console ::WriteLine("obj "+(obj->m_nx).ToString());
        Console ::WriteLine("obj2 "+(obj2->m_nx).ToString());
        Console ::WriteLine("obj3 "+(obj3->m_nx).ToString());

        return 0;
}

III-I. Les propriétés membres

Le but des propriétés est de permettre une encapsulation de variables membres en les cachant à l'utilisateur tout en permettant leurs accès contrôlés par des méthodes d'accesseurs et modificateurs (getter and setter).

Évitant du coup de définir des fonctions spécifiques pour permettre leur accès comme on le ferait en C++ classique.

L'accès aux variables se faisant par leurs noms, l'utilisation en est simplifiée.

Pour déclarer une simple variable propriété, on placera le mot clef « property » devant sa définition :
property typevar PropertyVar.

Une variable « property » va nous permettre de contrôler l'accès à sa variable en donnant des accès lecture seule ou écriture seule ou les deux :

 
Sélectionnez
property type PropertyVarName
{
type get(){}
void set(type value){}
}

Exemple :

 
Sélectionnez
ref class RefClass
{
public:
        RefClass(int x){m_x=x;}
        property int Intx
        {
                int get(){return m_x;}
                void set(int value){m_x=value;}
        }

        void Show()
        {
                Console::WriteLine(m_x);
        }
private:
        int m_x; // variable cachée pour l'utilisateur
};

RefClass r(8);
r.Intx=10;
Console::WriteLine(r.Intx);
r.Show();

Dans cet exemple aucun traitement ou contrôle n'a été réalisé, on s'est contenté d'affecter ou de récupérer la valeur de la variable interne dans ce cas précis la simple déclaration de la variable en tant que propriété suffit :

 
Sélectionnez
ref class RefClass
{
public:
        RefClass(int x){ Intx =x;}
        property int Intx;
        
        void Show()
        {
                Console::WriteLine(Intx);
        }
};

RefClass ^p= gcnew RefClass(10);
p->Show();
p->Intx=20;
p->Show();

III-I-1. Les Propriétés static

Il est tout à fait possible de définir une propriété statique, il suffira de rajouter le mot clef static derrière « property ».

La syntaxe est la suivante :

 
Sélectionnez
property static TypeVar  PropertyName
{
   TypeVar TypeVar get{}
   void set(TypeVar  value){}
}

III-I-2. Les propriétés tableau (array)

Le C++/CLI fournit une syntaxe simple pour un tableau de propriétés.

Exemple :

 
Sélectionnez
property array < int >^ nArray
{
array < int >^ get{}
void set(array < int >^ value}
}

Exemple :

 
Sélectionnez
ref class MyClass
{
public:
        MyClass(int nSize)
        {
                m_nIntArray= gcnew array< int >(nSize);
        }
        property array < int >^ nIntArray
        {
                array < int >^ get()
                {
                        return m_nIntArray;
                }
                void set(array < int >^ value
                {
               m_nIntArray=value;
                }
        }

private:
        array < int >^ m_nIntArray; // variable cachée pour l'utilisateur
};
int main(array<System::String ^> ^args)
{
    MyClass Obj(10);
        for(int i=0;i< Obj.nIntArray->Length;i++)       Obj.nIntArray[i]=(i*2);
        return 0;
}

III-I-3. Les propriétés indexées

Les propriétés indexées permettent de gérer l'indexation sur une variable propriété.

Qu'elle est la différence avec une propriété tableau ?

Alors que la propriété tableau fournit la variable tableau interne, les propriétés indexées permettent de gérer l'accès à l'élément, la variable interne ne sera donc pas forcément un tableau …, ou permettra tout simplement de s'assurer que l'index demandé pour un tableau soit correct.

Syntaxe :

 
Sélectionnez
Property TypeVar PropertyName [indexType,. . .,indexTypex]
{
        TypeVar get(indexType,. . .,indexTypex){};
        void set(indexType,. . .,indexTypex,TypeVar value){};
}

Exemple avec le contrôle des bornes d'un tableau :

 
Sélectionnez
ref class MyClass
{
public:
        MyClass(int nSize)
        {
                m_nIntArray= gcnew array< int >(nSize);
        }
        property int nIntArray [ int ]
        {
                int get(int n)
                {
                        if(n<0) n=0;
                        else
                        if(n>=m_nIntArray->Length) n=m_nIntArray->Length-1;
                        
                        return m_nIntArray[n];
                }
                void set(int nIndex,int nvalue)
                {
                        if(nIndex<0) nIndex=0;
                        else
                        if(nIndex>=m_nIntArray->Length) nIndex=m_nIntArray->Length-1;
                m_nIntArray[nIndex]=nvalue;
                }

        }
        int GetSizeArray(){return m_nIntArray->Length;}
private:
        array < int >^ m_nIntArray; // variable cachée pour l'utilisateur
};
int main(array<System::String ^> ^args)
{
        MyClass obj(10);
        for(int i=-1;i<12;i++) obj.nIntArray[i]=i*2;
        for(int i=0;i<obj.GetSizeArray();i++) Console ::WriteLine((i).ToString()+":="+obj.nIntArray[i].ToString());
    return 0;
}

III-I-4. Les propriétés indexées par défaut

La propriété indexée par défaut permet d'appliquer l'indexation directement sur l'instance de la classe à laquelle appartient l'objet.

La syntaxe est la même que pour les propriétés indexées, il faudra juste rajouter le mot clef default en lieu et place du nom de la propriété.

Exemple :

 
Sélectionnez
ref class IndexedProperty
{
public:
        IndexedProperty()
        {
                m_array= gcnew array<int>{1,2,3,10,100,200};
        }
        property int ^ default [int]
        {
                int ^get(int index)
                {
                        if(index<0) index=0;
                        else if(index>m_array->Length) index=m_array->Length-1;
                        return m_array[index];
                }
        }
private:
        array<int>^ m_array;
};

int main(array<System::String ^> ^args)
{    
        IndexedProperty TestIndex;

        Console::WriteLine(TestIndex[-10]);
        Console::WriteLine(TestIndex[2]);
        Console::WriteLine(TestIndex[10]);
      return 0;
}

III-J. Gestion des transtypages entre classes

De même qu'en C++, les opérations de transtypage d'une classe à l'autre sont possibles.

Néanmoins le transtypage avec static_cast du C++ ne peut être vérifié et doit être considéré comme peu sûr (unsafe). On lui préférera le mot clef safe_cast.

Quant à dynamic_cast, il fonctionne comme en C++ et renverra nul si le transtypage n'est pas correct.

III-K. Les différents types de classes

III-K-1. Les classes ref sealed

Une classe ref sealed (scellée) ne peut être héritée.

Il faut vraiment avoir un motif particulier pour empêcher l'héritage sur une classe ref.

 
Sélectionnez
ref class MySealClass sealed
{
};
ref class DerivedClass: public MySealClass //error C3246: 'DerivedClass' : ne peut pas hériter de 'MySealClass',
{                                       //,  car il a été déclaré comme 'sealed' 
};

III-K-2. Les classes ref Abstraites

Une classe ref abstraite se construit comme une classe ref classique sans son implémentation.

Elle doit posséder au minimum une fonction virtuelle pure.

Elle peut posséder des variables, des méthodes, des propriétés, des constructeurs et des destructeurs.

Mais elle ne pourra pas être instanciée directement, seule une classe héritée de cette classe pourra être instanciée.

Exemple :

 
Sélectionnez
ref class AbstractClass abstract 
{
public:
        AbstractClass()
        {
                m_n=100;
        }
        virtual void function()=0;
        void GetNumber()
        {               
                Console::WriteLine(m_n.ToString());
        }
private:
        int m_n;
};

III-K-3. Classe Interface

Le fonctionnement est le même que pour une classe abstraite, si ce n'est que :

  • l'accès doit être public, et donc par défaut c'est ce mode qui est actif ;
  • la classe ne peut être composée que de fonctions virtuelles pures.

Donc pas de variables ou méthodes.

On pourra éviter le mot clef virtual et =0 puisque la classe est explicitement déclarée comme une interface.

Exemple :

 
Sélectionnez
interface class MyInterfaceClass
{
        void Function();
};
ref class MyClass
{
        void OneFunction(){Console::WriteLine("OneFunction");}
};
ref class MyFinalClass: public MyClass, public MyInterfaceClass
{
public:
        void Function(){Console::WriteLine("Function");}
};

III-K-4. Les classes génériques

Elles sont similaires aux templates du C++.

Quelles sont les différences ?

  • Les spécialisations ne sont pas autorisées et il n'y a pas de paramètre par défaut.
  • Elles disposent d'un système de contrainte du type.
  • Elles ont un runtime, les objets génériques sont vérifiables.

Autres différences importantes

  • Pour un template tout se passe à la compilation du code, pour une classe générique c'est à l'exécution…
  • Une classe générique pourra être utilisée par les autres langages .net un template non .

Exemples

On pourrait être tenté par ce type de code

 
Sélectionnez
generic<typename T>
T Min(T a,T b)
{
        return(a<b?a:b); // error C2676
}

Ce code provoque l'erreur de compilation C2676 qui me dit que le type T ne supporte pas l'opérateur binaire « < » …

Après recherche, une fonction ou classe générique ne supporte pas l'utilisation des opérateurs sur le paramètre.

C'est une limitation dont il faudra tenir compte.

Il semble que certaines versions du compilateur supportent ce type de code, c'est une bizarrerie que j'ai constatée avec mon collègue NicoPyright(c).

Mes recherches sur le sujet m'ont amené à une discussion sur le forum MSDN où Herb Sutter lui-même disait que cette fonctionnalité n'était pas supportée dans les langages .Net, d'ailleurs en C# ce code ne compile pas.

À suivre…

La déclaration d'une classe générique se fait de la même manière qu'une classe template :

 
Sélectionnez
generic <class T>
ref class Point
{
public:
        Point(T x,T y){xCoord=x;yCoord=y;}
        void ShowCoord()
        {
                Console::WriteLine("XCoord:"+(xCoord)->ToString());
                Console::WriteLine("YCoord:"+(yCoord)->ToString());
        }
        static bool operator ==(Point ^lefth,Point ^righth)
        {
                // conversion en string ,ce n'est pas terrible c'est juste pour l'illustration...
                String ^lx=lefth->xCoord->ToString();
                String ^rx=righth->xCoord->ToString();
                String ^ly=lefth->yCoord->ToString();
                String ^ry=righth->yCoord->ToString();

                return (lx==rx && ly==ry);
        }
        static bool operator ==(Point %left,Point %right)
        {
                // conversion en string ,ce n'est pas terrible c'est juste pour l'illustration...
                String ^lx=left.xCoord->ToString();
                String ^rx=right.xCoord->ToString();
                String ^ly=left.yCoord->ToString();
                String ^ry=right.yCoord->ToString();

                return (lx==rx && ly==ry);
        }
property T xCoord;
property T yCoord;
};
int main(array<System::String ^> ^args)
{
        Point<int>^ pt= gcnew Point<int>(10,100);
        pt->ShowCoord();
        Point<int>^ pt2= gcnew Point<int>(10,100);
        Console::WriteLine("pt==pt2 "+(pt==pt2).ToString());

        Point<int> pt3(10,100);
        pt3.ShowCoord();
        Point<int> pt4(10,100);
        Console::WriteLine("pt3==pt4 "+((pt3)==(pt4)).ToString());
    return 0;
}

Dans cet exemple, j'ai contourné le problème lié à l'utilisation d'un opérateur sur le type en paramètre, en le convertissant en String (pour l'instant) pour réaliser le test.

III-K-4-a. Définition d'une contrainte de type

Les contraintes permettent de spécifier le type accepté en paramètre.

La syntaxe générale est la suivante :

where typeParametre : [class contrainte,] [interface liste des contraintes]

La contrainte peut être une classe ou une interface comme IComparable ou Ienumerable.

Exemples d'écritures issus de MSDN

Une contrainte avec une interface :

 
Sélectionnez
interface class IItem {};

generic <class ItemType> where ItemType : IItem
ref class Stack
{
};

Une contrainte avec une classe :

 
Sélectionnez
generic <typename T>
ref class G1 {};

generic <typename Type1, typename Type2> where Type1 : G1<Type2>   // OK, G1 takes one type parameter
ref class G2
{
};

Pour montrer l'utilisation d'une contrainte de type j'ai repris l'exemple précédent de la classe générique Point.

 
Sélectionnez
generic <class T> where T: IComparable
ref class Point 
{
public:
        Point(T x,T y){xCoord=x;yCoord=y;}
        void ShowCoord()
        {
                Console::WriteLine("XCoord:"+(xCoord)->ToString());
                Console::WriteLine("YCoord:"+(yCoord)->ToString());
        }
        
        static bool operator ==( Point ^lefth, Point ^righth)
        {
                return !lefth->xCoord->CompareTo(righth->xCoord) &&
                           !lefth->yCoord->CompareTo(righth->yCoord);
        }
        static bool operator ==(Point %left,Point %right)
        {
                return !left.xCoord->CompareTo(right.xCoord) &&
                           !left.yCoord->CompareTo(right.yCoord);
                
        }
property T xCoord;
property T yCoord;
};

int main(array<System::String ^> ^args)
{
        Point< int >^ pt= gcnew Point< Int32>(10,100); // j'ai utilisé un int classique pourquoi ça fonctionne ?
        pt->ShowCoord();
        Point< Int32 >^ pt2= gcnew Point< Int32 >(10,100);
        Console::WriteLine("pt==pt2 "+(pt==pt2).ToString());

        Point< Int32 > pt3(10,100);
        pt3.ShowCoord();
        Point< Int32> pt4(10,100);
        Console::WriteLine("pt3==pt4 "+((pt3)==(pt4)).ToString());
    return 0;
}
}

j'ai spécifié que l'argument T devait hériter de l'interface Icomparable.

Si vous vous rappelez de ce qui a été dit sur les types de données: ils disposent tous d'alias dans le framework.

Ainsi maintenant je peux utiliser un int ou son alias Int32 définit dans l'espace de nom Systeme, pourquoi ?

Ces alias sont des value class héritées (entre autres) de l'interface Icomparable.

la difficulté rencontrée précédemment n'en est plus une…

Enfin notre fonction générique Min pourra donc s'écrire comme suit :

 
Sélectionnez
generic<class T> where T: IComparable
T Min(T a,T b)
{
        return(a->CompareTo(b)<0?a:b);
}
int main(array<System::String ^> ^args)
{
        int a=20;
        int b=10;
        Console::WriteLine(Min(a ,b).ToString());

        Console::WriteLine(Min<int>(20,10).ToString());

    return 0;
}

III-K-5. Les Templates

Il n'y pas de différences avec le C++ si ce n'est la possibilité d'utiliser une classe référence, On pourra donc définir classiquement une fonction template :

 
Sélectionnez
template<typename T>
T Min(T a,T b)
{
    return(a<b?a:b);
}
int main(array<System::String ^> ^args)
{
    int  n=10;
    int n2=20;
    Console::WriteLine(Min(n,n2));
}

Et déclarer une ref class template :

 
Sélectionnez
template <class T>
ref class Point
{
public:
        Point(T x,T y){xCoord=x;yCoord=y;}
        void ShowCoord()
        {
                Console::WriteLine("XCoord:"+(xCoord).ToString());
                Console::WriteLine("YCoord:"+(yCoord).ToString());
        }
        static bool operator ==(Point ^lefth,Point ^righth)
        {
                return (lefth->xCoord==righth->xCoord && lefth->yCoord==righth->yCoord);
        }
        static bool operator ==(Point %left,Point %right)
        {
                return (left.xCoord==right.xCoord && left.yCoord==right.yCoord);
        }
property T xCoord;
property T yCoord;
};
int main(array<System::String ^> ^args)
{
        Point<int>^ pt= gcnew Point<int>(10,100);
        pt->ShowCoord();
        Point<int>^ pt2= gcnew Point<int>(10,100);
        Console::WriteLine("pt==pt2 "+(pt==pt2).ToString());

        Point<int> pt3(10,100);
        pt3.ShowCoord();
        Point<int> pt4(10,100);
        Console::WriteLine("pt3==pt4 "+(pt3==pt4).ToString());
    return 0;
}

Vous remarquerez dans cet exemple.

L'utilisation des propriétés, et la surcharge de l'opérateur == supportant les deux formes de création de l'objet: par handle ou sur la pile.

De même qu'en C++ notre classe ref template supportera la spécialisation complète ou partielle de la classe.

III-L. Les Exceptions

Elles se codent de la même manière qu'en C++ avec un bloc « try catch ».

 
Sélectionnez
try
{
// votre code qui doit être contrôlé
}
catch(ExceptionType e)
{
// code d'interception
}

On pourra à la suite du catch ajouter d'autres catch pour d'autres interceptions spécifiques.

III-L-1. Exception spécialisée

Le Framework possède dans l'espace de noms Systeme une exception InvalidCastException qui pourra être utilisée conjointement avec l'opérateur safe_cast.

L'exemple qui suit est une adaptation d'un exemple C# trouvé sur MSDN :

 
Sélectionnez
ref class Employee 
{
public :
   static void PromoteEmployee(Object ^emp)
   {
   //Cast object to Employee.
   Employee ^e = (Employee ^) emp;
   // Increment employee level.
   e->EmLevel = e->EmLevel + 1; 
   }
        property int EmLevel;
};
try
  {
        Object ^o = gcnew Employee;
        DateTime ^newyears = gcnew DateTime(2001, 1, 1);
        //Promote the new employee.
        Employee::PromoteEmployee(o);
        //Promote DateTime; results in InvalidCastException as newyears is not an employee instance.
        Employee::PromoteEmployee(newyears);
   }
   catch (InvalidCastException ^e)
   {
           Console::WriteLine("Error passing data to PromoteEmployee method. " + e);
   }

Dans cet exemple j'ai volontairement employé le cast du C qui provoquera la levée de l'exception sur la deuxième tentative de promotion.

Si j'avais utilisé l'opérateur static_cast l'exception ArgumentException aurait été levée.

Si j'avais utilisé l'opérateur safe_cast j'aurai eu le même résultat qu'avec le cast C, ceci est dû au fait que le cast C utilise en interne safe_cast.

Le paradoxe n'aura pas échappé au programmeur C++ utilisant static_cast dans ses sources au lieu du cast C, en C++/CLI il faudra mettre de coté cette bonne habitude et utiliser safe_cast ou revenir au bon vieux cast du C.

III-L-2. La classe de base Exception du framework

Le Framework .net dispose d'une classe de base pour la gestion des exceptions : Exception.

Deux types d'exceptions sont gérés :

  • les exceptions d'application: ApplicationException. Cette classe de base permettra de gérer nos propres exceptions ;
  • les exceptions systèmes : SystemException. Elles gèrent toutes les exceptions d'entrées/sorties, etc.
III-L-2-a. Interception des exceptions utilisateurs

J'ai repris l'exemple précédent en le modifiant pour lever ma propre exception :

 
Sélectionnez
ref class EmployeeException : public ApplicationException
{
public:
        EmployeeException(System::String ^err):ApplicationException(err)
        {
        }
};

ref class Employee 
{
public :
   static void PromoteEmployee(Object ^emp)
   {
           Employee ^e;
           //Cast object to Employee.
           try
           {
                 e= safe_cast<Employee ^> (emp);
           }
           catch(InvalidCastException ^err)
           {
                throw gcnew EmployeeException("\nErreur de conversion sur le type Employé "+err);
                return;
           }
                // Increment employee level.
                e->EmLevel = e->EmLevel + 1; 
   }
        property int EmLevel;
};
int main(array<System::String ^> ^args)
{
      Object ^o = gcnew Employee;
        DateTime ^newyears = gcnew DateTime(2001, 1, 1);
        //Promote the new employee.
        Employee::PromoteEmployee(o);
        //Promote DateTime; results in InvalidCastException as newyears is not an employee instance.
        Employee::PromoteEmployee(newyears);
return 0;
}

III-M. Delegates

Un delegate est une classe managée (ref class) qui permet d'appeler une méthode qui partage la même signature qu'une fonction globale ou qu'une classe possédant des méthodes avec cette même signature.

Le framework supporte deux formes de delegates :

  • System::Delegate un delegate qui accepte d'appeler uniquement une méthode ;
  • System::MulticastDelegate : un delegate qui accepte d'appeler une chaine de méthodes.

Les méthodes utilisées pour le delegate peuvent être : Globale (comme en C), une méthode statique à une classe, une méthode d'une instance.

Exemple : Déclaration du delegate :

 
Sélectionnez
delegate void FuncMessDelegate(String ^mess);

Maintenant les trois formes d'utilisation :

 
Sélectionnez
void GlobalMess(String ^mess)
{
        Console::Write("Fonction Globale: ");
        Console::WriteLine(mess);
}
ref class OneClass
{
public:
        static void staticMethodeMess(String ^mess)
        {
                Console::Write("Fonction statique: ");
                Console::WriteLine(mess);
        }
};
ref class AnotherClass
{
        public:
        void MethodeMess(String ^mess)
        {
                Console::Write("Fonction d'une instance: ");
                Console::WriteLine(mess);
        }
};

L'appel de la fonction :

 
Sélectionnez
int main(array<System::String ^> ^args)
{
// declaration du delegate
        FuncMessDelegate ^Global= gcnew FuncMessDelegate(&GlobalMess);
        // ajouter une fonction au delegate
        FuncMessDelegate ^statique= gcnew FuncMessDelegate(&OneClass::staticMethodeMess);

        AnotherClass ^pAnotherClass= gcnew AnotherClass;

        FuncMessDelegate ^instance= gcnew FuncMessDelegate(pAnotherClass,&AnotherClass::MethodeMess);

        // l'appel de la fonction&#8230;
        Global->Invoke("Hello");

        statique->Invoke("Hello");
        instance->Invoke("Hello");

    return 0;
}

Les delegates peuvent être combinés sous forme de chaine et un élément peut être supprimé.

On utilisera la méthode Combine() ou l'opérateur « + » pour l'ajout.

Et la méthode Remove() ou l'opérateur « - » pour la suppression.

Exemple :

 
Sélectionnez
FuncMessDelegate ^ChaineMess= gcnew FuncMessDelegate(&GlobalMess);

ChaineMess += gcnew FuncMessDelegate(&OneClass::staticMethodeMess);

AnotherClass ^pAnotherClass= gcnew AnotherClass;
ChaineMess += gcnew FuncMessDelegate(pAnotherClass,&AnotherClass::MethodeMess);

ChaineMess->Invoke("Hello");

ChaineMess -= gcnew FuncMessDelegate(&OneClass::staticMethodeMess);
ChaineMess->Invoke("Hello2");

Note : j'ai volontairement utilisé les opérateurs à la place des méthodes, car ceux-ci imposent un cast vers le delegate déclaré, ce qui alourdit grandement l'écriture.

III-N. Events

Un event est une implémentation spécifique du delegate ou plutôt du multicast delegate.

Un event permet à partir d'une classe d'appeler des méthodes situées dans d'autres classes sans rien connaitre de ces classes.

Il est ainsi possible pour une classe d'appeler une chaine de méthodes issue de différentes classes.

Exemple :

 
Sélectionnez
using namespace System;

delegate void DelegateAnalyse(int %n); // la fonction de traitement reçoit une référence sur un entier.

// classe declencheur de traitment
ref class AnalyseTrigger
{
public:
    event DelegateAnalyse ^OnWork;
    void RunWork(int %n)
    {
        OnWork(n);
    }
};

// classe effectuant un traitement
ref class Analyse
{
    public:
    AnalyseTrigger ^ m_AnalyseTrigger;
    Analyse(AnalyseTrigger ^src)
    {
        if(src==nullptr)
            throw gcnew ArgumentNullException("erreur argument non specifié");
        m_AnalyseTrigger=src;
        m_AnalyseTrigger->OnWork+= gcnew DelegateAnalyse(this,&Analyse::Treatment);
        m_AnalyseTrigger->OnWork+= gcnew DelegateAnalyse(this,&Analyse::TreatmentTwo);
    }
    void RemoveTreatmentTwo()
    {
        m_AnalyseTrigger->OnWork-=gcnew DelegateAnalyse(this,&Analyse::TreatmentTwo);
    }
    // traitement déclenché
    void Treatment(int %n)
    {
        Console::Write("Class Analyse Treatment =+10 n:= ");
        Console::Write(n);
        n+=10;
        Console::Write(" -> n=");
        Console::WriteLine(n);
    }
    // traitement déclenché
    void TreatmentTwo(int %n)
    {
        Console::Write("Class Analyse TreatmentTwo =*2 n:= ");
        Console::Write(n);
        n*=2;
        Console::Write(" -> n=");
        Console::WriteLine(n);
    }
};

// classe effectuant un traitement
ref class AnalyseTwo
{
    public:
    AnalyseTrigger ^m_AnalyseTrigger;

    AnalyseTwo(AnalyseTrigger ^src)
    {
        if(src==nullptr)
            throw gcnew ArgumentNullException("erreur argument non specifié");
        m_AnalyseTrigger=src;
        m_AnalyseTrigger->OnWork+= gcnew DelegateAnalyse(this,&AnalyseTwo::Treatment);
        
    }
    // traitement déclenché
    void Treatment(int %n)
    {
        Console::Write("Class AnalyseTwo Treatment =+2 n:= ");
        Console::Write(n);
        n+=2;
        Console::Write(" -> n=");
        Console::WriteLine(n);
    }
};


int main(array<System::String ^> ^args)
{
    //element declecheur de l'action fixée par le delegate DelegateAnalyse 
    AnalyseTrigger ^Trigger = gcnew AnalyseTrigger();

    // classe traitements
    Analyse ^analyse = gcnew Analyse(Trigger);

    // classe traitements
    AnalyseTwo ^analyseTwo = gcnew AnalyseTwo(Trigger);

    int n=2;

    Trigger->RunWork(n);
    analyse->RemoveTreatmentTwo();

    Console::WriteLine("-----------------------");
    n=2;
    Trigger->RunWork(n);

    return 0;
}

IV. Conclusion

Pour les programmeurs C++

  • Le C++/CLI est l'occasion d'aborder le monde .Net avec un langage familier.
  • C'est aussi une opportunité de faire évoluer une application existante en C++ en utilisant les outils du framework .Net.
  • Comme je l'ai précisé en introduction, un programme MFC peut être enrichi de Winform, et pourquoi pas utiliser une vue WPF.
  • Vous l'aurez compris le formidable avantage du C++/CLI c'est la récupération de votre code C++ de vos projets et la possibilité d'interopérabilité avec les autres langages .NET.
  • Vous voilà armé avec les éléments de base syntaxiques du C++/CLI, il ne vous reste plus qu'à mettre en pratique.
  • J'ai volontairement mis de côté certains sujets comme les collections de données, la gestion des fichiers, etc., qui seront prétextes à un autre article. Je vous invite à consulter la FaqC++/CLI qui traite aussi nombre de points abordés dans cet article.

Pour les autres programmeurs .NET

  • Quel intérêt un programmeur C# (par exemple) peut-il avoir à regarder le C++/CLI ?
  • La possibilité d'interfacer son application avec du code C++ ou C existant, le tout encapsulé dans une assembly.
  • C'est un argument à méditer pour la récupération de code existant ou tout simplement l'utilisation d'une bibliothèque sans équivalence dans le langage .NET pratiqué.

V. Remerciements

Je remercie toute l'équipe C++, particulièrement Nicopyright(c), l'équipe Dotnet, pour leur relecture attentive du document.

Merci à Skalp pour la relecture finale de cet article.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © 2013 Farscape. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.