10. Identification dynamique des types▲
Le C++ est un langage fortement typé. Malgré cela, il se peut que le type exact d'un objet soit inconnu à cause de l'héritage. Par exemple, si un objet est considéré comme un objet d'une classe de base de sa véritable classe, on ne peut pas déterminer a priori quelle est sa véritable nature.
Cependant, les objets polymorphiques (qui, rappelons-le, sont des objets disposant de méthodes virtuelles) conservent des informations sur leur type dynamique, à savoir leur véritable nature. En effet, lors de l'appel des méthodes virtuelles, la méthode appelée est la méthode de la véritable classe de l'objet.
Il est possible d'utiliser cette propriété pour mettre en place un mécanisme permettant d'identifier le type dynamique des objets, mais cette manière de procéder n'est pas portable. Le C++ fournit donc un mécanisme standard permettant de manipuler les informations de type des objets polymorphiques. Ce mécanisme prend en charge l'identification dynamique des types et la vérification de la validité des transtypages dans le cadre de la dérivation.
10.1. Identification dynamique des types▲
10.1.1. L'opérateur typeid▲
Le C++ fournit l'opérateur typeid, qui permet de récupérer les informations de type des expressions. Sa syntaxe est la suivante :
typeid
(expression)
où expression est l'expression dont il faut déterminer le type.
Le résultat de l'opérateur typeid est une référence sur un objet constant de classe type_info. Cette classe sera décrite dans la Section 10.1.2.
Les informations de type récupérées sont les informations de type statique pour les types non polymorphiques. Cela signifie que l'objet renvoyé par typeid caractérisera le type de l'expression fournie en paramètre, que cette expression soit un sous-objet d'un objet plus dérivé ou non. En revanche, pour les types polymorphiques, si le type ne peut pas être déterminé statiquement (c'est-à-dire à la compilation), une détermination dynamique (c'est-à-dire à l'exécution) du type a lieu, et l'objet de classe type_info renvoyé décrit le vrai type de l'expression (même si elle représente un sous-objet d'un objet d'une classe dérivée). Cette situation peut arriver lorsqu'on manipule un objet à l'aide d'un pointeur ou d'une référence sur une classe de base de la classe de cet objet.
#include
<typeinfo>
using
namespace
std;
class
Base
{
public
:
virtual
~
Base(void
); // Il faut une fonction virtuelle
// pour avoir du polymorphisme.
}
;
Base::
~
Base(void
)
{
return
;
}
class
Derivee : public
Base
{
public
:
virtual
~
Derivee(void
);
}
;
Derivee::
~
Derivee(void
)
{
return
;
}
int
main(void
)
{
Derivee*
pd =
new
Derivee;
Base*
pb =
pd;
const
type_info &
t1=
typeid
(*
pd); // t1 qualifie le type de *pd.
const
type_info &
t2=
typeid
(*
pb); // t2 qualifie le type de *pb.
return
0
;
}
Les objets t1 et t2 sont égaux, puisqu'ils qualifient tous les deux le même type (à savoir, la classe Derivee). t2 ne contient pas les informations de type de la classe Base, parce que le vrai type de l'objet pointé par pb est la classe Derivee.
Note : Notez que la classe type_info est définie dans l'espace de nommage std::, réservé à la bibliothèque standard C++, dans l'en-tête typeinfo. Par conséquent, son nom doit être précédé du préfixe std::. Vous pouvez vous passer de ce préfixe en important les définitions de l'espace de nommage de la bibliothèque standard à l'aide d'une directive using. Vous trouverez de plus amples renseignements sur les espaces de nommage dans le Chapitre 11.
On fera bien attention à déréférencer les pointeurs, car sinon, on obtient les informations de type sur ce pointeur, pas sur l'objet pointé. Si le pointeur déréférencé est le pointeur nul, l'opérateur typeid lance une exception dont l'objet est une instance de la classe bad_typeid. Cette classe est définie comme suit dans l'en-tête typeinfo :
class
bad_typeid : public
logic
{
public
:
bad_typeid(const
char
*
what_arg) : logic(what_arg)
{
return
;
}
void
raise(void
)
{
handle_raise();
throw
*
this
;
}
}
;
10.1.2. La classe type_info▲
Les informations de type sont enregistrées dans des objets de la classe type_info prédéfinie par le langage. Cette classe est déclarée dans l'en-tête typeinfo de la manière suivante :
class
type_info
{
public
:
virtual
~
type_info();
bool
operator
==
(const
type_info &
rhs) const
;
bool
operator
!=
(const
type_info &
rhs) const
;
bool
before(const
type_info &
rhs) const
;
const
char
*
name() const
;
private
:
type_info(const
type_info &
rhs);
type_info &
operator
=
(const
type_info &
rhs);
}
;
Les objets de la classe type_info ne peuvent pas être copiés, puisque l'opérateur d'affectation et le constructeur de copie sont tous les deux déclarés private. Par conséquent, le seul moyen de générer un objet de la classe type_info est d'utiliser l'opérateur typeid.
Les opérateurs de comparaison permettent de tester l'égalité et la différence de deux objets type_info, ce qui revient exactement à comparer les types des expressions.
Les objets type_info contiennent des informations sur les types sous la forme de chaînes de caractères. Une de ces chaînes représente le type sous une forme lisible par un être humain, et une autre sous une forme plus appropriée pour le traitement des types. Le format de ces chaînes de caractères n'est pas précisé et peut varier d'une implémentation à une autre. Il est possible de récupérer le nom lisible du type à l'aide de la méthode name. La valeur renvoyée est un pointeur sur une chaîne de caractères. On ne doit pas libérer la mémoire utilisée pour stocker cette chaîne de caractères.
La méthode before permet de déterminer un ordre dans les différents types appartenant à la même hiérarchie de classes, en se basant sur les propriétés d'héritage. L'utilisation de cette méthode est toutefois difficile, puisque l'ordre entre les différentes classes n'est pas fixé et peut dépendre de l'implémentation.
10.2. Transtypages C++▲
Les règles de dérivation permettent d'assurer le fait que lorsqu'on utilise un pointeur sur une classe, l'objet pointé existe bien et est bien de la classe sur laquelle le pointeur est basé. En particulier, il est possible de convertir un pointeur sur un objet en un pointeur sur un sous-objet.
En revanche, il est interdit d'utiliser un pointeur sur une classe de base pour initialiser un pointeur sur une classe dérivée. Pourtant, cette opération peut être légale, si le programmeur sait que le pointeur pointe bien sur un objet de la classe dérivée. Le langage exige cependant un transtypage explicite. Une telle situation demande l'analyse du programme afin de savoir si elle est légale ou non.
Parfois, il est impossible de faire cette analyse. Cela signifie que le programmeur ne peut pas certifier que le pointeur dont il dispose est un pointeur sur un sous-objet. Le mécanisme d'identification dynamique des types peut être alors utilisé pour vérifier, à l'exécution, si le transtypage est légal. S'il ne l'est pas, un traitement particulier doit être effectué, mais s'il l'est, le programme peut se poursuivre normalement.
Le C++ fournit un jeu d'opérateurs de transtypage qui permettent de faire ces vérifications dynamiques, et qui donc sont nettement plus sûrs que le transtypage tout puissant du C que l'on a utilisé jusqu'ici. Ces opérateurs sont capables de faire un transtypage dynamique, un transtypage statique, un transtypage de constance et un transtypage de réinterprétation des données. Nous allons voir les différents opérateurs permettant de faire ces transtypages, ainsi que leur signification.
10.2.1. Transtypage dynamique▲
Le transtypage dynamique permet de convertir une expression en un pointeur ou une référence d'une classe, ou un pointeur sur void. Il est réalisé à l'aide de l'opérateur dynamic_cast. Cet opérateur impose des restrictions lors des transtypages afin de garantir une plus grande fiabilité :
- il effectue une vérification de la validité du transtypage ;
- il n'est pas possible d'éliminer les qualifications de constance (pour cela, il faut utiliser l'opérateur const_cast, que l'on verra plus loin).
En revanche, l'opérateur dynamic_cast permet parfaitement d'accroître la constance d'un type complexe, comme le font les conversions implicites du langage vues dans la Section 3.2 et dans la Section 4.7.
Il ne peut pas travailler sur les types de base du langage, sauf void *.
La syntaxe de l'opérateur dynamic_cast est donnée ci-dessous :
dynamic_cast
<
type>
(expression)
où type désigne le type cible du transtypage, et expression l'expression à transtyper.
Le transtypage d'un pointeur ou d'une référence d'une classe dérivée en classe de base se fait donc directement, sans vérification dynamique, puisque cette opération est toujours valide. Les lignes suivantes :
// La classe B hérite de la classe A :
B *
pb;
A *
pA=
dynamic_cast
<
A *>
(pB);
sont donc strictement équivalentes à celles-ci :
// La classe B hérite de la classe A :
B *
pb;
A *
pA=
pB;
Tout autre transtypage doit se faire à partir d'un type polymorphique, afin que le compilateur puisse utiliser l'identification dynamique des types lors du transtypage. Le transtypage d'un pointeur d'un objet vers un pointeur de type void renvoie l'adresse du début de l'objet le plus dérivé, c'est-à-dire l'adresse de l'objet complet. Le transtypage d'un pointeur ou d'une référence sur un sous-objet d'un objet vers un pointeur ou une référence de l'objet complet est effectué après vérification du type dynamique. Si l'objet pointé ou référencé est bien du type indiqué pour le transtypage, l'opération se déroule correctement. En revanche, s'il n'est pas du bon type, dynamic_cast n'effectue pas le transtypage. Si le type cible est un pointeur, le pointeur nul est renvoyé. Si en revanche l'expression caractérise un objet ou une référence d'objet, une exception de type bad_cast est lancée.
La classe bad_cast est définie comme suit dans l'en-tête typeinfo :
class
bad_cast : public
exception
{
public
:
bad_cast(void
) throw
();
bad_cast(const
bad_cast&
) throw
();
bad_cast &
operator
=
(const
bad_cast&
) throw
();
virtual
~
bad_cast(void
) throw
();
virtual
const
char
*
what(void
) const
throw
();
}
;
Lors d'un transtypage, aucune ambiguïté ne doit avoir lieu pendant la recherche dynamique du type. De telles ambiguïtés peuvent apparaître dans les cas d'héritage multiple, où plusieurs objets de même type peuvent coexister dans le même objet. Cette restriction mise à part, l'opérateur dynamic_cast est capable de parcourir une hiérarchie de classe aussi bien verticalement (convertir un pointeur de sous-objet vers un pointeur d'objet complet) que transversalement (convertir un pointeur d'objet vers un pointeur d'un autre objet frère dans la hiérarchie de classes).
L'opérateur dynamic_cast peut être utilisé dans le but de convertir un pointeur sur une classe de base virtuelle vers une des ses classes filles, ce que ne pouvaient pas faire les transtypages classiques du C. En revanche, il ne peut pas être utilisé afin d'accéder à des classes de base qui ne sont pas visibles (en particulier, les classes de base héritées en private).
struct
A
{
virtual
void
f(void
)
{
return
;
}
}
;
struct
B : virtual
public
A
{
}
;
struct
C : virtual
public
A, public
B
{
}
;
struct
D
{
virtual
void
g(void
)
{
return
;
}
}
;
struct
E : public
B, public
C, public
D
{
}
;
int
main(void
)
{
E e; // e contient deux sous-objets de classe B
// (mais un seul sous-objet de classe A).
// Les sous-objets de classe C et D sont
// frères.
A *
pA=&
e; // Dérivation légale : le sous-objet
// de classe A est unique.
// C *pC=(C *) pA;// Illégal : A est une classe de base
// virtuelle (erreur de compilation).
C *
pC=
dynamic_cast
<
C *>
(pA); // Légal. Transtypage
// dynamique vertical.
D *
pD=
dynamic_cast
<
D *>
(pC); // Légal. Transtypage
// dynamique horizontal.
B *
pB=
dynamic_cast
<
B *>
(pA); // Légal, mais échouera
// à l'exécution (ambiguïté).
return
0
;
}
10.2.2. Transtypage statique▲
Contrairement au transtypage dynamique, le transtypage statique n'effectue aucune vérification des types dynamiques lors du transtypage. Il est donc nettement plus dangereux que le transtypage dynamique. Cependant, contrairement au transtypage C classique, il ne permet toujours pas de supprimer les qualifications de constance.
Le transtypage statique s'effectue à l'aide de l'opérateur static_cast, dont la syntaxe est exactement la même que celle de l'opérateur dynamic_cast :
static_cast
<
type>
(expression)
où type et expression ont la même signification que pour l'opérateur dynamic_cast.
Essentiellement, l'opérateur static_cast n'effectue l'opération de transtypage que si l'expression suivante est valide :
type temporaire(expression);
Cette expression construit un objet temporaire quelconque de type type et l'initialise avec la valeur de expression. Contrairement à l'opérateur dynamic_cast, l'opérateur static_cast permet donc d'effectuer les conversions entre les types autres que les classes définies par l'utilisateur. Aucune vérification de la validité de la conversion n'a lieu cependant (comme pour le transtypage C classique).
Si une telle expression n'est pas valide, le transtypage peut malgré tout avoir lieu s'il s'agit d'un transtypage entre classes dérivées et classes de base. L'opérateur static_cast permet d'effectuer les transtypages de ce type dans les deux sens (classe de base vers classe dérivée et classe dérivée vers classe de base). Le transtypage d'une classe de base vers une classe dérivée ne doit être fait que lorsqu'on est sûr qu'il n'y a pas de danger, puisqu'aucune vérification dynamique n'a lieu avec static_cast.
Enfin, toutes les expressions peuvent être converties en void avec des qualifications de constance et de volatilité. Cette opération a simplement pour but de supprimer la valeur de l'expression (puisque void représente le type vide).
10.2.3. Transtypage de constance et de volatilité▲
La suppression des attributs de constance et de volatilité peut être réalisée grâce à l'opérateur const_cast. Cet opérateur suit exactement la même syntaxe que les opérateurs dynamic_cast et static_cast :
const_cast
<
type>
(expression)
L'opérateur const_cast peut travailler essentiellement avec des références et des pointeurs. Il permet de réaliser les transtypages dont le type destination est moins contraint que le type source vis-à-vis des mots clés const et volatile.
En revanche, l'opérateur const_cast ne permet pas d'effectuer d'autres conversions que les autres opérateurs de transtypage (ou simplement les transtypages C classiques) peuvent réaliser. Par exemple, il est impossible de l'utiliser pour convertir un flottant en entier. Lorsqu'il travaille avec des références, l'opérateur const_cast vérifie que le transtypage est légal en convertissant les références en pointeurs et en regardant si le transtypage n'implique que les attributs const et volatile. const_cast ne permet pas de convertir les pointeurs de fonctions.
10.2.4. Réinterprétation des données▲
L'opérateur de transtypage le plus dangereux est reinterpret_cast. Sa syntaxe est la même que celle des autres opérateurs de transtypage dynamic_cast, static_cast et const_cast :
reinterpret_cast
<
type>
(expression)
Cet opérateur permet de réinterpréter les données d'un type en un autre type. Aucune vérification de la validité de cette opération n'est faite. Ainsi, les lignes suivantes :
double
f=
2.3
;
int
i=
1
;
const_cast
<
int
&>
(f)=
i;
sont strictement équivalentes aux lignes suivantes :
double
f=
2.3
;
int
i=
1
;
*
((int
*
) &
f)=
i;
- il ne doit pas permettre la suppression des attributs de constance et de volatilité ;
- il doit être symétrique (c'est-à-dire que la réinterprétation d'un type T1 en tant que type T2, puis la réinterprétation du résultat en type T1 doit redonner l'objet initial).