XXII. Informations sur les types (dynamiques et statiques) et conversions▲
XXII-A. Types polymorphes▲
Le C++ définit un type polymorphe comme un type contenant au moins une fonction virtuelle ou dont une des classes de bases contient une fonction virtuelle.
struct base
{
};
struct derived_1 : private base
{
virtual void do_it();
};
struct derived_2 : private base
{
void do_it();
};
struct base_2
{
virtual ~base_2();
};
struct derived_3 : public base_2
{
};Dans l'exemple précédent, cela donne :
- base : non polymorphe
- derived_1 : polymorphe
- derived_2 : non polymorphe
- base_2 : polymorphe
- derived_3 : polymorphe
L'héritage ne suffit pas pour qu'un type soit polymorphe. Une hiérarchie de classe ne contenant aucune fonction virtuelle n'est pas considérée comme polymorphe. L'explication est facile à comprendre au regard de ce que nous avons vu jusqu'à présent : en l'absence de fonction virtuelle, cela signifie que quelque soit l'expression utilisée, la résolution des appels se fait toujours avec le type statique de l'objet (variable, référence ou pointeur). Il n'est donc pas possible d'invoquer le comportement d'une classe dérivée à partir de la classe mère. En ce sens, il n'y a pas vraiment de polymorphisme car l'objet est toujours vu comme le type statique qui le manipule.
XXII-B. Comment connaître le type dynamique d'une variable ? ▲
XXII-B-1. L'opérateur typeid▲
Pour être utilisé, l'opérateur typeid nécessite le fichier d'en-tête <typeinfo>. L'opérateur typeid retourne un objet de type statique const std::type_info et dont le type dynamique est défini par l'implémentation du compilateur (mais qui a de toute façon const std::type_info comme une de ses classes de base). L'opérateur s'applique aux variables, aux types ainsi qu'aux expressions. Elle permet d'avoir des informations de typage. Les qualificateurs const sont ignorés. Les références sont identiques au type qu'elles désignent et les pointeurs déréférencés aussi. Les valeurs retournées par typeid peuvent être comparées (avec l'opérateur == ou l'opérateur !=), cette comparaison permet de savoir si deux variables/types/expressions sont du même type :
#include<typeinfo>
#include <iostream>
struct my_type
{
};
int main()
{
my_type t1;
my_type t2;
my_type &rt = t1;
my_type *pt = &t1;
my_type const ct=my_type();
std::cout<<std::boolalpha;
std::cout<<"typeid(t1)==typeid(t2) : "<<(typeid(t1)==typeid(t2))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type) : "<<(typeid(t1)==typeid(my_type))<<"\n";
std::cout<<"typeid(t1)==typeid(rt) : "<<(typeid(t1)==typeid(rt))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type&) : "<<(typeid(t1)==typeid(my_type&))<<"\n";
std::cout<<"typeid(t1)==typeid(pt) : "<<(typeid(t1)==typeid(pt))<<"\n";
std::cout<<"typeid(t1)==typeid(ct) : "<<(typeid(t1)==typeid(ct))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type const) : "<<(typeid(t1)==typeid(my_type const))<<"\n";
std::cout<<"typeid(t1)==typeid(*pt) : "<<(typeid(t1)==typeid(*pt))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type*) : "<<(typeid(t1)==typeid(my_type*))<<"\n";
std::cout<<"typeid(t1)==typeid(int) : "<<(typeid(t1)==typeid(int))<<"\n";
return 0;
}Appliquer à un type polymorphe, typeid retourne un const std::type_info relatif au type dynamique de l'objet :
#include<typeinfo>
#include <iostream>
struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};
int main()
{
polymorphic_base b;
polymorphic_derived d;
std::cout<<std::boolalpha;
std::cout<<"typeid(b)==typeid(d) : "<<(typeid(b)==typeid(d))<<"\n";
polymorphic_base &rb=b;
std::cout<<"typeid(b)==typeid(rb) : "<<(typeid(b)==typeid(rb))<<"\n";
polymorphic_base &rd=d;
std::cout<<"typeid(b)==typeid(rd) : "<<(typeid(b)==typeid(rd))<<"\n";
std::cout<<"typeid(d)==typeid(rd) : "<<(typeid(d)==typeid(rd))<<"\n";
return 0;
}Ce code produit comme sortie :
typeid(b)==typeid(d) : false
typeid(b)==typeid(rb) : true
typeid(b)==typeid(rd) : false
typeid(d)==typeid(rd) : trueLe type dynamique sur la référence rd est bien retrouvé et correspond au type polymorphic_derived de d.
Appliquer à un type non polymorphe, l'information de type retournée est relative au type statique :
#include<typeinfo>
#include <iostream>
struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};
int main()
{
non_polymorphic_base b;
non_polymorphic_derived d;
std::cout<<std::boolalpha;
std::cout<<"typeid(b)==typeid(d) : "<<(typeid(b)==typeid(d))<<"\n";
non_polymorphic_base &rb=b;
std::cout<<"typeid(b)==typeid(rb) : "<<(typeid(b)==typeid(rb))<<"\n";
non_polymorphic_base &rd=d;
std::cout<<"typeid(b)==typeid(rd) : "<<(typeid(b)==typeid(rd))<<"\n";
std::cout<<"typeid(d)==typeid(rd) : "<<(typeid(d)==typeid(rd))<<"\n";
return 0;
}Ce code donne comme résultat :
typeid(b)==typeid(d) : false
typeid(b)==typeid(rb) : true
typeid(b)==typeid(rd) : true
typeid(d)==typeid(rd) : falseLes deux dernières lignes mettent en évidence que typeid(rd) prend le type statique de la référence non_polymorphic_base et non son type dynamique car non_polymorphic_base n'est pas une classe polymorphe.
XXII-B-2. Evaluation de l'expression▲
Appliqué à un type non polymorphe et utilisant par conséquent le type statique de l'expression, l'opérateur typeid n'évalue pas l'expression à laquelle il est appliqué. En particulier, si un pointeur est nul, et qu'il est déréférencé dans l'expression, cela ne provoque pas d'erreur :
#include<typeinfo>
#include <iostream>
struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};
int main()
{
non_polymorphic_base *pb=0;
std::cout<<std::boolalpha;
std::cout<<"typeid(*pb)==typeid(non_polymorphic_base) : "
<<(typeid(*pb)==typeid(non_polymorphic_base))<<"\n";
return 0;
}Ce code est correct et ne pose pas de problème à l'exécution.
En revanche, sur un type polymorphe, l'expression est évaluée car c'est le type dynamique obtenu à l'exécution qui est nécessaire à l'opérateur typeid. En particulier, si un pointeur est nul et que celui-ci est déréférencé dans l'expression, une exception de type std::bad_typeid est générée :
#include<typeinfo>
#include <iostream>
struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};
int main()
{
polymorphic_base *pb=0;
std::cout<<std::boolalpha;
try{
std::cout<<"typeid(*pb)==typeid(polymorphic_base) : "
<<(typeid(*pb)==typeid(polymorphic_base))<<"\n";
}
catch(std::bad_typeid const &e_)
{
std::cout<<"Erreur : "<<e_.what()<<"\n";
}
return 0;
}Lorsqu'une expression est évaluée, cela signifie aussi que les fonctions qu'elle contient sont effectivement déroulées. Alors que lorsque l'expression n'est pas évaluée, les fonctions ne sont pas exécutées :
#include<typeinfo>
#include <iostream>
struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};
polymorphic_base&identity_1(polymorphic_base&r_)
{
std::cout<<"Fonction identity_1";
return r_;
}
struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};
non_polymorphic_base&identity_2(non_polymorphic_base&r_)
{
std::cout<<"Fonction identity_2";
return r_;
}
int main()
{
polymorphic_base b;
std::cout<<"typeid(identity_1(b)) : ";
typeid(identity_1(b));
std::cout<<" --\n";
non_polymorphic_base b2;
std::cout<<"typeid(identity_2(b2)) : ";
typeid(identity_2(b2));
std::cout<<" --\n";
return 0;
}Ce code donne le résultat :
typeid(identity_1(b)) : Fonction identity_1 --
typeid(identity_2(b2)) : --Notons que GCC donne un avertissement lorsqu'un l'expression contient un appel non exécuté :
warning : statement has no effectXXII-B-3. La classe type_info▲
Le synopsis de cette classe est assez simple :
namespace std {
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);
};
}type_info est dans l'espace de nom
std. Le constructeur par copie et
l'opérateur d'affectation étant privés, les objets retournés
par typeid ne sont pas copiables. Il n'est
donc pas possible de garder par valeur le retour
de typeid dans un autre objet ou dans
un conteneur. En revanche, on peut garder une référence
ou un pointeur sur l'objet type_info
retourné, la norme garantit que la durée de vie de cet
objet doit dépasser celle du programme (pas du processus !).
type_info::name() retourne une chaîne
de caractère terminée par un '\0' contenant le nom du
type. Ce nom est spécifique au compilateur :
#include<typeinfo>
#include <iostream>
class A{};
int main()
{
std::cout<<typeid(int).name()<<"\n";
std::cout<<typeid(A).name()<<"\n";
return 0;
}Produit comme sortie avec GCC :
i
1AEt avec Visual C++ :
int
class Atype_info::before() permet de définir un ordre sur les objets retournés. Cet ordre est spécifique au compilateur.
XXII-B-4. Pourquoi récupérer le type dynamique ?▲
Il est fort à parier que si vous ressentez le besoin de récupérer le type dynamique d'une variable ou d'une expression, c'est que vous avez probablement une erreur de conception de votre logiciel. En revanche, typeid peut être utile en mode debug et pour le log afin d'avoir des traces sur les objets effectivement manipulés. En dehors de ces cas de debug et de tests-U, rares sont les bonnes raisons d'avoir à le faire.
XXII-C. Comment connaître le type statique d'une variable ?▲
Cette question peut sembler incongrue puisque nous avons toujours dit et vu que le type statique d'une variable est celui que nous avons sous le nez en lisant le code. Ecartons le typedef qui ne définit qu'un synonyme d'un type et dont on peut retrouver facilement le type effectivement désigné. Avec les fonctions et les classes génériques (template) le type est un paramètre et ne peut donc être connu immédiatement lors de son utilisation :
template<class T> void function(T const&t_)
{
// quelles informations sur le type T ?
}Si l'utilisation de template suppose que justement le type n'a pas d'importance, en pratique, on souhaite parfois avoir des informations sur le type effectivement utilisé pour instancier la fonction ou la classe générique. Pour cela, on s'appuie sur des classes traits. Les classes traits utilisent le mécanisme des classes génériques et des spécialisations pour permettre d'obtenir des informations sur les types :
#include <iostream>
template<class T> struct is_int
{
static const bool value = false;
};
template<> struct is_int<int>
{
static const bool value = true;
};
template<class T> void is_an_int(T const &)
{
std::cout<<is_int<T>::value<<"\n";
}
class A{};
int main()
{
int i;
A a;
double d;
std::cout<<std::boolalpha;
std::cout<<"is_an_int(i) ? ";
is_an_int(i);
std::cout<<"is_an_int(a) ? ";
is_an_int(a);
std::cout<<"is_an_int(d) ? ";
is_an_int(d);
return 0;
}
Les classes traits sont beaucoup utilisées dans la programmation
générique. Vous pouvez trouver une introduction aux classes
traits et aux classes de politiques dans : Présentation des classes de Traits et de Politiques en C++Présentation des classes de Traits et de Politiques en C++,
par Alp Mestan.
Actuellement, le C++ possède quelques classes traits dans
la STL. Par exemple, la bibliothèque string
contient des classes traits sur les types de caractères pour
définir des propriétés sur ceux-ci :
template<class charT> char_traits<charT>
est spécialisée avec char et wchar_t.
Cela permet de n'avoir qu'une seule classe std::basic_string
et de s'appuyer sur ces classes traits pour définir les chaînes
ANSI (std::string) ou UNICODE
(std::wstring) :
namespace std {
template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string;
typedef basic_string<char,char_traits<char> > string;
typedef basic_string<wchar_t,char_traits<<wchar_t> > wstring;
}La bibliothèque Boost.Type TraitsBoost.Type Traits propose beaucoup de classes traits permettant d'avoir un nombre appréciable d'informations.
C++0x : la nouvelle norme prévoir d'intégrer une collection plus riche de classes traits. Beaucoup des ces classes qui nécessitent aujourd'hui une bibliothèque comme Boost.Type Traits seront avec les compilateurs conformes à cette nouvelle norme directement disponibles avec le fichier d'en-tête #include <type_traits>. Voici quelques exemples tirés du draft N3000 de septembre 2009 (donc encore susceptible d'évoluer) :
namespace std {
template <class T> struct is_void;
template <class T> struct is_integral;
template <class T> struct is_floating_point;
template <class T> struct is_array;
template <class T> struct is_pointer;
template <class T> struct is_class;
template <class T> struct is_const;
template <class T> struct is_polymorphic;
template <class T> struct is_abstract;
// etc.
}XXII-D. Conversions entre type de base et type dérivé▲
XXII-D-1. Conversion du type dérivé vers le type de base▲
La conversion depuis le type dérivé vers le type de base (upcast) est automatique en C++ dès lors que l'héritage est public. Nous l'avons employé dans chacun des exemples précédents sans que cela pose le moindre problème :
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
int main()
{
derived d;
base b = d;
base b2;
b2 = d;
base &rb = d;
base *pb = &d;
return 0;
}base b = d; suppose que
base est constructible par copie. En
effet cette ligne appel le constructeur de copie de
base sur la nouvelle variable
b avec en paramètre d.
b2 = d; suppose que
base est assignable. En
effet cette ligne appel l'opérateur d'affectation
base sur la variable b2
avec en paramètre d.
Les deux autres lignes base &rb = d;
et base *pb = &d; ne supposent
rien sur le type base et définissent
une référence (respectivement un pointeur) dont
le type statique est base et le type
dynamique derived.
La conversion depuis un type dérivé vers un type de base
ne pose strictement aucun problème. S'il s'agit
de pointeurs ou de références alors la nouvelle variable
pointe/référence un objet du type dérivé au travers
d'un pointeur/référence de type statique de base. Si
la conversion va vers une valeur, bien sûr l'information
du type dérivé est perdue puisqu'un nouvel objet du
type de base est construit avec uniquement les
informations du type de base contenu dans l'objet
dérivée.
XXII-D-2. Conversion du type de base vers le type dérivé▲
Si vous venez du monde du C ou que vous avez eu un cours mal appelé C/C++, vous aurez peut être le réflexe de faire ça :
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
int main()
{
derived d;
base &rb = d;
derived&rd = (derived&)d;
return 0;
}Oubliez tout de suite ce genre de conversion car un jour vous aurez ça :
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
int main()
{
derived d;
int i;
derived&rd = (derived&)i;
return 0;
}
Code qui compile parfaitement mais qui emmènera votre
application directement dans le mur.
Le C++ propose un premier opérateur de conversion entre
types compatibles : static_cast. Cet
opérateur vous préserve de l'erreur précédente :
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
int main()
{
derived d;
int i;
derived&rd = static_cast<derived&>(i);
// 'static_cast' : impossible de convertir de 'int' en 'derived &'
return 0;
}Utilisé avec une valeur, l'opérateur static_cast<T>(e) est valide si la déclaration T t_(e) est valide. Autrement dit, la conversion réussie s'il existe un constructeur prenant un argument du type de e ou d'un type pouvant être automatiquement converti depuis e. Donc, en général, vouloir affecter à une classe dérivée un objet d'une classe de base provoque une erreur ce qui est bien normale :
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
int main()
{
base b;
derived d = static_cast<derived>(b);
// 'static_cast' : impossible de convertir de 'base' en 'derived'
return 0;
}La conversion d'un type de base vers un type dérivé (downcast) peut être intéressante si le type dynamique du type de base correspond au type dérivé cible. Cela peut donc se faire avec les références et les pointeurs :
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
int main()
{
derived d;
base &rb = d;
derived &rd = static_cast<derived&>(rb); // rd désigne maintenant d
return 0;
}L'opérateur static_cast ne peut pas s'appliquer si derived hérite virtuellement de base. Le compilateur signale une erreur.
A noter que si le type dynamique de rb n'est pas derived (ou une classe dérivant de derived), la conversion est indéterminée, en d'autres termes c'est une erreur d'exécution. Cette erreur est silencieuse et le plantage peut avoir lieu à n'importe quel moment, en particulier très loin après le cast malheureux :
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
struct derived2 : public base
{
};
int main()
{
derived d;
base b;
base &rb = b;
derived &rd = static_cast<derived&>(rb);
// OK à la compilation mais
// erreur à l'exécution car rb désigne un base (b) et non un derived
derived2 d2;
base &rb2 = d2;
derived &rd2 = static_cast<derived&>(rb2);
// OK à la compilation mais
// erreur à l'exécution car rb2 désigne un derived_2 (d2) et non un derived
return 0;
}Pour pallier à ces défauts, un autre opérateur existe en C++ : dynamic_cast. Il fonctionne de façon semblable à static_cast si ce n'est qu'il vérifie à l'exécution que le type dynamique de l'expression peut être converti vers le type demandé. Comme le type dynamique est sollicité, dynamic_cast ne s'applique qu'aux pointeurs et aux références. Si la conversion échoue avec une référence, alors l'opérateur soulève une exception std::bad_cast définie dans le fichier typeinfo. Cette exception peut ainsi être récupérée et traitée :
#include <typeinfo>
#include <iostream>
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
struct derived2 : public base
{
};
int main()
{
derived d;
base &rb_1 = d;
try{
std::cout<<"dynamic_cast<derived&>(base &rb_1 = derived d) : ";
derived &rd = dynamic_cast<derived&>(rb_1);
std::cout<<"Conversion valide\n";
}
catch(std::bad_cast const &e_){
std::cout<<"Erreur de dynamic_cast : "<<e_.what()<<std::endl;
}
base b;
base &rb_2 = b;
try{
std::cout<<"dynamic_cast<derived&>(base &rb_2 = base b) : ";
derived &rd = dynamic_cast<derived&>(rb_2);
std::cout<<"Conversion valide\n";
}
catch(std::bad_cast const &e_){
std::cout<<"Erreur de dynamic_cast : "<<e_.what()<<std::endl;
}
derived2 d2;
base &rb_3 = d2;
try{
std::cout<<"dynamic_cast<derived&>(base &rb_3 = derived2 d2) : ";
derived &rd = dynamic_cast<derived&>(rb_3);
std::cout<<"Conversion valide\n";
}
catch(std::bad_cast const &e_){
std::cout<<"Erreur de dynamic_cast : "<<e_.what()<<std::endl;
}
return 0;
}Ce code produit la sortie suivante :
dynamic_cast<derived&>(base &rb_1 = derived d) : Conversion valide
dynamic_cast<derived&>(base &rb_2 = base b) : Erreur de dynamic_cast : std::bad_cast
dynamic_cast<derived&>(base &rb_3 = derived2 d2) : Erreur de dynamic_cast : std::bad_castAvec les pointeurs, l'erreur n'est pas remontée avec une exception mais en retournant un pointeur nul :
#include <iostream>
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
struct derived2 : public base
{
};
int main()
{
derived d;
base *pb_1 = &d;
std::cout<<"dynamic_cast<derived*>(base *pb_1 = derived d) : ";
derived *pd_1 = dynamic_cast<derived*>(pb_1);
if(pd_1){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}
base b;
base *pb_2 = &b;
std::cout<<"dynamic_cast<derived*>(base *pb_2 = base b) : ";
derived *pd_2 = dynamic_cast<derived*>(pb_2);
if(pd_2){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}
derived2 d2;
base *pb_3 = &d2;
std::cout<<"dynamic_cast<derived*>(base *pb_3 = derived2 d2) : ";
derived *pd_3 = dynamic_cast<derived*>(pb_3);
if(pd_3){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}
return 0;
}Le résultat obtenu :
dynamic_cast<derived*>(base *pb_1 = derived d) : Conversion valide
dynamic_cast<derived*>(base *pb_2 = base b) : Echec de la conversion
dynamic_cast<derived*>(base *pb_3 = derived2 d2) : Echec de la conversionAppliqué à un pointeur nul, dynamic_cast retourne un pointeur nul :
#include <iostream>
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};
int main()
{
base *pb_1 = 0;
std::cout<<"dynamic_cast<derived*>(base *pb_1 = 0) : ";
derived *pd_1 = dynamic_cast<derived*>(pb_1);
if(pd_1){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}
return 0;
}La sortie est :
dynamic_cast<derived*>(base *pb_1 = 0) : Echec de la conversiondynamic_cast ne peut s'appliquer que sur une expression dont le type est polymorphique :
struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};
struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};
int main()
{
polymorphic_derived pd;
polymorphic_base &rpd = pd;
polymorphic_derived &rpdd = dynamic_cast<polymorphic_derived &>(rpd);// OK
non_polymorphic_derived npd;
non_polymorphic_base &rnpd = npd;
non_polymorphic_derived &rnpdd = dynamic_cast<non_polymorphic_derived &>(rnpd);
// Erreur de compilation : 'non_polymorphic_base' n'est pas un type polymorphe
return 0;
}Enfin, dynamic_cast échoue si l'héritage n'est pas public (héritage privée ou protégé) ou s'il est ambigüe (héritage multiple).
XXII-D-3. Pourquoi faire une conversion d'un type de base vers un type dérivé▲
95% des cas où vous avez besoin de convertir un pointeur ou une référence depuis une classe de base vers un type dérivé traduisent une erreur de conception. Je ne parlerai pas des 5% restant pour ne pas semer le trouble. Si le downcast vous titille, visiter le forum C++ pour confirmer votre besoin ou refroidir vos ardeurs.


