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

Cours de C/C++


précédentsommairesuivant

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 :

 
Sélectionnez
typeid(expression)

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.

Exemple 10-1. Opérateur typeid
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
// La classe B hérite de la classe A :
 
B *pb;
A *pA=dynamic_cast<A *>(pB);

sont donc strictement équivalentes à celles-ci :

 
Sélectionnez
// 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 :

 
Sélectionnez
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).

Exemple 10-2. Opérateur dynamic_cast
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
double f=2.3;
int i=1;
const_cast<int &>(f)=i;

sont strictement équivalentes aux lignes suivantes :

 
Sélectionnez
double f=2.3;
int i=1;
*((int *) &f)=i;
L'opérateur reinterpret_cast doit cependant respecter les règles suivantes :
  • 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).

précédentsommairesuivant

Copyright © 2003 Christian Casteyde. Permission vous est donnée de copier, distribuer et modifier ce document selon les termes de la licence GNU pour les documentations libres, version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation. Une copie de cette licence est incluse dans l'annexe intitulée "GNU Free Documentation License". Vous trouverez également une traduction non officielle de cette licence dans l'annexe intitulée "Licence de documentation libre GNU".