Transtypage en C++

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 20/12/1999 - Dernière mise à jour : 07/03/2001

Sommaire

Transtypage
Opérateurs de transtypage
Operateur dynamic_cast
Exception bad_cast
Opérateur static_cast
Opérateur const_cast
Opérateur reinterpret_cast
Bibliographie

Transtypage

    En C++, si une classe est dérivée d’une classe de base contenant des fonctions virtuelles, les fonctions virtuelles implémentées dans la classe dérivée peuvent être appelées par l’intermédiaire d’un pointeur sur la classe de base. Cette caractéristique est appelée le polymorphisme. Une classe contenant des fonctions virtuelles est appelée classe polymorphique.
Soit un pointeur sur  une classe de base. Si l'objet courant pointé par le pointeur est de la classe dérivée, il est dit "objet complet". En revanche, le pointeur est dit pointer sur un sous-objet de l'objet complet (figure 1 et figure 2) :


figure 1 : hiérarchie de classe


figure 2 : classe C avec un sous-objet B et un sous-objet A

    L'information "run-time" de type  permet de savoir si un pointeur pointe actuellement sur un objet complet et peut être transtypé sans risque vers un autre objet dans sa hiérarchie. L'opérateur dynamic_cast peut être utilisé pour effectuer de tels transtypages. En outre il effectue les tests "run-time" nécessaires pour rendre cette opération sûre.

Sommaire

Opérateurs de transtypage

    Il y a différents opérateurs de transtypage spécifiques au langage C++, dont le but est de supprimer certaines ambiguïtés et dangers inhérents au langage C :
 
dynamic_cast  utilisé pour la conversion de types polymorphiques
static_cast  utilisé pour la conversion de types non polymorphiques
const_cast  utilisé pour supprimer les attributs const, volatile, et __unaligned
reinterpret__cast  utilisé pour la simple réinterpretation des bits

    N'utiliser const_cast et reinterpret_cast qu'en dernier ressort puisqu'ils présentent les mêmes dangers que les transtypages "old style". Ils sont cependant nécessaires pour remplacer les transtypages dans l'ancien style C.

Sommaire

Opérateur dynamic_cast

    L'expression dynamic_cast<type-id>(expression) convertit l'expression opérande en un objet de type type-id. Le type-id doit être un pointeur ou une référence à un type de classe préalablement défini ou un "pointeur sur void". Le type de expression doit être un pointeur si type-id est un pointeur, ou une l-value si type-id est une référence.

Syntaxe
 
dynamic_cast <type-id> (expression)

    Si type-id est un pointeur sur une classe de base accessible directement ou indirectement sans ambiguïté, le résultat est un pointeur sur l'unique sous-objet de type type-id. Par exemple :
 
class B { ... };
class C : public B { ... };
class D : public C { ... };

void f (D* pd)
{
    C* pc = dynamic_cast <C*>(pd);  // ok: C est une classe de base directe
                                    // pc pointe sur le sous-objet C de pd
    B* pb = dynamic_cast <B*>(pd);  // ok: B est une classe de base indirecte
                                    // pb pointe sur le sous-objet B de pd
    ...
}

    Ce type de conversion est appelé un “upcast” parce qu'il déplace le pointeur vers le haut de la hiérarchie des classes, à partir d'une classe dérivée vers une classe dont elle dérive. Un upcast est une conversion implicite.

    Si type-id est void*, un test run-time est effectué pour déterminer le type actuel de l'expression. Le résultat est un pointeur sur l'objet complet pointé par l'expression. Par exemple :
 
class A { ... };
class B { ... };
void f()
{
    A* pa = new A;
    B* pb = new B;
    void* pv = dynamic_cast <void*> (pa);
                        // pv pointe maintenant sur un objet de type A
    ...
    pv = dynamic_cast <void*> (pb);
                        // pv pointe maintenant sur un objet de type B
}

    Si type-id n'est pas void*, un test run-time est effectué pour voir si l'objet pointé par l'expression peut être converti dans le type pointé par type-id.

    Si le type de expression est une classe de base du type de type-id, un test run-time est effectué pour voir si expression pointe actuellement sur un objet complet du type de type-id. Si c'est vrai, le résultat est un pointeur vers un objet complet du type de type-id. Par exemple :
 
class B { ... };
class D : public B { ... };

void f()
{
    B* pb  = new D;    // pas clair mais ok
    B* pb2 = new B;

    D* pd  = dynamic_cast <D*>(pb);  // ok: pb pointe actuellement sur un D
    ...
    D* pd2 = dynamic_cast <D*>(pb2); // erreur : pb2 pointe sur un B, pas sur un
                                     //          D  =>  pd2 == NULL
    ...
}

    Ce type de conversion est appelé un “downcast” parce qu'il déplace le pointeur vers le bas de la hiérarchie des classes, à partir d'une classe vers une classe qui en est dérivée. L'héritage multiple peut introduire des ambiguïtés. Considérons la hiérarchie des classes montrée dans la figure 3 :


figure 3 : hiérarchie de classes illustrant l'héritage multiple

    Un pointeur sur un objet de la classe D peut être transtypé vers C ou B. Cependant, s'il est transtypé pour pointer vers un objet de type A, vers quelle instance? Pour éviter une erreur de transtypage due à cette ambiguïté, il est possible d'effectuer deux transtypages non ambigus. Par exemple :
 
void f()
{
    D* pd  = new D;
    A* pa  = dynamic_cast<A*>(pd);    // erreur: ambigu
    B* pb  = dynamic_cast<B*>(pd);    // premier transtypage en B
    A* pa2 = dynamic_cast<A*>(pb);    // ok: non ambigu
}

    D'autres ambiguïtés peuvent apparaître si on utilise des classes de base virtuelles. Considérons la hiérarchie de classes illustrée dans la figure 4 :


figure 4 : hiérarchie de classes montrant des classes de base virtuelles

    Dans cette hiérarchie, A est une classe de base virtuelle. Soit une instance de E et un pointeur vers la classe de base virtuelle A. Un dynamic_cast sur un pointeur vers B échouera à cause d'une ambiguïté. Il faut tout d'abord transtyper sur l'objet complet E, puis remonter dans la hiérarchie de manière non ambiguë jusqu'à atteindre l'objet B correct.

    Considérons maintenant la hiérarchie montrée dans la figure 5 :


figure 5 : hiérarchie montrant des classes de base dupliquées

    Soit un objet de type E et un pointeur sur le sous-objet D, trois conversions sont nécessaires pour aller du sous-objet D au sous-objet A le plus à gauche : il faut tout d'abord effectuer une conversion dynamic_cast du pointeur D vers un pointeur E, puis une conversion (soit dynamic_cast soit implicite) de E vers B, et enfin une conversion implicite de B vers A. Par exemple:
 
void f(D* pd)
{
    E* pe = dynamic_cast <E*>(pd);
    B* pb = pe;                 // upcast, conversion implicite
    A* pa = pb;                 // upcast, conversion implicite
}

    L'opérateur dynamic_cast peut aussi être utilisé pour effectuer un transtypage croisé ("cross cast"). En utilisant la même hiérarchie (figure 5), il est possible de transtyper un pointeur, par exemple du sous-objet B vers le sous-objet D, sous réserve que l'objet complet soit de type E. Il est possible de n'effectuer  la conversion d'un pointeur sur D en un pointeur sur le sous-objet A le plus à gauche qu'en deux opérations : un transtypage croisé de D vers B, puis une conversion implicite de B vers A. Par exemple :
 
void f(D* pd)
{
    B* pb = dynamic_cast <B*>(pd); // transtypage croisé
    A* pa = pb;                    // upcast, conversion implicite
}

    Une valeur de pointeur nulle est convertie en une valeur de pointeur nulle du type de destination par dynamic_cast.
Si expression ne peut être proprement convertie dans le type type-id lors de l'utilisation de dynamic_cast<type-id>(expression), le test run-time conduit à un échec. Par exemple :
 
class A { ... };
class B { ... };
void f()
{
    A* pa = new A;
    B* pb = dynamic_cast <B*>(pa);  // échec : B ne dérive pas de A
 ...
}

    La valeur retournée par un transtypage dynamique de pointeur est un pointeur nul si l'expression est un pointeur. L'échec d'un transtypage en une référence lève l'exception bad_cast.

Sommaire

Exception bad_cast

    Le dynamic_cast lève une exception si le transtypage en un type référence a échoué. L'interface de bad_cast est :
 
class bad_cast : public logic {
  public:
    bad_cast (const __exString& what_arg) : logic (what_arg) {}
    void raise() { handle_raise(); throw *this; }
    // virtual __exString what() const;            // héritée
};

Sommaire

Opérateur static_cast

    L'expression static_cast <type-id> (expression) convertit expression dans le type de type-id sur la seule base des types présents dans expression. Aucun test run-time n'est effectué pour s'assurer de la validité de la conversion.

Syntaxe
 
static_cast <type-id> (expression)

    L'opérateur static_cast peut être utilisé pour des opérations telles que la conversion d'un pointeur d'une classe de base en un pointeur vers une classe dérivée. De telles conversions ne sont pas toujours sûres. Par exemple :
 
class B { ... };
class D : public B { ... };
void f(B* pb, D* pd)
{
    D* pd2 = static_cast <D*>(pb);  // pas sûre, pb peut ne pointer que sur un B
    B* pb2 = static_cast <B*>(pd);  // conversion sûre
    ...
}

    Contrairement à dynamic_cast, aucun test run-time n'est effectué lors de la conversion static_cast  de pb. L'objet pointé par pb peut ne pas être de type D, auquel cas l'utilisation de *pd2 pourrait être désastreuse. Par exemple l'appel d'une fonction membre de la classe D, mais pas de la classe B, pourrait provoquer une violation d'accès.
 
class B { ... };
class D : public B { ... };

void f(B* pb)
{
    D* pd1 = dynamic_cast <D*>(pb);
    D* pd2 = static_cast  <D*>(pb);
}

    Si pb pointe réellement sur un objet de type D, alors pd1 et pd2 auront la même valeur, de même que si pb == 0.

    Si pb pointe sur un objet de type B et pas sur la classe complète D, alors dynamic_cast en sait suffisamment pour retourner 0. Cependant, static_cast fait confiance au programmeur, suppose que pb pointe réellement sur un objet de type D, et retourne simplement un pointeur sur ce supposé objet D. En conséquence, static_cast peut faire l'inverse des conversions implicites, et les résultats peuvent être indéfinis. C'est de la responsabilité du programmeur de s'assurer que la conversion static_cast est applicable.

    Ce comportement s'applique aussi aux types autres que les types de classes. Par exemple, static_cast peut être utilisé pour convertir un int en un char. Cependant, le char peut ne pas avoir suffisamment de bits pour contenir la valeur int. C'est de nouveau au programmeur de s'assurer de la validité de l'opération.

    L'opérateur static_cast peut aussi être utilisé pour effectuer toute conversion implicite, standard ou définie par l'utilisateur. Par exemple :
 
typedef unsigned char BYTE
void f()
{
    char ch;
    int   i = 65;
    float f = 2.5;
    double dbl;

    ch  = static_cast <char>(i);    // int en char
    dbl = static_cast <double>(f);  // float en double
    ...
    i = static_cast <BYTE>(ch);
    ...
}

    L'opérateur static_cast peut explicitement convertir une valeur entière en un type énuméré. Si la valeur entière ne tombe pas dans le domaine des valeurs énumérées, la valeur résultante d'énumération est indéfinie.

    L'opérateur static_cast convertit une valeur de pointeur nulle en une valeur de pointeur nulle du type de destination.
Toute expression peut être explicitement convertie en un type void par l'opérateur static_cast. Le type de destination void peut inclure de façon optionnelle les attributs const, volatile, et __unaligned.

Sommaire

Operateur const_cast

    L'opérateur const_cast peut être utilisé pour supprimer les attributs const, volatile, et __unaligned d'une classe.

Syntaxe
 
const_cast <type-id> (expression)

    Un pointeur sur n'importe quel objet ou sur une donnée membre peut être explicitement converti en un type identique à part les qualifieurs const, volatile, et __unaligned. Pour les pointeurs et les références, le résultat référera l'objet original. Pour les pointeurs sur données membres, le résultat référera la même donnée membre que le pointeur original. Selon le type d'objet référencé, une opération d'écriture par le biais du pointeur résultant, d'une référence ou d'un pointeur sur donnée membre peut produire un comportement indéfini.

    L'opérateur const_cast convertit un pointeur de valeur nulle en un pointeur de valeur nulle du type destination.

Sommaire

Operateur reinterpret_cast

    L'opérateur reinterpret_cast permet à tout pointeur d'être converti en n'importe quel autre type de pointeur. Il permet aussi à tout type d'entier d'être converti en tout type de pointeur et vice versa. Une mauvaise utilisation de l'opérateur reinterpret_cast peut facilement avoir des conséquences catastrophiques et il doit être réservé à un usage de bas niveau.

Syntaxe
 
 reinterpret_cast <type-id> (expression)

    L'opérateur reinterpret_cast peut être utilisé pour des conversions telles que char* en int*, ou One_class* en Unrelated_class*, qui sont par nature peu sûrs.

    Le résultat de reinterpret_cast ne peut être utilisé de façon sûre pour autre chose que le retour au type d'origine. Toute autre utilisation est, au mieux, non portable.

    L'opérateur reinterpret_cast convertit un pointeur de valeur nulle en un pointeur de valeur nulle du type destination.

Sommaire

Bibliographie

Microsoft C++, version 4.0, Aide en ligne

Sommaire

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique