PARTIE 1 | PARTIE 2 | PARTIE 3 | PARTIE 4 | ||
---|---|---|---|---|---|
Préambule | Rappels C++ | Caractéristiques générales | Syntaxe de base | Syntaxe détaillée | Références |
Cette partie permet d’expliquer la syntaxe des éléments de base du code C++/CLI. La syntaxe d’autres éléments sera détaillée dans la partie suivante.
Définition et instanciation des objets
Classes non managées
Structures non managées
Objets managées de type référence
Objets managés de type valeur
Caractéristiques des structures en C++/CLI
Opérateur “address-of” et de déférencement
Constructeur
Constructeur de copie
Passage des objets en argument
Par “handle”
Objets managés passés par référence
Objets managés passés par tracking reference
Objets de type valeur
Construction d’objets mixtes
Utiliser un objet managé dans un objet non managé avec gcroot
Passage d’objets managés en argument de fonctions non managées
Définition et instanciation des objets
Comme indiqué plus haut, en C++/CLI, on peut définir et instancier:
- Des objets managés comme les classes,
struct
, interfaces et enums. - Des objets non managés comme les classes et les
struct
.
Même si certains objets semblent communs entre code managé et code non managé, ils sont en réalité très différents. Les objets managés sont gérés par le CLR (Common Language Runtime) comme en C#, ils sont alloués sur le tas managé (managed heap) ou la pile (stack). Les objets non managés sont, quant à eux, gérés par le CRT (C/C++ Runtime) et sont alloués sur le tas (heap) ou la pile (stack).
On peut résumer les différents objets utilisables en C++/CLI dans le tableau suivant:
Mot clé | Nom | Type managé ? | Accès des membres par défaut | Equivalent | Allocation | Utilisation |
---|---|---|---|---|---|---|
ref class |
Classe | Oui | Privé | Classe C# | Tas managé ou la pile |
Par “handle” ou par valeur |
ref struct |
Structure | Oui | Public | |||
value class |
Classe | Oui | Privé | Struct C# | Pile | Par valeur |
value struct |
Structure | Oui | Public | |||
class |
Classe | Non | Privé | Classe C++ | Tas ou pile | Par référence, par pointeur ou par valeur |
struct |
Structure | Non | Public | Struct C++ | ||
interface class |
Interface | Oui | Public | Interface C# | Non instanciable | Par “handle” |
interface struct |
||||||
enum class |
Enumération | Oui | Public | Enum C# | Pile | Par valeur |
enum |
Non | Enum C++ |
Classes non managées
Les classes non managées sont les classes C++ classiques, par exemple:
class NativePoint
{
public:
NativePoint(int x, int y)
{
innerX = x;
innerY = y;
}
bool IsEqual(int x, int y)
{
return this->innerX == x && this->innerY == y;
}
private:
int innerX, innerY;
};
Pour créer une classe par valeur et l’allouer sur la pile:
NativePoint point(3, 9);
point.IsEqual(8, 3);
Pour allouer une classe sur le tas:
NativePoint *point = new NativePoint(9, 3);
Point->IsEqual(2, 54);
delete point;
Quand un objet non managé est alloué sur le tas avec new
, il doit être libéré en utilisant le mot clé delete
.
Structures non managées
Les struct
C++ sont très semblables des classes C++. Par défaut c’est-à-dire sans opérateur de portée (public, protected
ou private
), les membres d’une classe sont privés. Pour une struct
, par défaut, les membres sont publics.
Une struct
C++ se déclare:
struct NativeStruct
{
// ...
};
Objets managées de type référence
Comme en C#, on distingue les objets de type référence et les objets de type valeur. Les objets de type référence sont ceux déclarés avec ref class
ou ref struct
. Ils sont semblables aux classes C# car ils sont alloués sur le tas managé et généralement ils sont instanciés par référence (toutefois on peut les allouer sur le tas). Ils sont gérés par le CLR et par suite il n’est pas nécessaire d’utiliser le mot clé delete
pour les libérer car ils sont libérés par le garbage collector. Comme en C#, ces objets dérivent implicitement de System::Object
.
Les références vers ces objets sont appelées des “handles” pour les différencier des références classiques vers des classes C++ non managées. Un “handle” est identifié par "^"
. Les objets managés alloués sur le tas managé sont instanciés avec gcnew
(utilisé new
provoque une erreur).
Par exemple, si on déclare la classe:
ref class ManagedPoint
{
public:
ManagedPoint(int x, int y)
{
innerX = x;
innerY = y;
}
bool IsEqual(int x, int y)
{
return this->innerX == x && this->innerY == y;
}
private:
int innerX, innerY;
};
On peut allouer la classe dans le tas managé:
ManagedPoint ^managedPoint = gcnew ManagedPoint(3, 6);
ManagedPoint->IsEqual(1, 5);
Ou allouer ces objets sur la pile:
ManagedPoint managedPoint(3, 6);
managedPoint.IsEqual(1, 5);
Handle
Dans l’exemple précédent, la variable managedPoint
de type ManagedPoint ^
est appelé “handle”. Il s’agit de l’équivalent des pointeurs en C++ pur toutefois ils sont assez différents car contrairement aux objets natifs, les objets managés peuvent être déplacés par le garbage collector pendant sa phase de “compactage”. Ainsi, l’adresse pointée par un “handle” peut varier au cours de sa vie.
Même en cas de déplacement des objets, le CLR garantit que les “handles” pointeront vers les bons objets en mémoire.
Précisions sur “ref struct”
Il ne faut pas confondre ce type d’objet avec les struct
en C#. La différence entre les ref class
et les ref struct
réside dans la portée des membres par défaut:
ref class
: ces objets sont semblables aux classes C#, les membres sont privés par défaut.ref struct
: les membres sont publics par défaut.
Pour éviter les confusions, on peut éviter d’utiliser ce type d’objets.
Objet de type System::String et array
Les objets de type System::String
et array
ne peuvent être alloués sur la pile, ils doivent obligatoirement être instanciés en utilisant gcnew
.
Objets managés de type valeur
Les objets de type valeur peuvent être déclarés avec value class
et value struct
. Ils sont alloués sur la pile et généralement ils sont instanciés par valeur (toutefois on peut les instancier par référence avec gcnew
). Le passage de ces objets se fait par copie comme pour les struct
en C#, ils dérivent implicitement de System::ValueType
qui dérive elle-même de System::Object
. Toutefois il n’est pas possible d’instancier directement un objet de type System::ValueType
.
Les objets de type valeur sont implicitement sealed
c’est-à-dire qu’on ne peut pas en dériver et ils possédent un constructeur par défaut. Lorsqu’ils sont passés en paramètre de fonction, ils sont copiés par valeur et une copie par bit est effectuée.
L’intérêt d’utiliser des objets de type valeur est d’allouer ces objets sur la pile plutôt que sur le tas managé, la pile étant plus rapide que le tas managé.
Par exemple, si on déclare la struct
:
value struct ManagedPointAsStruct
{
public:
ManagedPointAsStruct(int x, int y)
{
innerX = x;
innerY = y;
}
bool IsEqual(int x, int y)
{
return innerX == x && innerY == y;
}
private:
int innerX, innerY;
};
On peut l’instancier par valeur:
ManagedPointAsStruct managedPointAsStruct(3, 6);
managedPointAsStruct.IsEqual(1, 5);
Il est possible de l’instancier par référence avec gcnew
:
ManagedPointAsStruct ^managedPointAsStruct = gcnew ManagedPointAsStruct(3, 6);
managedPointAsStruct->IsEqual(1, 5);
Précisions sur l’instanciation de type valeur par référence
Même s’il est possible d’instancier un objet de type valeur par référence avec gcnew
, l’objet est toujours alloué sur la pile et une opération de “boxing” est effectuée pour le convertir en type ManagedPointAsStruct ^
. Ce “boxing” implique une opération supplémentaire qui a un coût en performance.
Précisions sur “value class”
La différence entre value struct
et value class
concerne la différence de portée des membres par défaut:
value class
: les membres sont privés par défaut.value struct
: les membres sont publics par défaut.
On peut éviter d’utiliser value class
qui prête beaucoup à confusion.
Caractéristiques des structures en C++/CLI
Les caractéristiques principales des struct en C++/CLI sont:
- Il n’y a pas d’arguments par défaut dans le constructeur (contrairement au C++), il faut implémenter explicitement un constructeur par défaut.
Par exemple, si on définit la structure:value struct Point { int innerX, innerY; Point(int x, int y) { innerX = x; innerY = y; } };
- On ne peut pas initialiser des membres dans la définition de la structure, il faut le faire dans le constructeur.
- On ne peut pas utiliser de constructeur sans argument, par défaut, les membres sont initialisés avec une valeur par défaut: 0 pour les entiers,
false
pour les booléens etc… - Les membres sont par défaut publics. Dans l’exemple précédent, il n’est pas nécessaire de préciser
public
pour accéder aux membres de la structure. - Une structure ne peut pas hériter d’un objet et on ne peut pas hériter d’une structure.
- Une structure peut satisfaire une interface.
- On ne peut pas avoir d’objets de type référence dans une structure. Les structures étant des objets de type valeur, elles sont initialisées sur la pile. Si le membre d’une structure est un objet de type référence (qui sont alloués dans le tas managé et gérés par le garbage collector), on ne peut plus placer la structure sur la pile. Il existe une exception à cette règle: les chaînes de caractères qui sont des objets de type référence définis en C++/CLI avec un “handle”:
Si on reprend la classeManagedPoint
définie plus haut:value struct Point { String ^pointName; // String est autorisé dans une struct ManagedPoint ^point; // L'objet de type référence ManagedPoint n'est pas autorisé dans une struct };
Aggregate initializer
On peut initialiser une structure avec des “aggregate initializer”. Par exemple, si on considère la structure:
value struct Point
{
int innerX, innerY;
String ^pointName;
};
On peut initialiser directement cette structure en faisant:
Point point = { 3, 8, "pointA" };
Le type n’est pas vérifié par le compilateur lorsqu’on utilise un “aggregate initializer”.
Copie des structures
Les structures sont des objets de type valeur donc quand on effectue une affectation simple d’objets de ce type ou quand on les passe en argument de fonction, il y a une duplication de l’objet.
Par exemple:
Point p1;
Point p2;
p2 = p1; // L'objet est dupliqué, p1 et p2 désignent 2 objets différents.
Si on souhaite copier la référence de l’objet, il faut utiliser l’opérateur "%"
qui désigne une “tracking reference”.
Opérateur “address-of” et de déférencement
Les opérateurs ne sont pas les mêmes entre le code managé et le code non managé:
- L’opérateur “address-of” pour des “handles” est
"%"
(au lieu de"&"
en C++ non managé). - L’opérateur permettant de déclarer un “handle” est
"^"
(au lieu de"*"
pour un pointeur C++ non managé).
Ainsi en code non managé, on peut définir un pointeur et une réference de cette façon:
NativePoint *pointer = new NativePoint(4, 7);
NativePoint &reference = *pointer;
En code managé, on peut définir un “handle” et une référence vers le tas de cette façon:
ManagedPoint ^managedPoint = gcnew ManagedPoint();
ManagedPoint %heapRef = *managedPoint;
"*"
désigne l’objet vers lequel pointe managedPoint
. managedPoint
et heapRef
désignent donc le même objet en mémoire.
Si on écrit:
ManagedPoint managedPoint2 = *managedPoint;
managedPoint2
ne désigne pas le même objet que managedPoint
. Une copie est effectuée et l’objet est dupliqué. Durant cette duplication le constructeur de copie est appelé.
Tracking reference
Dans l’exemple précédent:
ManagedPoint %heapRef = *managedPoint;
L’objet heapRef
de type ManagedPoint %
est appelée “tracking référence”. Comme pour les “handles”, le CLR garantit que si le garbage collector déplace un objet en mémoire, la “tracking reference” désignera toujours le même objet.
Pour un objet de type valeur, on peut le déclarer de cette façon:
int i = 5;
int %iTrackingRef = i;
iTrackingRef
est aussi une “tracking reference”. iTrackingRef
désigne le même objet que i
.
Pour un objet de type valeur, la notation:
int %iTrackingRef = I;
Est équivalente à:
int &iRef = i;
Une “tracking reference” peut aussi être utilisé avec un type non managé class
ou struct
. Il est équivalent à "&"
.
Par exemple si on déclare la classe:
class NativePointClass
{
public:
NativePointClass(int x) : innerX(x) { };
int innerX;
};
On peut instancier une “tracking reference” vers un objet de ce type alloué sur la pile avec:
NativePointClass pointClass(4);
NativePointClass %pointClassTrackingRef = pointClass;
De même pour une "struct":
struct NativePointStruct
{
NativePointStruct(int x)
{
innerX = x;
};
int innerX;
};
On peut instancier une “tracking reference” de la même façon:
NativePointStruct pointStruct(7);
NativePointStruct %pointStructTrackingRef = pointStruct;
En résumé
On peut résumer les différents opérateurs dans le tableau suivant:
Opération | Code non managé | Code managé |
---|---|---|
Définition de pointeur ou de référence managé | * |
^ |
Address-of | & |
% |
Accès aux membres d’un objet instancié par référence | -> |
-> |
Accès aux membres d’un objet instancié par valeur | . | . |
Instanciation | new |
gcnew |
Destruction | delete |
delete (bien que l’appel à cet opérateur ne soit pas indispensable puisque la suppression de l’objet sera effectué par le garbage collector) |
Constructeur
Les constructeurs des objets en C++/CLI ont la même syntaxe que les constructeurs en C#. Comme en C++, il existe une forme qui est plus spécifique pour initialiser des membres:
ref class ManagedPoint
{
private:
int innerX, innerY;
String ^innerName;
public:
ManagedPoint(int x, int y, String ^name) : innerX(x), innerY(y), innerName(name)
{}
};
Constructeur de copie
Les constructeurs de copie (copy constructor) ne sont pas indispensables en C++/CLI. Le compilateur ne rajoute pas implicitement un constructeur de copie comme en C++. Le cas échéant, une implémentation explicite du contructeur de copie est nécessaire en C++/CLI.
Si on déclare la classe:
ref class ManagedPoint
{
private:
int innerX, innerY;
public:
ManagedPoint(int x, int y);
};
On peut implémenter un constructeur de copie de la façon suivante:
ManagedPoint(const ManagedPoint %other)
{
innerX = other.innerX;
innerY = other.innerY;
}
Destructeur et “finalizer”
Les destructeurs se définissent de la même façon entre un objet managé et un objet non managé: on utilise la syntaxe ~ClassName()
.
Par exemple, la déclaration d’un destructeur pour une classe non managée se définit:
class NativePoint
{
public:
NativePoint(int x, int y);
~NativePoint();
};
Pour un objet managé:
ref class ManagedPoint
{
public:
ManagedPoint(int x, int y);
~ManagedPoint();
};
Dans le cas d’objets managés, l’utilisation des fonctions de destruction répond aux mêmes impératifs qu’en C#. L’utilisation d’un destructeur n’est pas obligatoire, toutefois il est nécessaire lorsqu’on souhaite maitriser la libération d’une ressource:
- Si la ressource nécessite une opération particulière de fermeture lorsqu’on ne souhaite plus l’utiliser.
- Si on veut forcer la libération d’une ressource sans attendre que le garbage collector n’effectue cette libération.
Le destructeur est appelé:
- Pour les objets alloués dans le tas managé (heap): lorsqu’on utilise l’instruction
delete
pour supprimer un objet de la mémoire. - Pour les objets alloués sur la pile (stack): lorsqu’on sort du scope de la fonction qui a alloué l’objet.
Finalizer
En C++/CLI, pour déclarer un “finalizer”, on utilise la syntaxe: !ManagedPoint()
.
Par exemple, pour la classe précédente, la déclaration sera:
ref class ManagedPoint
{
public:
ManagedPoint(int x, int y); // Constructeur
~ManagedPoint(); // Destructeur
!ManagedPoint(); // Finalizer
};
Il peut être nécessaire de déclarer un “finalizer” si la classe utilise des ressources non managées comme des pointeurs vers des classes non managées, un “handle” d’un fichier, un “handle” d’un objet graphique etc…
Lorsqu’on utilise un “finalizer”, il faut avoir en tête certaines règles:
- Si plusieurs classes implémentent un “finalizer”, et que des objets du type de ces classes doivent être libérés, il n’y a pas de garantie de l’ordre dans lequel les “finalizers” de ces objets seront exécutés. Il ne faut donc pas appelé un “finalizer” d’une classe à partir du finalizer d’une autre classe.
- Il n’y a pas de connaissance sur le moment où le “finalizer” sera exécuté.
- Si un objet implémentant un “finalizer” est toujours utilisé au moment de la fermeture d’une application dans un “background thread”, au moment de la fermeture de l’application, le “finalizer” ne sera pas exécuté.
- L’implémentation d’un “finalizer” dans une classe implique que la mémoire occupée par un objet du type de cette classe ne sera pas immédiatement libérée à la première phase de “collection” du garbage collector. Si le garbage collector constate qu’un “finalizer” est implémenté, il marquera l’objet pour exécuter son “finalizer” plus tard. C’est lors du second passage du garbage collector, qu’il executera effectivement le “finalizer” de l’objet et que la mémoire sera libérée. L’implémentation d’un “finalizer” a donc une conséquence sur les performances et la libération des objets.
- Si le destructeur d’une classe est exécuté (après un appel à
delete
par exemple), le “finalizer” ne sera pas exécuté.
Passage des objets en argument
Par “handle”
On peut passer directement les objets managés alloués dans le tas managé en utilisant le “handle”:
Par exemple, si on déclare la classe:
ref class ManagedPoint
{
public:
int innerX;
};
Si on définit la méthode:
void ChangePointXByReference(ManagedPoint ^point)
{
point->innerX = 78;
}
Le passage de l’objet par “handle” se fait directement:
ManagedPoint ^point = gcnew ManagedPoint;
ChangePointXByReference(point);
Objets managés passés par référence
Les objets managés alloués dans le tas managé en utilisant un “handle” peuvent être passés par référence en utilisant "%"
.
Pour passer un objet de type ManagedPoint ^
par référence, on déclare la méthode:
void ChangePointXByReference(ManagedPoint ^%point)
{
point->innerX = 78;
}
On peut allouer un objet de type ManagedPoint
et l’utiliser en faisant:
ManagedPoint ^point = gcnew ManagedPoint;
ChangePointXByReference(point);
Objets managés passés par tracking reference
On peut aussi utiliser une “tracking reference”.
En définissant la méthode:
void ChangePointXByTrackingRef(ManagedPoint %point)
{
point.innerX = 78;
}
L’appel se fait en utilisant l’opérateur de déférencement "*"
:
ManagedPoint ^point = gcnew ManagedPoint;
ChangePointXByTrackingRef(*point);
Si on définit la méthode:
void ChangePointX(ManagedPoint copyOfPoint)
{
copyOfPoint.innerX = 78;
}
Et qu’on fait l’appel en faisant:
ManagedPoint ^point = gcnew ManagedPoint;
ChangePointX(*point);
On ne changera la valeur de point
mais la valeur d’une copie.
Objets de type valeur
On utilise la “tracking reference” avec "%"
.
Par exemple si on déclare la struct
:
value struct PointStruct
{
PointStruct(int x)
{
innerX = x;
};
int innerX;
};
Et la méthode:
void ChangePointX(PointStruct %point)
{
point.innerX = 78;
}
On peut effectuer l’appel en faisant:
PointStruct point(3);
ChangePointX(point);
Construction d’objets mixtes
Comme indiqué plus haut, le plus grand intérêt du C++/CLI est de pouvoir utiliser des objets managés et des objets non managés. Il est possible d’utiliser des constructions mixtes dans un même objet suivant certaines règles.
Les limitations dans les constructions mixtes sont dues à 2 raisons:
- Les objets managés sont alloués dans le tas managé et sont gérés par le CLR, ils ne peuvent donc pas contenir des objets alloués dans le tas non managé. En effet les objets managés sont gérés par le garbage collector qui peut les déplacer en mémoire.
- Les objets non managés sont alloués dans le tas non managé, ils ne peuvent donc pas contenir des objets alloués dans le tas managé et gérés par le garbage collector.
Ainsi:
- Une classe managée peut contenir des membres non managés seulement sous forme de pointeur: si l’objet managé est déplacé par le garbage collector en mémoire, l’adresse de l’objet non managé reste inchangé car celui-ci se trouve dans le tas non managé (les objets de cette pile n’étant pas gérés par le garbage collector).
- Une classe non managée ne peut pas contenir directement des objets managés: les objets managés pouvant être déplacés par le garbage collector dans le tas managé, ils n’ont pas d’adresse fixe. C’est la raison pour laquelle on ne peut pas utiliser directement des pointeurs pour les objets managés. De même, les “handles” ne sont pas utilisables dans des objets natifs.
Par exemple, si on définit les objets:
ref class ManagedPoint
{
};
class UnmanagedPoint
{
};
Alors:
ref class ManagedObject
{
ManagedPoint ^managedPoint; // OK: un objet managé dans un objet managé
UnmanagedPoint *unManagedPoint; // OK: un pointeur vers un objet non managé
UnmanagedPoint unManagedPointValue; // ERROR: on ne peut pas utiliser un objet alloué sur la pile
};
class UnmanagedObject
{
ManagedPoint ^managedPoint; // ERROR: pas d'utilisation de "handles"
UnmanagedPoint *unManagedPoint; // OK: un pointeur vers un objet non managé
};
Utiliser un objet managé dans un objet non managé avec gcroot
La classe gcroot
permet d’utiliser un objet managé dans un objet non managé.
Par exemple, si on définit l’objet:
ref class ManagedPoint
{
private:
int innerX;
public:
ManagedPoint(int x): innerX(x)
{};
void SetX(int newX)
{
innerX = newX;
}
};
On peut l’utiliser dans un objet non managé avec gcroot
:
#include <vcclr.h>
#include <msclr/auto_gcroot.h>
using msclr::gcroot;
class UnmanagedObject
{
private:
gcroot<ManagedPoint^> managedPoint;
public:
UnmanagedPoint(int x)
{
managedPoint = gcnew ManagedPoint(x); // L'initialisation se fait directement
}
void ChangedInnerX(int newX)
{
managedPoint->SetX(newX); // on peut utiliser directement l'opérateur ->
}
};
gcroot
effectue des appels non managés vers du code managés qui peuvent être couteux en performance, il faut donc observer quelques précautions quand on utilise cette classe:
- Réduire le nombre de membres
gcroot
etauto_gcroot
autant que possible: il est préférable d’avoir un seul objet managé qui contient des membres qu’on voudrait accéder à partir d’une classe non managée. On peut ensuite encapsuler cet objet avec “gcroot” dans la classe non managé. Cette construction est préférable par rapport à l’utilisation de plusieurs membres encapsulés avecgcroot
. - Eviter d’effectuer trop d’appels à l’opérateur
"->"
degcroot
.
Passage d’objets managés en argument de fonctions non managées
On peut faire passer un objet managé en argument d’une fonction non managée dans certaines conditions. Par exemple, on peut convertir un “handle” d’un tableau managé de “char” en pointeur d’un tableau de char
en fixant (pinning) le tableau managé de façon à fixer son adresse en mémoire.
Par exemple:
void FunctionUsingManagedArray(array<unsigned char> ^bytes)
{
cli::pin_ptr<unsigned char> pinnedPointer = &(bytes[0]);
unsigned char *convertedBytes = static_cast<unsigned char*>(pinnedPointer);
// ATTENTION: convertedBytes n'est utilisable que dans le scope de la fonction
// ...
}
L’utilisation de cli::pin_ptr
est limitée au scope de la fonction. Si on utilise le pointeur à l’extérieur de la fonction et si le garbage collector déplace l’objet, le pointeur pourrait pointer vers une mauvaise adresse.
Calling base constructor:
http://stackoverflow.com/questions/4666261/calling-base-constructor-in-c-cli