FAQ C++/CLI et VC++.Net
FAQ C++/CLI et VC++.NetConsultez toutes les FAQ
Nombre d'auteurs : 29, nombre de questions : 248, création le 22 février 2013
- Qu'est-ce qu'un handle d'objet (^) ?
- Qu'est-ce qu'une tracking reference (%) ?
- Comment utiliser le référencement et le déférencement ?
- Comment modifier la valeur d'un objet pointé par un handle passé en paramètre d'une fonction ?
- Qu'est-ce qu'un pointeur interne ?
- Qu'est-ce qu'un pointeur épingle (pin_ptr) ?
- Comment allouer un objet managé avec gcnew ?
- Quel est la différence entre le destructeur et le finalizer ?
- Qu'est-ce qu'un espace de nom (namespace) ?
- Comment créer une fonction avec un nombre d'arguments variable ?
- Quelles sont les visibilités ajoutées par le C++/CLI ?
- Qu'est ce qu'un delegate ?
- Qu'est ce qu'un Event ?
- Comment faire de la destruction déterministe en C++/CLI (auto_handle) ?
- Comment utiliser un mot clé comme nom de variable ?
- Peut-on utiliser l'opérateur ?? du C# ?
- Peut-on utiliser des classes partielles (partial) ?
- 3.2.1. Cast (3)
On utilise l'opérateur carret (ou hat) [^] pour définir un handle vers un objet managé qui est en fait une référence vers cet objet managé. Attention ce n'est pas un pointeur. Un handle est une référence sur un objet managé sur le tas (heap) managé, alors que les pointeurs pointent vers une zone mémoire.
Pour allouer un objet managé et disposer d'un handle sur cet objet, on utilise gcnew.
Une tracking reference est similaire à une référence C++ dans la mesure où elle représente un alias pour un objet / type définit sur le heap CLR.
Bien que les tracking references soient créés sur la pile, elles peuvent référencer un type sur la pile ou un handle dans le heap CLR.
On utilise l'opérateur % pour définir une telle référence. On s'en sert souvent pour pouvoir modifier un paramètre dans une fonction.
int
entier =
1
;
int
%
reference =
entier;
reference =
2
;
Après ces lignes, entier vaut 2.
Il s'avère utile de comprendre les handles et les références, comme en C++.
Voyons cet exemple :
int
main(array<
System::
String ^>
^
args)
{
int
^
x =
5
;
x =
*
x +
2
;
int
y =
6
;
int
%
z =
y;
inc(*
x);
inc(y);
inc(z);
Console::
WriteLine("x : {0} / y : {1}"
, x,y);
Console::
WriteLine("Somme : {0} "
, add(x,y));
}
void
inc(int
%
a)
{
a++
;
}
int
add(int
^
a, int
^
b)
{
return
*
a +
*
b;
}
- On crée un handle d'entier x. La valeur de l'entier x vaut 5.
- On ajoute 2 à x, il vaut donc 7. Notez le déréférencement de x dans la partie droite qui permet d'accéder à sa valeur. Le déréférencement de x à gauche est facultatif, le compilateur le faisant pour nous.
- On déclare un entier y dont la valeur est 6.
- On créé une référence CLI z qui est un alias de y. Notez qu'on ne peut pas créer une référence CLI sur un handle, dans le cas de x, il faudrait évidement le déréférencer (int %z = *x).
- La fonction inc attend une référence afin de pouvoir modifier la valeur passée en paramètre. On déréférence x pour le passer à la fonction. x vaut maintenant 8.
- On incrémente y maintenant. y vaut 7.
- Puis on incrémente z. z étant un alias de y, c'est évidement y qui est incrémenté. y vaut maintenant 8.
- On affiche x et y, à savoir 8 et 8.
- Puis la fonction add qui accepte deux handles en paramètre, affiche donc 16.
Remarque : il est inutile de vouloir connaitre la valeur d'un handle, le garbage collector pouvant la changer à son aise.
Comme en C++, on utilise une référence sur le handle, cela donne une syntaxe particulière mais le principe est le même :
void
change(String ^%
chaine)
{
chaine =
"Nouvelle"
;
}
int
main()
{
String ^
chaine =
gcnew String("Ancienne"
);
Console::
WriteLine(chaine);
change(chaine);
Console::
WriteLine(chaine);
}
Un pointeur interne peut être assimilé au pointeur classique du C. Il "pointe" vers un objet managé, donc soumis au garbage collector. Il représente une adresse qui peut évoluer en fonction des opérations du GC (compactage, allocation, etc ...). On récupère un pointeur interne avec le mot clé interior_ptr.
array<
double
>^
tableau =
{
1
, 2
, 3
, 4
, 5
}
;
interior_ptr<
double
>
p_tableau =
&
tableau[0
];
Si le pointeur interne n'est pas initialisé, il contient par défaut nullptr.
Voici un exemple de ce qu'on peut faire avec un pointeur interne :
array<
String^>^
chaine =
{
L"Ne"
,L"touchez"
,L"pas"
,L"trop"
,L"aux"
,L"pointeurs"
}
;
for
(interior_ptr<
String^>
p_chaine =
&
chaine[0
]; p_chaine -
&
chaine[0
] <
chaine->
Length ; ++
p_chaine)
Console::
WriteLine(*
p_chaine);
Cet exemple produit en sortie :
Ne
touchez
pas
trop
aux
pointeurs
Je conseille bien évidement de se passer des pointeurs internes, sauf quand cela est indispensable.
On utilise le mot clé pin_ptr pour déclarer un pinning pointer, ce que j'ai vu traduire par un pointeur épingle.
Il s'agit en fait d'un pointeur interne (Voir Qu'est-ce qu'un pointeur interne ?) dont l'objet pointé ne sera pas déplacé en mémoire par le mécanisme du garbage collector. Concrètement, ce pointeur ne changera pas de valeur.
Ceci est indispensable lorsqu'on travaille directement sur le pointeur dans des fonctions non managées, par exemple lors de conversion de chaînes.
Voir l'exemple de conversion
On utilise gcnew pour obtenir un handle sur un objet managé par le CLR.
String ^
str =
gcnew String("Ma chaîne managée est alloué sur le heap managé"
);
Le garbage collector s'occupe de la destruction de l'objet.
Un finalizer est une fonction particulière de la classe, au même titre que le constructeur ou le destructeur, qui est appelée automatiquement par le garbage collector
quand un objet est détruit.
Il diffère du destructeur selon les éléments suivants :
- Le finalizer n'est pas appelé si le destructeur a été appelé explicitement lors de la destruction de l'objet.
- Le finalizer est appelé lorsque l'objet meurt naturellement en sortant du scope.
On définit le finalizer avec l'opérateur !.
ref class
MaClasse
{
public
:
MaClasse(int
v) : valeur(v){}
// constructeur
~
MaClasse() // destructeur
{
Console::
WriteLine("Destructeur de l'objet ({0})"
, valeur);
}
protected
:
!
MaClasse() // finalizer
{
Console::
WriteLine("Finalizer de l'objet ({0})"
, valeur);
}
private
:
int
valeur;
}
;
int
main(array<
System::
String ^>
^
args)
{
MaClasse^
obj1 =
gcnew MaClasse(1
);
MaClasse^
obj2 =
gcnew MaClasse(2
);
delete
obj1;
Console::
WriteLine("Fin"
);
return
0
;
}
Cet exemple produit le résultat suivant :
Destructeur de l'objet (1
)
Fin
Finalizer de l'objet (2
)
Notez que le finalizer de l'objet 2 est appelé après la fin du programme, lorsqu'il sort du scope.
Il est important de comprendre qu'il y a à chaque fois seulement le finalizer ou seulement le destructeur qui est appelé, suivant la méthode de destruction. Mais jamais les deux.
On peut conclure de cet exemple que si l'on veut être sur que des éléments (non managés par exemple) utilisés par un objet sont bien détruit, tout en ne tenant pas compte de la façon dont un objet est détruit (explicitement ou par le GC), alors il faut implémenter à la fois un destructeur et un finalizer.
Remarque : Le destructeur est déclaré en public, le finalizer doit l'être en protected.
Les classes (objets) .Net sont regroupées sémantiquement en catégories, sous la forme de bibliothèques. Elles forment un espace de noms : namespace.
Exemple : le namespace System::Collections contient les différents objets de collections du framework.net.
Ils sont accessibles directement en indiquant le chemin et en utilisant les :: ou bien en important le namespace directement avec using namespace.
System::Collections::
ArrayList ^
tableau =
gcnew System::Collections::
ArrayList();
ou bien :
using
namespace
System::
Collections;
ArrayList ^
tableau =
gcnew ArrayList();
Le C++/CLI nous autorise une telle chose gràce aux ... et à un array CLI :
int
somme(... array<
int
>^
args)
{
int
result =
0
;
for
each(int
arg in args)
result +=
arg;
return
result;
}
int
main(array<
System::
String ^>
^
args)
{
Console::
WriteLine(somme(1
,2
,3
,4
,5
,6
));
return
0
;
}
En plus des classiques private, public et protected, le C++/CLI ajoute trois nouvelles visibilités :
- internal : Les membres sont accessibles depuis la classe, à l'intérieur de l'assembly parent.
- public protected : Les membres sont accessibles dans des classes dérivées de notre classe à l'extérieur de l'assembly parent et dans n'importe quelle classe à l'intérieur de l'assembly parent.
- private protected : Les membres sont accessibles dans les classes dérivées de notre classe uniquement à l'intérieur de l'assembly parent.
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 d'une classe possédant une méthode avec cette même signature.
Le framework supporte deux formes de delegates :
un delegate qui accepte d'appeler uniquement une méthode :
System::
Delegate
et un delegate qui accepte d'appeler une chaine de méthodes :
System::
MulticastDelegate
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 :
delegate void
FuncMessDelegate(String ^
mess);
Maintenant les trois formes d'utilisation :
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 :
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…
Global->
Invoke("Hello"
);
statique->
Invoke("Hello"
);
instance->
Invoke("Hello"
);
return
0
;
}
Les delagates peuvent être combinés sous forme de chaine et un élément peut être supprimé (Multicast Chain).
On utilisera la méthode Combine() ou l'operateur + pour l'ajout.
Et la méthode Remove() ou l'opérateur - pour la suppression.
Exemple :
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 operateurs à la place des méthodes car ceux-ci imposent un cast vers le delegate déclaré ce qui alourdi grandement l'écriture.
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 issues de différentes classes.
Exemple :
using
namespace
System;
delegate void
AnalyseHandler(int
%
n); // la fonction de traitement recoit une reference sur un entier.
// classe declencheur de traitment
ref class
AnalyseTrigger
{
public
:
event AnalyseHandler ^
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 AnalyseHandler(this
,&
Analyse::
Treatment);
m_AnalyseTrigger->
OnWork+=
gcnew AnalyseHandler(this
,&
Analyse::
TreatmentTwo);
}
void
RemoveTreatmentTwo()
{
m_AnalyseTrigger->
OnWork-=
gcnew AnalyseHandler(this
,&
Analyse::
TreatmentTwo);
}
// traitement declenché
void
Treatment(int
%
n)
{
Console::
Write("Class Analyse Treatment =+10 n:= "
);
Console::
Write(n);
n+=
10
;
Console::
Write(" -> n="
);
Console::
WriteLine(n);
}
// traitement declenché
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 AnalyseHandler(this
,&
AnalyseTwo::
Treatment);
}
// traitement declenché
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 AnalyseHandler
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
;
}
Le C++/CLI dispose du template auto_handle pour forcer la destruction d'un objet quand il sort de portée (il fonctionne comme le template auto_ptr du C++).
Pour utiliser le template, on doit inclure :
#include
<msclr
\a
uto_handle.h>
Exemple de code :
#include
<msclr
\a
uto_handle.h>
using
namespace
System;
ref class
CManagee
{
private
:
String ^
s;
public
:
CManagee(String ^
val) {
s =
val;}
;
~
CManagee() {
Console::
WriteLine("Destructeur : "
+
s); }
protected
:
!
CManagee() {
Console::
WriteLine("Finalizer :"
+
s); }
}
;
int
main(array<
System::
String ^>
^
args)
{
{
msclr::
auto_handle<
CManagee>
cAutoHandle =
gcnew CManagee("auto_handle"
);
CManagee ^
cNormal =
gcnew CManagee("normal"
);
Console::
WriteLine("Après la construction"
);
}
Console::
WriteLine("fin"
);
return
0
;
}
On aura en sortie :
Après la construction
Destructeur : auto_handle
fin
Finalizer : normal
J'ai rajouté des { } pour forcer un bloc et montrer alors la destruction par l'appel au destructeur quand le auto_handle sort de portée. Tandis que le handle normal, lui, est détruit par le garbage collector en fin de programme.
Le template surcharge les opérateurs ->, ==, etc ... ainsi il peut être utilisé comme un handle classique.
Notez que si on souhaite pouvoir continuer à utiliser le auto_handle une fois qu'il est sorti de portée, il faut au préalable l'avoir libéré avec release :
{
msclr::
auto_handle<
CManagee>
cAutoHandle =
gcnew CManagee("auto_handle"
);
cAutoHandle.release();
Console::
WriteLine("Après la construction"
);
}
Console::
WriteLine("fin"
);
Cette fois-ci en sortie on aura :
Après la construction
fin
Finalizer : auto_handle
Il est possible d'utiliser des mots clés réservés du C++/CLI comme nom de variable. On utilise le mot clé __identifier :
String ^
__identifier(gcnew) =
gcnew String("Abc"
);
Console::
WriteLine(__identifier(gcnew));
__identifier(gcnew) =
"DEF"
;
Console::
WriteLine(__identifier(gcnew));
Je trouve personnellement que ça alourdit le code pour un intérêt limité. Cela pourrait s'avérer utile dans le cadre d'une migration. Auparavant, on utilisait une variable nommée gcnew. Une fois passé à Visual C++ 2005, pour continuer à utiliser cette variable on doit l'entourer de __identifier. Mon conseil est de changer de nom de variable.
Non, cet opérateur n'est pas disponible en C++/CLI.
Pour rappel, en C# on peut utiliser cet opérateur comme suit :
Console.
WriteLine
(
i ??
0
);
Cela permet d'utiliser l'entier 0 si jamais l'entier passé est nul.
Pour faire l'équivalent en C++/CLI, on fera
Console::
WriteLine(i.HasValue ? i : 0
);
Notez que l'on teste la propriété HasValue du type nullable i pour vérifier s'il contient une valeur.
Non, il n'y a pas d'équivalent en C++/CLI.