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
Les génériques du C++/CLI (mot clé generic) ressemblent beaucoup aux templates du C++.
Ils sont quand même subtilement différents dans la mesure où les génériques CLI sont instanciés par le VES (Virtual Execution System) au moment de l'exécution plutôt qu'à la compilation pour les templates.
Une définition générique doit être :
- une classe référence (ref class).
- une classe de valeur (value class).
- une interface (interface class).
- un délégate.
- Ou enfin une fonction.
Le C++ supporte les templates, pourquoi choisir les génériques ?
Pour pouvoir choisir, il est important de connaître certaines des différences entre ceux-ci.
Les templates sont instanciés à la compilation alors que les generics sont instanciés au moment de l'exécution. Ce qui implique que l'on peut spécialiser un generic dans un autre assembly, ce que l'on ne pourra pas faire avec un template.
En effet ces derniers au moment de l'exécution ne sont plus des types paramétrables. Ainsi, deux generics spécialisés dans deux assemblys différentes avec le même type d'arguments correspondront au même type. A contrario de deux templates qui seront considérés comme deux types différents.
Cela s'explique parce que les generics de type référence génèrent une seule portion de code valable pour tous les types d'arguments. Les tempates (ainsi que les generic de type de valeur) généreront une portion de code pour chaque spécialisation.
Le compilateur est aussi capable d'optimiser les generics en fonction des types en paramètres.
En revanche les templates pourront supporter des templates de templates en paramètres, alors que les generics ne le pourront pas.
template
<
template
<
class
T>
class
X>
class
MyClass
Pour créer une fonction générique, on utilise le mot clé generic sur la première ligne pour indiquer que nous faisons une définition générique.
Le mot clé typename entre crochets <> indique que le type du paramètre est T dans la fonction générique. Ce type sera remplacé par le type utilisé au moment de l'utilisation de la fonction.
On peut utiliser plusieurs paramètres en séparant les types dans les crochets par des virgules.
Imaginons que nous voulions faire une fonction qui énumère des éléments d'un tableau CLI :
generic<
typename
T>
void
afficheTableau(array<
T>
^
tab)
{
for
each(T i in tab)
if
(i)
Console::
Write(i->
ToString() +
" "
);
Console::
WriteLine();
}
On pourra appeler cette fonction par exemple de la sorte :
array<
String^>
^
s =
{
"1"
, "2"
, "4"
, "6"
, "8"
}
;
afficheTableau(s);
array<
int
>
^
i =
{
1
, 2
, 4
, 6
, 8
}
;
afficheTableau(i);
Cela peut aussi fonctionner pour une classe perso, dans la mesure où celle-ci implémente dans notre cas la méthode ToString() :
ref class
Personne
{
private
:
String ^
nom;
String ^
prenom;
public
:
Personne(String ^
n, String ^
p)
{
nom =
n;
prenom =
p;
}
virtual
String ^
ToString() override
{
return
nom +
" "
+
prenom;
}
}
;
array<
Personne^>
^
p =
gcnew array<
Personne ^>
(3
);
p[0
] =
gcnew Personne("nico"
, "pyright(c)"
);
p[1
] =
gcnew Personne("Herb"
, "Sutter"
);
afficheTableau(p);
Remarque : Au niveau de la syntaxe il est aussi bien correct d'écrire :
array<
int
>
^
i =
{
1
, 2
, 4
, 6
, 8
}
;
afficheTableau<
int
>
(i);
que
array<
int
>
^
i =
{
1
, 2
, 4
, 6
, 8
}
;
afficheTableau(i);
Dans le second cas, le type du paramètre est déduit à partir de i.
Voici un exemple de classe générique, qui reproduit le fonctionnement (basique) d'une pile :
generic<
typename
T>
public
ref class
Pile
{
array<
T>^
elements;
int
curElement;
public
:
Pile(int
taille)
{
elements =
gcnew array<
T>
(taille);
curElement =
0
;
}
void
Push(T elt)
{
if
(curElement >
elements->
Length -
1
)
throw
gcnew Exception("Il n'y a plus de place dans la pile"
);
else
{
elements[curElement] =
elt;
curElement++
;
}
}
T Pop()
{
if
(curElement>
0
)
{
curElement--
;
return
elements[curElement];
}
throw
gcnew Exception("Il n'y a plus d'éléments dans la pile"
);
}
}
;
Pour l'utiliser, il suffit de l'instancier ainsi :
Pile<
int
>^
p =
gcnew Pile<
int
>
(10
);
try
{
p->
Push(1
);
p->
Push(2
);
p->
Push(3
);
Console::
WriteLine(p->
Pop());
Console::
WriteLine(p->
Pop());
p->
Push(4
);
Console::
WriteLine(p->
Pop());
Console::
WriteLine(p->
Pop());
}
catch
(String ^
e)
{
Console::
WriteLine(e);
}
Pour utiliser la pile avec un autre type que int, il suffirait de l'instancier ainsi :
Pile<
MonObjet^>^
p =
gcnew Pile<
MonObjet^>
(10
);
p->
Push(gcnew MonObjet);
MonObjet^
m =
p->
Pop();
On procède de la façon que pour Comment créer une fonction générique ?, en utilisant le mot clé where.
La contrainte se fait sur l'utilisation d'une interface.
Dans notre exemple, nous n'accepterons uniquement que des types en premier paramètre qui implèmentent l'interface IEnumerable (notamment le for each) et qui implémentent IComparable pour le deuxième paramètre (notamment la méthode Equals) :
generic<
typename
T1, typename
T2>
where T1:IEnumerable where T2:IComparable
int
donnePosition(T1 x, T2 y)
{
int
i=
0
;
for
each(T2 t in x)
{
if
(y->
Equals(t))
return
i;
i++
;
}
return
-
1
;
}
Cette fonction nous retourne la position d'un élément dans un tableau. On pourra l'appeler ainsi :
array<
int
>
^
a =
{
4
, 5
, 11
, 2
, 3
}
;
Console::
WriteLine(donnePosition(a,11
));
array<
String ^>
^
s =
{
"un"
, "deux"
, "trois"
, "quatre"
, "cinq"
}
;
Console::
WriteLine(donnePosition(s,"cinq"
));
ArrayList ^
g =
gcnew ArrayList();
g->
Add("un"
);
g->
Add("deux"
);
Console::
WriteLine(donnePosition(g,"un"
));
Vous pouvez constater que cette fonction marche aussi bien pour des array CLI que des ArrayList .Net qui implémentent tous les deux IEnumerable.
Remarque : On obtient une erreur de compilation si les types ne respectent pas les contraintes :
'monObjet ^' : invalid type argument for generic parameter 'T2' of generic 'donnePosition', does not meet constraint 'System::IComparable ^'