Programmation C-C++/Identification des types/Transtypages C++

Un livre de Wikilivres.
Cours de C/C++
^
C++ : Identification des types
Identification dynamique des types
Transtypages C++

Livre original de C. Casteyde

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 objet parent.

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.

Transtypage dynamique[modifier | modifier le wikicode]

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).

Exemple 10-2. Opérateur dynamic_cast[modifier | modifier le wikicode]

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 ;
}

Transtypage statique[modifier | modifier le wikicode]

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).

Transtypage de constance et de volatilité[modifier | modifier le wikicode]

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.

Réinterprétation des données[modifier | modifier le wikicode]

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;
reinterpret_cast<int &>(f)=i;

sont strictement équivalentes aux lignes suivantes :

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).