Вы находитесь на странице: 1из 40

LE C++ POUR LES

PROS
OURS BLANC DES CARPATHES
ISIMA1999

Le C++ pour les Pros

Table des matires


1. LA FORME CANONIQUE DE COPLIEN
1.1
1.2
1.2.1
1.2.2
1.2.3
1.3
1.4
1.4.1
1.4.2
1.5
1.5.1
1.5.2
1.5.3
1.6
1.7

CONSTRUCTEUR PAR DFAUT


CONSTRUCTEUR DE RECOPIE
BUT DU CONSTRUCTEUR DE RECOPIES
PROTOTYPE DU CONSTRUCTEUR DE RECOPIE
QUAND EST IL NCESSAIRE DE CRER UN CONSTRUCTEUR DE RECOPIE ?
LE DESTRUCTEUR
OPRATEUR DAFFECTATION
PROTOTYPE DE LOPRATEUR DAFFECTATION
DIFFICULT SUPPLMENTAIRE PAR RAPPORT AU CONSTRUCTEUR PAR RECOPIE
EXERCICE RSOLU : UNE CLASSE CHANE DE CARACTRES
STRUCTURE :
MISE EN UVRE
CODE RSULTANT
EXERCICE RSOLU : DIFFRENCE ENTRE CONSTRUCTION ET AFFECTATION
LES CONSTRUCTEURS NE SUPPORTENT PAS LE POLYMORPHISME !

4
5
5
5
6
6
7
7
7
7
7
8
8
10
11

2. DE LA SURCHARGE DES OPRATEURS ET DE LA BONNE UTILISATION DES


RFRENCES EN GNRAL

13

2.1
2.2
2.2.1
2.2.2
2.2.3
2.3
2.4
2.4.1
2.4.2
2.5
2.6
2.7
2.7.1
2.7.2
2.7.3

13
13
13
14
15
15
17
17
19
19
22
24
24
25
25

BUTS
CHOISIR ENTRE MTHODE ET FONCTION
UN EXEMPLE PARTICULIER : LES OPRATEURS DE REDIRECTION
UNE RGLE BIEN UTILE
RCAPITULATIF
QUELS OPRATEURS NE PAS SURCHARGER ?
QUELS PARAMTRES DOIVENT PRENDRE LES OPRATEURS
PRFRER LES RFRENCES CONSTANTES AUX OBJETS
POURQUOI NE PAS TOUJOURS RENVOYER UNE RFRENCE
QUE DOIVENT RENVOYER LES OPRATEURS ?
LE CAS PARTICULIER DES OPRATEURS DINCRMENTATION
LE CAS PARTICULIER DES OPRATEURS NEW ET DELETE
BIEN COMPRENDRE LE FONCTIONNEMENT DE NEW ET DELETE
UNE VERSION SPCIALE DE NEW : OPERATOR NEW DE PLACEMENT
EXERCICE : UN EXEMPLE SIMPLISTE DE SURCHARGE DE NEW ET DELETE

3. LA GESTION DES EXCEPTIONS

26

3.1
3.2
3.3
3.4
3.4.1
3.4.2
3.5

26
26
27
28
28
30
31

SCHMA GNRAL DINTERCEPTION DES EXCEPTIONS


LA LOGIQUE DE TRAITEMENT DES EXCEPTIONS
RELANCER UNE EXCEPTION
UTILISER SES PROPRES EXCEPTIONS
DCLARER SES EXCEPTIONS
UTILISER SES EXCEPTIONS
LES SPCIFICATEURS DEXCEPTION

4. LES TRANSTYPAGES EN C++

Le C++ pour les Pros

32

4.1
NOUVELLE SYNTAXE DE LOPRATEUR TRADITIONNEL
4.2
NOUVEAUX OPRATEURS DE TRANSTYPAGE
4.2.1
POURQUOI ?
4.2.2
SYNTAXE
4.2.3
FONCTIONNALITS
4.3
LES CONVERSIONS IMPLICITES
4.3.1
LES OPRATEURS DE CONVERSION IMPLICITES
4.3.2
LES CONSTRUCTEURS AVEC UN SEUL PARAMTRE

32
33
33
33
34
37
37
39

5. EN GUISE DE CONCLUSION

40

Table des illustrations


Figure 3.1 Schma gnral de gestion des exceptions en C++...................................................................................26
Figure 3.1 Hirarchie dexceptions du TasMax .........................................................................................................29
Figure 4.1 La ligne des objets graphiques................................................................................................................35
Programme 1.1 Forme canonique de Coplien ..............................................................................................................4
Programme 1.1 Constructeur par dfaut et tableaux....................................................................................................5
Programme 1.1 Ralisation de la classe Chaine ..........................................................................................................9
Programme 1.1 Exercice sur la forme canonique de Coplien.....................................................................................10
Programme 1.1 tentative dappel polymorphique au sein dun constructeur ..............................................................11
Programme 1.2 fonction main de tentative dappel polymorphique dans un constructeur.........................................12
Programme 1.3 Rsultat chimrique obtenu par appel polymorphique dans un constructeur .....................................12
Programme 1.4 Rsultat (dcevant ) rellement obtenu ...........................................................................................12
Programme 2.1 la classe Evaluable ......................................................................................................................18
Programme 2.2 un oprateur de comparaison gnrique ? ........................................................................................18
Programme 2.3 la classe Penalites ......................................................................................................................18
Programme 2.4 Utilisation de loprateur < ..............................................................................................................19
Programme 2.5 un oprateur de comparaison gnrique !.........................................................................................19
Programme 2.1 Dclaration dune classe Rationnel .............................................................................................20
Programme 2.2 Oprateur de multiplication sur les rationnels ..................................................................................20
Programme 2.3 oprateur de multiplication 2me forme (errone)...............................................................................22
Programme 2.4 oprateur de multiplication 3me forme (errone)...............................................................................22
Programme 2.1 forme canonique des oprateurs de pr et post incrmentation .........................................................23
Programme 2.2 criture de loprateur de post incrmentation .................................................................................24
Programme 2.1 Loperator new de placement....................................................................................................25
Programme 3.1 Relancer une exception.....................................................................................................................27
Programme 3.1 Dclaration de la classe std::exception.............................................................................................28
Programme 3.2 Exemple dune hirarchie dexceptions.............................................................................................30
Programme 3.1 Exemple de gestion des erreurs en provenance du Tas Max ..............................................................31
Programme 3.1 Tentative de lancement dune exception non prvue .........................................................................31
Programme 3.2 Spcificateurs dexceptions pour les mthodes du TasMax................................................................31
Programme 4.1 exemple minimaliste de static_cast ...........................................................................................33
Programme 4.1 exemple dutilisation de const_cast ............................................................................................34
Programme 4.1 utilisation typique de dynamic_cast ............................................................................................36
Programme 4.1 Dfinition dun oprateur de conversion implicite depuis Rationnel vers le type double...........37
Programme 4.2 Utilisation dun oprateur de conversion implicite ...........................................................................37
Programme 4.1 Bug insidieux d un oprateur de conversion implicite ..................................................................38
Programme 4.1 Exemple derreur lie aux constructeurs un seul paramtre ...........................................................39

Tableau 2.1 Description des surcharge doprateurs .................................................................................................15


Tableau 2.1 Oprateurs du C++ ne pouvant tre surchargs.....................................................................................16
Tableau 2.2 Oprateurs C++ quil vaut mieux ne pas surcharger..............................................................................16
Tableau 4.1 Transtypages ralisables avec const_cast ........................................................................................34

Le C++ pour les Pros

1. La forme canonique de Coplien


La forme canonique, dite de Coplien, fournit un cadre de base respecter pour
les classes non triviales dont certains attributs sont allous dynamiquement.
Une classe T est dite sous forme canonique (ou forme normale ou forme
standard) si elle prsente les mthodes suivantes :
class T
{
public:
T ();
T (const T&);
~T ();
T &operator=(const T&);
};

//
//
//
//

Constructeur par dfaut


Constructeur de recopie
Destructeur ventuellement virtuel
Operator daffectation

Programme 1.1 Forme canonique de Coplien


Etudions chacune des composantes de la forme canonique :

1.1 Constructeur par dfaut


Le constructeur par dfaut est un constructeur sans argument ou dont chacun
des arguments possde une valeur par dfaut.
Il est utilis, par exemple, lorsque lon cre des tableaux dobjet. En effet, il nest
pas possible de passer un constructeur loprateur new[] responsable de la cration
des tableaux.
En outre, de nombreuses bibliothques commerciales ou non (Third Party
Software), dont la bibliothque standard du C++, ncessitent un constructeur par
dfaut pour la fabrication dobjets temporaires.
Si vous ne spcifiez aucun constructeur pour votre classe, alors le compilateur
essaye de fournir un constructeur par dfaut, qui, selon les environnements de
dveloppement, initialise zro chacun des membres de la classe ou les laisse vide. Au
cas o votre classe agrgerait par valeur des objets, ceux-ci sont initialiss par leur
constructeur par dfaut sil existe autrement, le constructeur par dfaut
automatique ne peut pas tre construit faute de moyen de construction des objets
agrgs.
A partir du moment o vous spcifiez au moins un constructeur pour votre
classe, le compilateur nessaie plus de gnrer de constructeur par dfaut automatique
considrant que ceci est de votre ressort. De manire gnrale, il est trs dangereux de
ne pas fournir de constructeur pour une classe, sauf, peut-tre, dans le cas de classes
exception sans attribut.
Le Programme 1.2 montre un exemple o il est ncessaire de fournir un
constructeur par dfaut la classe Toto.

Le C++ pour les Pros

class Toto
{
private:
// declarations sans intrt
public:
// Toto(int i); // Constructeur
};
int main(int, char**)
{
Toto tableau[3]; //
//
//
//
}

Erreur !
il faudrait un
constructeur par
dfaut !

class Toto
{
private:
// declarations sans intrt
public:
// Toto(int i=3); // Constructeur
// par dfaut !
};
int main(int, char**)
{
Toto tableau[3]; //
//
//
//
}

Correct !
tous les objets
sont construits
avec la valeur 3

Programme 1.2 Constructeur par dfaut et tableaux

1.2 Constructeur de recopie


1.2.1 But du constructeur de recopies
Comme son nom lindique, le constructeur de recopie est spcialis dans la
cration dun objet partir dun autre objet pris comme modle. Jinsiste sur le fait
quil sagit dune cration, donc lintrieur dune dclaration dobjet. Toute autre
duplication (au cours de la vie dun objet) sera faite par loprateur daffectation.
Le constructeur de recopie a galement deux autres utilisations spcifies dans
le langage :

Lorsquun objet est pass en paramtre par valeur une fonction ou une
mthode, il y a appel du constructeur par recopie pour gnrer lobjet utilis
en interne dans celle-ci. Ceci garantit que lobjet original ne peut tre
modifi ! Lautre solution consiste utiliser une rfrence constante.
Au retour dune fonction / mthode renvoyant un objet, il y a cration dun
objet temporaire par le constructeur de recopie, lequel est plac sur la pile
pour usage par le code appelant.

Ces deux fonctionnalits seront tudies en dtail dans les paragraphes


concernant le type de retour des surcharges doprateurs.

1.2.2 Prototype du constructeur de recopie


La forme habituelle dun constructeur de recopie est la suivante :
T::T(const T&)
Le paramtre est pass en rfrence, ce qui assure de bonnes performances et
empche un bouclage infini. En effet, si largument tait pass par valeur, il devrait
tre construit par le constructeur de recopie. Hors cest prcisment ce dernier que
nous btissons : il y aurait un appel rcursif infini !
Le C++ pour les Pros

Dautre part, la rfrence est constante, ce qui garantit que seules des mthodes
constantes ne pouvant pas modifier les attributs seront appelables sur lobjet pass en
argument. Notons toutefois que cette prcaution est une chimre. En effet, le
constructeur par recopie tant ncessairement une mthode dinstance appartenant
la classe T, il a visibilit sur les attributs de la classe et peut donc ainsi dlibrment
modifier les attributs de lobjet pass en argument sans passer par ses mthodes. Il
appartient donc au programmeur de ne pas modifier lobjet argument.

1.2.3 Quand est il ncessaire de crer un constructeur de recopie ?


Si nous ne spcifiez pas de constructeur par recopie, le compilateur en cre un
automatiquement. Celui-ci est trs simple : il recopie bit bit, de manire
optimise lobjet source dans lobjet destination.
Ce comportement est absolument parfait pour les classes simples ne ralisant ni
agrgation ni allocations dynamiques. Dans ce cas l, vous navez pas besoin de
fournir un constructeur de recopie car vous ferez probablement moins bien que le
compilateur lui mme.
Dans tous les autres cas, vous aurez besoin de dfinir un constructeur de
recopie :
Agrgation par valeur : il vous faudra recopier les objets agrgs par valeur,
en appelant leur constructeur par recopie !
Agrgation par rfrence (ou pointeur) et allocation dynamique de
mmoire : la copie optimise ne gnrant que des copies de pointeurs,
diffrents objets utiliseraient les mme cases mmoire. Dune part, les
modifications de lun seraient rpercutes sur lautre, mais, plus grave, en
labsence de mcanismes de garde fou, les mmes blocs de mmoire seraient
librs plusieurs fois par les destructeurs : il est ncessaire de faire une
recopie des objets agrgs par rfrence (avec le constructeur de recopie )
ou des blocs de mmoire (new )

1.3 Le destructeur
Le destructeur est l pour rendre les ressources occupes par un objet lorsquil
arrive en fin de vie, soit quil soit allou statiquement et sorte de porte, soit quil ait
t allou dynamiquement laide de new et quon lui applique delete pour le
supprimer.
Si un objet agrge statiquement dautres objets, ceux-ci sont automatiquement
dtruits : il ny a pas lieu de sen soucier.
En revanche, sil agrge dynamiquement dautres objets (en particulier par
lintermdiaire de pointeurs), il sera indispensable de les dtruire (par un appel
delete) sous peine de ne pouvoir disposer nouveau de la mmoire avant le prochain
reboot !
Pour conclure sur le destructeur, celui-ci sera ncessaire ds lors que votre
classe ralise de lagrgation par pointeur ou de lallocation dynamique de mmoire.
Le C++ pour les Pros

1.4 Oprateur daffectation


Loprateur daffectation est utilis pour dupliquer un objet dans un autre en
dehors des dclarations o ce rle est dvolu au constructeur de recopie. De fait, il est
ncessaire de crer un oprateur lorsquil est ncessaire de crer un oprateur de
recopie : agrgation et allocation dynamique de mmoire ; dans tous les autres cas, le
compilateur cre pour vous un oprateur daffectation par recopie bit bit
optimise qui frle la perfection .

1.4.1 Prototype de loprateur daffectation


Loprateur daffectation prsente typiquement la forme suivante :
T& T::operator=(const T&)
Le paramtre est pass par rfrence constante pour les raisons explicites dans
le cas du constructeur par recopie.
Cet oprateur renvoie une rfrence sur T afin de pouvoir le chaner avec
dautres affectations. Rappelons que loprateur daffectation est associatif droite, en
effet, lexpression a=b=c est value comme : a=(b=c). Ainsi, la valeur renvoye par
une affectation doit tre son tour modifiable, aussi, il est naturel de renvoyer une
rfrence.

1.4.2 Difficult supplmentaire par rapport au constructeur par


recopie
Un constructeur travaille toujours sur un matriau vierge : en effet, il est
toujours appel juste aprs lallocation de mmoire. Tout au contraire, loprateur
daffectation est appel sur un objet totalement instanci et, bien souvent, ayant dj
un lourd pass derrire lui. Aussi, toute opration daffectation devra commencer par
rtablir un terrain favorable la recopie en dtruisant les blocs de mmoire dj
affects ainsi que les objets agrgs.
Pour rsumer, loprateur daffectation cumule des fonctionnalits limites de
destructeur et de constructeur par recopie.
Notons que la plupart des bibliothques (dont la STL du C++) ncessitent la
disponibilit de loprateur daffectation pour la plupart des classes quelles
manipulent.

1.5 Exercice rsolu : une classe Chane de Caractres


Appliquons les principes prcdents la constitution dune chane de caractres
mise sous forme canonique de Coplien.

1.5.1 Structure :
Nous allons crer une chane toute simple, dote de 3 attributs :

Le C++ pour les Pros

Un tableau de caractres (char *) contenant la chane proprement dite.


Un entier dnotant la capacit de la chane, i.e. le nombre de caractres
quelle peut contenir un instant donn. Il sagit donc de la capacit
physique du tableau de caractres.
Un entier dnotant la longueur de la chane, i.e. le nombre de caractres
significatifs quelle contient. Il sagit de linformation renvoye par
lapplication de strlen sur le tableau de caractres.

1.5.2 Mise en uvre


Examinons chacun des membres de la forme canonique :
Constructeur par dfaut : nous allons utiliser un constructeur qui cre une
chane de longueur nulle mais susceptible de contenir 10 caractres.
Constructeur par recopie : il devra positionner correctement la capacit et la
longueur mais galement allouer de la mmoire pour le tableau de
caractres et recopier celui de son modle.
Destructeur : il sera charg de rendre la mmoire du tableau de caractres
Oprateur daffectation : aprs nettoyage de la mmoire, il devra allouer de
la mmoire puis recopier la chane du modle. Attention ! il ne faut surtout
pas employer strdup car ce dernier utilise malloc, incompatible avec les
mcanismes de new et delete.

1.5.3 Code rsultant


Voici limplmentation finale de notre classe chane. Bien entendu, la forme
canonique ne fournit que le canevas de base ncessaire une utilisation rationnelle de
la mmoire qui ne comprend pas les mthodes ncessaires au bon fonctionnement de
la chane.
#ifndef __Chaine_H__
#define __Chaine_H__
#include <string.h>
class Chaine
{
private:
int
capacite_;
int
longueur_;
char *tab;
enum {CAPACITE_DEFAUT=16}; // Capacit par dfaut
// et granularit d'allocation
public:
// constructeur par dfaut
Chaine(int capacite=CAPACITE_DEFAUT) :
capacite_(capacite),
longueur_(0)
{
tab=new char[capacite_];

Le C++ pour les Pros

tab[0]=0;
}
// second constructeur
Chaine(char *ch)
{
int lChaine=strlen(ch);
int modulo=lChaine % CAPACITE_DEFAUT;
if (modulo)
capacite_=((lChaine / CAPACITE_DEFAUT)+1)*CAPACITE_DEFAUT;
else
capacite_=lChaine;
longueur_=lChaine;
tab=new char[capacite_];
strcpy(tab,ch);
}
// constructeur par recopie
Chaine(const Chaine &uneChaine) :
capacite_(uneChaine.capacite_),
longueur_(uneChaine.longueur_)
{
tab=new char[capacite_];
strcpy(tab, uneChaine.tab);
}
// accs aux membres
int capacite(void)
{
return capacite_;
}
int longueur(void)
{
return longueur_;
}
// destructeur
~Chaine(void)
{
if (tab)
delete [] tab; // ne jamais oublier les crochets lorsque
// l'on dsalloue un tableau !
}
// Operateur d'affectation
Chaine & operator=(const Chaine& uneChaine)
{
if (tab)
delete [] tab;
capacite_=uneChaine.capacite_;
longueur_=uneChaine.longueur_;
tab = new char[capacite_];
strcpy(tab,uneChaine.tab);
return *this;
}
};
#endif

Programme 1.3 Ralisation de la classe Chaine

Le C++ pour les Pros

1.6 Exercice rsolu : diffrence entre construction et affectation


Enonc :
Dcrivez lignes de code suivantes (mettant en jeu les classes T et U) en
sparant ventuellement les cas T=U et T8
1)
2)
3)
4)
5)
6)
7)
8)

T
U
T
T
T
z
T
t

t;
u;
z(t);
v();
y=t;
= t;
a(u);
= u;

Programme 1.4 Exercice sur la forme canonique de Coplien


Correction :
T t
Construction dun objet t de classe T avec appel du constructeur par
dfaut
U u
Construction dun objet u de classe U avec appel du constructeur par
dfaut
T z(t) Construction dun objet z de classe T avec appel du constructeur de
recopie sur lobjet t
T v() Dclaration dune fonction v renvoyant un objet de classe T et ne
prenant pas de paramtre.
T y=t Construction dun dobjet y de classe T avec appel du constructeur de
recopie sur lobjet t. Il ne peut en aucun cas sagir dune affectation car nous
sommes dans une dclaration et lobjet y doit tre construit ! En fait les
lignes 3 et 5 sont quivalentes
z = t Affectation de lobjet t lobjet z par loprateur daffectation. Nous
sommes bien ici dans le cadre dune affectation : les objets t et z sont dj
construits et la ligne de code nest pas une dclaration
T a(u) Dans le cas gnral (T8  LO VDJLW GH OD FRQVWUXFWLRQ GXQ REMHW GH
classe T partir dun objet de classe U laide dun constructeur de la forme
T(const U&). Dans le cas dgnr (T=U), il sagit dun appel au
constructeur de recopie.
t = u Dans le cas gnral (T8  LO sagit de laffectation dun objet de classe
U un objet de classe T laide dun oprateur de la forme
T& operator=(const U&). Dans le cas dgnr (T=U), il sagit dune
affectation laide de loprateur daffectation de la forme canonique.

Le C++ pour les Pros

10

1.7 Les constructeurs ne supportent pas le polymorphisme !


Bien que ce dernier propos soit dtach de la librairie standard du C++, je crois
bon de rappeler que les constructeurs ne supportent pas le polymorphisme. En
particulier, il est impossible dappeler automatiquement depuis le constructeur de la
classe de base une mthode polymorphique.
Considrons par exemple le code suivant :
#include <iostream.h>
class T1
{
public:
virtual void afficher(void)
{
cout << "Classe T1" << endl;
}
T1(void)
{
afficher();
}
virtual ~T1()
{
cout << "On detruit T1" << endl << "Affichage ";
afficher();
}
};
class T2 : public T1
{
public:
virtual void afficher(void)
{
cout << "Classe T2" << endl;
}
T2(void) : T1()
{
}
virtual ~T2()
{
cout << "On detruit T2" << endl << "Affichage ";
afficher();
}
};

Programme 1.5 tentative dappel polymorphique au sein dun constructeur


Le but ici est dappeler la mthode afficher spcifique chaque classe
lintrieur de son constructeur. De mme, on ralise la mme opration lintrieur du
destructeur.
Voici la fonction main associe :
int main(int, char**)
{

Le C++ pour les Pros

11

cout << "Avant la construction d'un objet de classe T1" << endl;
T1 t1;
cout << "Avant la construction d'un objet de classe T2" << endl;
T2 t2;
return 0;
}

Programme 1.6 fonction main de tentative dappel polymorphique dans un


constructeur
On sattendrait donc un rsultat du style :
Avant la construction d'un objet de classe T1
Classe T1
Avant la construction d'un objet de classe T2
Classe T2
On detruit T2
Affichage Classe T2
On detruit T1
Affichage Classe T1
On detruit T1
Affichage Classe T1

Programme 1.7 Rsultat chimrique obtenu par appel polymorphique dans


un constructeur
Hors, le rsultat obtenu est le suivant :
Avant la construction d'un objet de classe T1
Classe T1
Avant la construction d'un objet de classe T2
Classe T1
On detruit T2
Affichage Classe T2
On detruit T1
Affichage Classe T1
On detruit T1
Affichage Classe T1

Programme 1.8 Rsultat (dcevant ) rellement obtenu


Le verdict est simple : le constructeur de T1 appelle toujours la mthode
T1::afficher et napplique pas le polymorphisme alors que le destructeur qui lui,
est une mthode dinstance virtuelle le fait sans problme !
Ceci sexplique en fait trs simplement : lors de la construction dun objet de
classe T1 par son constructeur la table des mthodes virtuelles nest pas encore
affecte lobjet et les diffrentes mthodes appeles le sont toujours par liaison
statique. Cest la sortie du constructeur que la table des mthodes virtuelles est
oprationnelle.
Bien entendu, il sagit l dun exemple dcole mais bien des personnes tentaient
dappeler une mthode dinitialisation automatique polymorphique des attributs des
valeurs par dfaut de cette manire. Je vous laisse augurer de leur dception !

Le C++ pour les Pros

12

2. De la surcharge des oprateurs et de la bonne


utilisation des rfrences en gnral
Ce chapitre traite plus spcialement de la surcharge des oprateurs mais plus
gnralement de la bonne utilisation des rfrences en C++. En effet, la plupart des
rgles exposes ici sur les oprateurs seront applicables directement aux autres
mthodes ou fonctions.

2.1 Buts
La surcharge des oprateurs standard est assurment lune des fonctionnalits
de C++ les plus apprcies. En effet, il est indubitable que cela permet dcrire du code
ayant un aspect trs naturel.
Hors, leur criture est trs pigeuse. En effet, certains doivent tre surchargs
en tant que fonctions, dautres en tant que mthodes, leur type de retour doit tre
soigneusement tudi etc.
Il nest donc pas inutile dy revenir quelque peu !

2.2 Choisir entre mthode et fonction


Comment choisir si lon doit dfinir un oprateur surcharg en tant que
mthode ou bien en tant que fonction externe la classe ?

2.2.1 Un exemple particulier : les oprateurs de redirection


Dans certains cas, la question ne se pose mme pas. Supposons, par exemple,
que lon souhaite fournir pour une classe T une version des oprateurs << et >>
permettant respectivement dcrire le contenu de T sur un flux en sortie (ostream) et
de lire depuis un flux en entre (istream) le contenu de T.
Ces oprations ne peuvent absolument pas tre codes en tant que mthodes de
la classe T. Considrons les rsultats dsastreux que cela pourrait avoir. La
dclaration / dfinition de cet oprateur pourrait tre :
class T
{
public:
ostream &operator<<(ostream &a)
{
// code omis ...
return a;
}
};

Avec les dclarations :


T t;

Le C++ pour les Pros

13

Au moment de lutiliser, loprateur << devrait scrire, sous forme suffixe :


t.operator<<(cout);

Soit, sous forme infixe :


T << cout;

Ce qui ne correspond absolument pas lutilisation habituelle de cet oprateur.


Le seul moyen dutiliser une mthode pour le surcharger avec sa syntaxe habituelle
serait donc de linclure dans la classe ostream, ce qui est inconcevable. Il est donc
ncessaire de le dfinir sous la forme dune fonction externe, ventuellement amie de
la classe T si elle doit accder aux attributs protgs, soit :
ostream &operator<<(ostream &a, const T &t)

Bien que cela pose moins de problmes, il sera galement judicieux de dclarer
sous forme dune fonction externe loprateur de redirection depuis un flux.

2.2.2 Une rgle bien utile


En rgle gnrale, nous appliquerons la rgle suivante pour dterminer si un
oprateur doit tre surcharg en tant que mthode ou en tant que fonction externe
(ventuellement amie).
Un oprateur ne sera dclar en tant que mthode que dans les cas o this
joue un rle privilgi par rapport aux autres arguments
Donnons deux exemples qui nous permettrons de juger du bien fond de cette
rgle fonde sur des critres conceptuels. En effet, on ne considrera quune mthode
comme lgitime si son impact sur le paramtre par dfaut est suffisant.
2.2.2.1 Le cas de loprateur daffectation
Soit le cas de loprateur daffectation que nous crivons comme une mthode :
T& T::operator=(const T&a)

Etudions les rles respectifs de largument par dfaut this et de largument


explicite a.

Largument a nest accd quen lecture et lopration ne modifie en rien son


statut.
En revanche, this est considrablement modifi car, a priori, tous ces
attributs sont modifis. En outre, cest une rfrence sur lobjet *this qui
est renvoye par loprateur.

Au regard de ces deux considrations, il est vident que this joue un rle
privilgi par rapport au second argument, il est donc logique que ce soit une mthode
de la classe T.
Le C++ pour les Pros

14

2.2.2.2 Le cas de loprateur daddition


Equipons dornavant notre classe T dun oprateur dyadique quelconque, par
exemple, laddition. Si nous dcidons den faire une mthode, son prototype sera :
T T::operator+(const T&a)

Comme pour le cas prcdent, tudions les rles respectifs de largument par
dfaut this et de largument explicite a. Dans une addition, aucun des deux membres
nest modifi et le rsultat nest ni le membre de gauche, ni celui de droite : en fait les
deux arguments jouent un rle absolument symtrique.
De ce fait, this na pas un rle privilgi par rapport a et loprateur est alors
transform en fonction externe, soit :
T ::operator+(const T&a, const T&b)

2.2.3 Rcapitulatif
Le tableau suivant rcapitule quels oprateurs seront dcrits en tant que
mthode ou fonction externe :
Affectation, affectation avec opration (=, +=, *=, etc.)

Mthode

Oprateur fonction ()

Mthode

Oprateur indirection *

Mthode

Oprateur crochets []

Mthode

Incrmentation ++, dcrmentation --

Mthode

Oprateur flche et flche appel -> et ->*

Mthode

Oprateurs de dcalage << et >>

Mthode

Oprateurs new et delete

Mthode

Oprateurs de lecture et criture sur flux << et >>

Fonction

Oprateurs dyadiques genre arithmtique (+, -, / etc.)

Fonction

Tableau 2.1 Description des surcharge doprateurs

2.3 Quels oprateurs ne pas surcharger ?


Pour commencer, il faut savoir que certains oprateurs ne peuvent pas tre
redfinis car le langage vous linterdit ! Il sagit de
.

Slection dun membre

Le C++ pour les Pros

15

.*

Appel dun pointeur de mthode membre

::

Slection de porte

:?

Oprateur ternaire
Tableau 2.2 Oprateurs du C++ ne pouvant tre surchargs

Vous noterez au passage, que si la slection dun membre dun objet statique
par loprateur . ne peut tre surcharge, en revanche la slection dun membre dun
objet dynamique par loprateur flche -> peut, elle, ltre ! Cela permettra, par
exemple de crer des classes de pointeurs intelligents.
En outre, mme si vous en avez la possibilit, il vaut mieux ne pas surcharger
les oprateurs suivants :
,

Evaluation squentielle dexpressions

Non logique

|| &&

Ou et Et logiques

Tableau 2.3 Oprateurs C++ quil vaut mieux ne pas surcharger


En effet, il faut respecter une rgle fondamentale :
Les oprateurs surchargs doivent conserver leur rle naturel sous peine de
perturber considrablement leur utilisateur
En effet, le but des oprateurs surchargs est de faciliter lcriture de code.
Autrement dit, lutilisateur de vos oprateurs ne doit pas avoir se poser de question
lorsquil va les utiliser, et ce nest pas en modifiant le comportement des plus
lmentaires que vous atteindrez ce but.
Par exemple, loprateur dvaluation squentielle est trs bien comme il est. Et,
moins de vouloir rgler son compte un ennemi, vous navez aucun intrt le
surcharger. La mme rgle sapplique directement au non logique.
Pour ce qui concerne les oprateurs logiques dyadiques, il existe une autre rgle
encore plus subtile. En effet, en C++ comme en C, les valuations sont dites court
circuit. En effet, si le premier membre dun et est faux, il est inutile dvaluer le
reste de lexpression, le rsultat sera faux quoiquil arrive. Rciproquement,
lvaluation dune expression ou sarrte ds que lon rencontre un membre vrai.
Considrez maintenant la redfinition des oprateurs || et &&. Leurs cas tant
symtriques, nous nallons tudier que le ou . Il scrira ainsi :
bool operator||(const bool a, const bool b)

Le C++ pour les Pros

16

(Notez au passage lutilisation des valeurs directes des boolens et non pas de
rfrences : sizeof(bool) < sizeof(@) !)
et souvenez vous ! les arguments dune fonction sont toujours valus
in extenso avant lappel de la fonction ! ainsi, il ne peut plus y avoir de court circuit et
une expression telle que, par exemple :
if (p && *p>0)

qui pouvait tre crite sans danger en C++ ne peut plus ltre du fait de votre
surcharge (la drfrence du pointeur sera faite mme si ce dernier est nul, premire
clause du &&).

2.4 Quels paramtres doivent prendre les oprateurs


Typiquement, les oprateurs doivent toujours
susceptibles dtre modifis par rfrence.

prendre

les

paramtres

Les paramtres qui ne doivent pas tre modifis sont soit :

Passs par rfrence constante


Passs par valeur sil sagit de scalaires dont la taille est infrieure celle
dun pointeur, en aucun cas, on ne passera un objet par valeur

2.4.1 Prfrer les rfrences constantes aux objets


Il faut toujours prfrer une rfrence constante un objet pass par valeur.
Comme vous ntes pas obligs de me croire sur parole, je vais tacher de vous le
dmontrer en identifiant les points positifs et ngatifs de la technique
2.4.1.1 Point ngatif :
Bien entendu, si la fonction ou la mthode prenant une rfrence (ft-ce telle
constante) est amie de la classe de lobjet pass par rfrence constante, vous ne
pourrez empcher un programmeur sale de modifier directement les attributs de ce
dernier. Le seul moyen dempcher de tels effets de bord consiste limiter au
maximum lutilisation des amis.
2.4.1.2 Points positifs :
Efficacit de lappel : rappelons en effet que lorsquun objet est pass par
valeur, il y a appel du constructeur de recopie pour gnrer une copie, qui
elle est utilise par la mthode / fonction appele. Hors, une rfrence est
fondamentalement un pointeur, et il sera plus rapide dempiler un simple
pointeur que dappeler un constructeur de recopie aussi optimis soit il.
Support du polymorphisme : supposons que vous criez la classe Evaluable,
dote de la mthode evaluation, comme dans lextrait de code suivant (les
partisans des vritables langages objets argueront, raison, que cette
classe aurait du tre une interface !)
Le C++ pour les Pros

17

class Evaluable
{
protected:
int (*f)(void);
// dtails omis
public:
// Evaluation simple : utilisation de la fonction objectif
virtual int evaluation(void) const
{
return (*f)();
}
};

Programme 2.1 la classe Evaluable


La classe Evaluable est dispose tre drive et sa mthode Evaluation
redfinie afin de modifier le format de lvaluation. Le but ultime est
dobtenir un comportement polymorphique sur la mthode Evaluation. Vous
construisez maintenant un oprateur de comparaison de la manire
suivante :
bool operator<(Evaluable gauche, Evaluable droite)
{
return (gauche.evaluation() < droite.evaluation());
}

Programme 2.2 un oprateur de comparaison gnrique ?


Vous esprez ainsi pouvoir lutiliser avec toutes les classes drives de
Evaluable par polymorphisme sur la mthode Evaluation.
Par exemple, soit la classe Penalites, dfinie de la manire suivante :
class Penalites : public Evaluable
{
private:
int facteurPenalites;
int violation;
public:
// Evaluation avec penalites quadratiques !
virtual int evaluation(void) const
{
return (*f)() + facteurPenalites*violation*violation;
};
};

Programme 2.3 la classe Penalites


Soit maintenant le code suivant :
int main(int, char**)
{
Evaluable e1,e2;
Penalites p1,p2;
if (e1 < e2) // utilise Evaluable::evaluation
// code omis

Le C++ pour les Pros

18

if (p1 < p2) // utilise egalement Evaluable::evaluation !!!!


// code omis
}

Programme 2.4 Utilisation de loprateur <


Lon sattendrait pour le moins ce que la comparaison entre p1 et p2,
objets
de
la
classe
utilise
la
mthode
Penalites
Penalites::evaluation. Hors, il nen est rien ! du fait du passage par
valeur, les objets p1 et p2 sont dgrads de Penalites vers Evaluable et
le polymorphisme est perdu. Afin de retrouver le comportement souhait, il
suffit de passer les arguments par rfrence :
bool operator<(const Evaluable &gauche, const Evaluable &droite)
{
return (gauche.evaluation() < droite.evaluation());
}

Programme 2.5 un oprateur de comparaison gnrique !


Grce aux rfrences, lon retrouve bien le comportement polymorphique
tant attendu et le code du Programme 2.4 fonctionne comme prvu !
2.4.1.3 Conclusion
Au regard de ces explications, il est clair quil vaut toujours mieux utiliser une
rfrence constant quun objet pass par valeur. Aussi, usez et abusez des paramtres
passs par rfrences constantes !

2.4.2 Pourquoi ne pas toujours renvoyer une rfrence


Aprs avoir trait le cas des paramtres positionnels des mthodes, tudions
dornavant le cas dun paramtre particulirement spcial : le retour de
mthode / fonction.
Nous y apprendrons que sil est quasi tout le temps recommand dutiliser des
rfrences constantes pour les paramtres, il faut le plus souvent retourner un objet.

2.5 Que doivent renvoyer les oprateurs ?


Il faut sparer deux grands cas dans les types de retour :
Les types de retour scalaires ne posent aucun problme particulier : le
rsultat est log sur la pile ou dans un registre du processeur avant dtre
rcupr par lappelant.
Les types de retour objet sont plus complexes grer car lon se retrouve
souvent face un dilemme : doit on retourner un objet ou une rfrence ?
A la question :
Quand peut on (et doit on !) absolument renvoyer une rfrence ?
Le C++ pour les Pros

19

Nous pourrons rpondre :


Il est ncessaire de renvoyer une rfrence lorsque loprateur (ou une
mthode quelconque) doit renvoyer lobjet cible (i.e. *this) de manire ce que celuici soit modifiable. Cest typiquement le cas des oprateurs dont le rsultat lvalue, cest
dire une expression que lon peut positionner gauche dun signe daffectation. Dans
cette catgorie, on retrouve notamment tous les oprateurs daffectation et assimils
ainsi que loprateur de princrmentation.
Dans tous les autres cas, il vaudra mieux retourner un objet.
Afin dtayer notre propos, considrons une classe modlisant les nombres
rationnels et dclare comme suit :
class Rationnel
{
private:
int numerateur;
int denominateur;
public:
Rationnel (int numVal, int denVal) :
numerateur(numVal), denominateur(denVal)
{
}
// dclarations omises
int num() const
{
return numerateur;
}
int deno() const
{
return denominateur;
}
};

Programme 2.6 Dclaration dune classe Rationnel


Ecrivons dornavant un oprateur de multiplication sur les rationnels, la
premire ide de codage est la suivante :
Rationnel operator*(const Rationnel &gauche, const Rationnel &droite)
{
return Rationnel(gauche.num()*droite.num(),
gauche.deno()*droite.deno());
}

Programme 2.7 Oprateur de multiplication sur les rationnels


Ecrit tel quel, cet oprateur renvoie un objet Rationnel. Examinons
maintenant la ligne de code suivante (qui suppose lexistence pralable de deux objets
Rationnel a et b) :
Rationnel c(0,0);
// code omis
c=a*b;

Le C++ pour les Pros

20

Typiquement, voici le comportement de cette instruction telle que spcifi dans


la documentation officielle du C++.
1. Appel de la fonction operator* avec passage par rfrence constantes des objets a et
b.
2. Construction de lobjet temporaire rsultat sur la pile dans lespace automatique de
operator* par le constructeur Rationnel(int,int).
3. Instruction return. Lobjet temporaire cr prcdemment (par le constructeur
Rationnel(int,int)) est recopi dans un espace temporaire, sur la pile, mais en
dehors de lespace automatique de operator* afin que ce nouvel objet ne soit pas
dtruit lors du retour de la fonction. Nous appellerons cet objet retour_temp.
Notez que dans ce cas, o la classe rationnelle na que des attributs
scalaires, le constructeur par recopie automatiquement fourni par le
compilateur (vous savez, celui qui fonctionne par recopie bit bit
optimise ) est parfait.
4. Sortie de la fonction operator*. Lobjet temporaire Rationnel(int,int)
construit par est dtruit au contraire de lobjet retour_temp plac en dehors de la
zone automatique de operator*.
5. Loprateur daffectation sur la classe Rationnel (une fois encore, loprateur
automatiquement cr par le compilateur est suffisant) est appel sur lobjet c avec
pour paramtre retour_temp.
6. Lobjet retour_temp devenu inutile est dtruit.
Ce comportement peut paratre lourd mais il savre particulirement sr. En
effet, il vite, par exemple, que lobjet temporaire cr par le constructeur
Rationnel(int,int) ne soit dtruit avant son utilisation lextrieur de la
fonction.
En outre, certains compilateurs sont capables de raliser de loptimisation de
return . Plutt que dutiliser un constructeur par recopie pour fabriquer lobjet
retour_temp, en dehors de la zone de pile dtruite au moment du retour, le
compilateur cr directement lobjet Rationnel(int,int) dans cet emplacement
privilgi spargnant ainsi un appel au constructeur de recopie.
Toutefois, le programmeur soucieux de performance est en mesure de se poser
la question :
Si les rfrences sont si efficaces pour passer les paramtres, pourquoi
nutiliserais-je pas une rfrence comme type de retour ?
Loprateur de multiplication pourrait alors scrire :
Rationnel &operator*(const Rationnel &gauche, const Rationnel &droite)
{
return Rationnel(gauche.num()*droite.num(),

Le C++ pour les Pros

21

gauche.deno()*droite.deno());
}

Programme 2.8 oprateur de multiplication 2me forme (errone)


Vous remarquez que lon renvoie une rfrence sur un objet automatique, ce qui
nest pas sans rappeler une erreur frquente des programmeurs en C qui renvoient
ladresse dune variable automatique : les consquences sont exactement les mmes :
laffectation suivante va se faire sur un objet qui nexiste plus.
Qu cela ne tienne me direz vous, je nai qu construire mon objet sur le tas, on
obtient alors :
Rationnel &operator*(const Rationnel &gauche, const Rationnel &droite)
{
return *(new Rationnel(gauche.num()*droite.num(),
gauche.deno()*droite.deno()));
}

Programme 2.9 oprateur de multiplication 3me forme (errone)


Le problme prcdent est en effet limin : la rfrence renvoye nest plus sur
un objet temporaire. Toutefois, qui va se charger dappeler delete sur lobjet cr
dans loprateur pour rendre la mmoire ?
Dans le cas o lon ralise un opration simple, ce nest pas trop grave, on
pourra toujours avoir :
c=a*b;
delete &c;

Cest trs contraignant (et source rapide derreurs ) mais a marche ! En


revanche considrez lexpression :
d=a*b*c;

Celle-ci se rcrit en fait :


d=operator*(a,operator*(b*c));

Notez quil a construction dun objet muet sans identificateur que vous navez
absolument pas la possibilit de dtruire. Il y a donc une relle perte de mmoire.
Alors, quelle solution choisir ? il vaut mieux jouer la scurit et adopter la
premire solution : celle qui renvoie des objets. Il est vrai que lon risque dutiliser un
oprateur de recopie supplmentaire mais il vaut mieux perdre un peu de temps que
risquer de dilapider la mmoire ou travailler sur des objets en cours de destruction, ne
croyez vous pas ?

2.6 Le cas particulier des oprateurs dincrmentation


Nous discutons ici des oprateurs dincrmentation au sens large, tout ce
discours sappliquant galement aux oprateurs de dcrmentation.
Le C++ pour les Pros

22

Les oprateurs dincrmentation sont particuliers bien des aspects. En


particulier ils peuvent tre utiliss en pr incrmentation (++i) ou en
post incrmentation (i++). La premire difficult va donc tre de sparer ces deux
formes. Le C++ emploie pour cela une astuce assez sale. En effet, loprateur de
pr incrmentation est dclar sans paramtre, alors que loprateur de
post incrmentation prend un paramtre int muet. En outre, reste le problme de
leur type de retour.
Une fois de plus, nous nous basons sur le comportement de ces oprateurs sur le
type canonique int.
Par exemple, lexpression
++i=5;

est tout fait valide, au contraire de :


i++=5;

En effet, dans le second cas, il y a conflit dans lordre des oprations entre
laffectation et la post incrmentation. Pour rsumer, il nous suffit de dire que ++X est
une lvalue, au contraire de X++. Ce qui nous indique clairement que loprateur de
pr incrmentation doit renvoyer une rfrence sur *this alors que loprateur de
post incrmentation doit renvoyer un objet constant.
Finalement, nous obtenons les dclarations suivantes pour une classe X
quelconque :
class X
{
// code omis
public:
X & operator++(void); // pr incrmentation
const X operator++(int); // post incrmentation
};

Programme 2.10 forme canonique des oprateurs de pr et post


incrmentation

Dautre part, reprenons un manuel (un bon, de prfrence ) de gnie logiciel.


Vous y trouverez probablement, sous quelque forme que ce soit, ladage suivant :
Toute duplication de code est proscrire
Nous allons lappliquer la rdaction de nos oprateurs dincrmentation. En
effet, il est vident de remarquer que loprateur de post incrmentation peut toujours
scrire de la manire suivante une fois loprateur de pr incrmentation crit :
const X X::operator++(int)
{
const X temp=const_cast<const X>(*this); // copie de lobjet dans
// son tat actuel
operator++(); // pre incrementation

Le C++ pour les Pros

23

return temp;
}

Programme 2.11 criture de loprateur de post incrmentation


En effet, il suffit dcrire une fois le code dincrmentation et ce dans loprateur
de pr incrmentation, lequel renvoie toujours *this. Le code de loprateur de
post incrmentation est alors trivial. On assiste alors au gigantesque gchis ralis
par loprateur de post incrmentation :
1. Construction dun objet temporaire par le constructeur de recopie, lequel
sera renvoy par la mthode
2. Appel loprateur de pr incrmentation pour raliser effectivement
lincrmentation
3. Renvoi de lobjet cr prcdemment, avec, si le compilateur ne fait pas
doptimisation de retour, appel au constructeur de recopie.
On voit bien que la post incrmentation est une opration beaucoup plus
onreuse que la pr incrmentation. Aussi, ds lors que lon pourra faire soit lune, soit
lautre, il faudra toujours utiliser la pr incrmentation.

2.7 Le cas particulier des oprateurs new et delete


Les oprateurs new et delete sont particulirement spciaux car ils grent
lallocation et la dsallocation des objets. Avant de se poser la question de leur
surcharge, nous allons dabord nous intresser leur fonctionnement dtaill.

2.7.1 Bien comprendre le fonctionnement de new et delete


Pour plagier Scott Meyers ( qui ce poly doit beaucoup !, merci Scott pour tout le
boulot ralis sur le C++), il faut bien diffrencier deux notions fondamentales.

Loprateur new
operator new(size_t taille)

Lorsque vous fates linstruction :


X *x=new X()

Voici ce qui se passe :


1. Loprateur new appelle X::operator new(sizeof(X)) pour allouer la
mmoire ncessaire un objet de type X
2. Loprateur new appelle ensuite le constructeur spcifi sur le bloc de
mmoire renvoy par X::operator new.

Le C++ pour les Pros

24

Ce comportement est immuable est vous ne pouvez absolument pas le changer


car il est codifi dans la norme du C++. Ce que vous pouvez modifier, cest le
comportement de operator new. En effet, celui-ci agit comme malloc.
Point particulirement intressant, il existe un operator new par classe,
lequel est affect par dfaut ::operator new, oprateur dallocation de mmoire
global. Vous pouvez galement surcharger loprateur dallocation de mmoire global,
mais vous avez intrt savoir exactement ce que vous fabriquez car cet oprateur est
utilis pour allouer la mmoire de lapplication elle mme. Aussi, toute boulette se
soldera immdiatement par une erreur systme qui pourrait se payer au prix fort.
Le mme raisonnement sapplique delete. Il faut alors distinguer :

Loprateur delete, qui appelle le destructeur puis operator delete.


operator delete qui rend la mmoire au systme

2.7.2 Une version spciale de new : operator new de placement


Pour chaque classe, il existe une version extrmement simple de operator
new. Cest ce que lon appelle operator new et qui rpond la dfinition suivante :
X *X::operator new(void *a)
{
return a;
}

Programme 2.12 Loperator new de placement


Comme vous pouvez le constater, cet oprateur est extrmement simple : il ne
fait que renvoyer la valeur qui lui est passe en paramtre. Il est nanmoins trs utile
lorsque lon a rserv lavance de la mmoire et que lon souhaite appliquer les
constructeurs dans un second temps.
Considrez par exemple le programme suivant qui alloue des objets dans un
tableau de caractres prvu lavance. Cette technique peut tre utile dans un
environnement o les appels new sont indsirables (surcot important, faible
fiabilit, etc.)

2.7.3 Exercice : un exemple simpliste de surcharge de new et


delete
Concevez un exemple dallocation et de dsallocation de mmoire dans un grand
tableau allou en tant que tableau de char. Il nest pas demand de grer la
fragmentation. En fait, il faut surcharger new et delete de manire leur confrer
par dfaut le comportement de lexemple prcdent.

Le C++ pour les Pros

25

3. La gestion des exceptions


Les exceptions ont t rajoutes la norme du C++ afin de faciliter la mise en
uvre de code robuste.

3.1 Schma gnral dinterception des exceptions


Le schma gnral sappuie sur la description de blocs protgs suivis de code
de prise en charge des exceptions appels traite exception ou gestionnaire dexception.

Bloc de code
protg (unique)

Gestionnaires
d'exceptions
(multiples)

{
{

try
{
// Code susceptible de lever une exception
}
catch (TypeException &identificateur)
{
// Code de gestion d'une exception
}

}
}

Figure 3.1 Schma gnral de gestion des exceptions en C++


Il est important de faire dores et dj quelques commentaires importants :

Un gestionnaire utilise toujours une rfrence sur un type dexception.


Lorsque lexception correspondante est leve, il se sert de lidentificateur
pour accder aux donnes membres ou aux mthodes de lexception.
Il existe un gestionnaire universel qui est introduit par la squence catch
(). Il convient toutefois de ne lutiliser que dans des circonstances limites
cest dire lorsque vous matrisez lensemble des exceptions qui risques
dtre leves par le bloc de code incrimin. En effet, ce gestionnaire
intercepte nimporte quelle exception.
Hors certains environnements de programmation encapsulent toute la
fonction main lintrieur dun bloc try et fournissent des gestionnaires
spcifiques certaines exceptions. Si vous utilisez catch (...)
lintrieur dun tel bloc, vous risquez de court-circuiter les mcanismes de
gestion de telles interruptions avec des consquences parfois dramatiques.
Aussi, il vaut mieux, comme nous le verrons plus loin, utilisez des
hirarchies bien penses de vos exceptions.

3.2 La logique de traitement des exceptions


Lorsquune exception est leve dans votre programme, que se passe-t-il ?
Le C++ pour les Pros

26

Si linstruction en faute nest incluse dans un bloc try, il y appel immdiat


de la fonction terminate.
Comme son nom lindique, cette dernire provoque la terminaison
immdiate du programme par lappel la fonction abort (lquivalent dun
SIG_KILL sous Unix) sans aucun appel de destructeur ou de code de
nettoyage ; ce qui savre la plupart du temps dsastreux.
Notons galement que terminate est appele par dfaut par la fonction
unexpected.
Pour terminer par une note optimiste, il est possible de modifier le
fonctionnement de terminate en utilisant la fonction set_terminate.

En revanche, si linstruction incrimine est incluse dans un bloc try, le


programme saute directement vers les gestionnaires dexception quil
examine squentiellement dans lordre du code source.

Si lun des gestionnaires correspond au type de lexception, il est


excut, et, sil ne provoque pas lui mme dinterruption ou ne met fin
lexcution du programme, lexcution se poursuit la premire ligne de
code suivant lensemble des gestionnaires dinterruption. En aucun cas
il nest possible de poursuivre lexcution la suite de la ligne de code
fautive.
Si aucun gestionnaire ne correspond au type de lexception, celle-ci est
propage au niveau suprieur de traitement des exceptions (ce qui
signifie le bloc try de niveau suprieur en cas de blocs try imbriqus)
jusqu arriver au programme principal qui lui appellera terminate.

3.3 Relancer une exception


Dans certains cas, il est impossible de traiter localement une exception.
Toutefois, le cas nest pas jug suffisamment grave pour justifier larrt du
programme. Le comportement standard consiste alors relever lexception incrimine
(aprs, par exemple, un avertissement lutilisateur) dans lespoir quun bloc de
niveau suprieur saura la traiter convenablement. Ceci se fait en utilisant
linstruction throw isole comme le montre lexemple suivant :
try
{
}
catch (TypeException &e)
{
// code de traitement local
throw; // Relance lexception
}

Programme 3.1 Relancer une exception

Le C++ pour les Pros

27

3.4 Utiliser ses propres exceptions


3.4.1 Dclarer ses exceptions
Selon la norme, les exceptions peuvent tre de nimporte quel type (y compris
un simple entier). Toutefois, il est pratique de les dfinir en tant que classes. Afin de
ne pas polluer lespace global de nommage, il est possible dencapsuler les exceptions
lintrieur des classes qui les utilisent.
En outre, et sans vouloir empiter sur le cours concernant la librairie standard
du C++ (STL), il me semble adquat de driver toute exception du type
std::exception propos en standard par tous les compilateurs modernes .

En effet, cette classe propose une mthode nomme what qui peut savrer des
plus prcieuses. Examinons le code suivant :
class exception
{
// code omis
virtual const char * what () const throw ()
{
return << pointeur vers une chane quelconque >> ;
}
};

Programme 3.2 Dclaration de la classe std::exception


De toute vidence, la mthode what est destine tre surcharge par chaque
classe drive de std::exception afin de fournir lutilisateur un message le
renseignant sur la nature de lerreur leve.
En outre, cela permet de rcuprer votre exception avec une clause :
catch (std::exception &e)

plutt quen utilisant linfme :


catch()

Considrons par exemple une classe nomme TasMax et implmentant les


fonctionnalits dun Tas max (on sen doutait un peu ). Les erreurs peuvent tre
multiples :

Erreurs dallocation
Erreurs daccs aux lments, lesquelles seront principalement :

Tentative de dpilage dun lment sur un tas vide


Tentative dempilage dun lment sur un tas dj plein

Le C++ pour les Pros

28

Afin de se simplifier lexistence, il est agrable dutiliser une classe mre de


toutes les classes dexception que lon peut lancer. La hirarchie devient alors :
std::exception

TasMax::Erreur

TasMax::ErreurAcces

TasMax::ErreurTasVide

TasMax::ErreurAllocation

TasMax::ErreurTasPlein

Figure 3.2 Hirarchie dexceptions du TasMax


Le code associ peut tre :
class TasMax
{
public:
class Erreur : public std::exception
{
const char *what(void) const throw()
{
return "Exception generale sur un tas max";
}
};
class ErreurAcces : public TasMax::Erreur
{
const char *what(void) const throw()
{
return "Exception d'acces sur un tas max";
}
};
class ErreurTasVide : public TasMax::ErreurAcces
{
const char *what(void) const throw()
{
return "Exception de depilement d'un tas max vide";
}
};
class ErreurTasPlein : public TasMax::ErreurAcces

Le C++ pour les Pros

29

{
const char *what(void) const throw()
{
return "Exception d'empilement sur un tas max plein";
}
};
class ErreurAllocation : public TasMax::Erreur
{
const char *what(void) const throw()
{
return "Impossible d'allouer de la memoire dans le tas max";
}
};
// reste du code omis
};

Programme 3.3 Exemple dune hirarchie dexceptions


Vous remarquerez au passage que ce code utilise des noms compltement
qualifis pour driver les classes imbriques. Ceci nest pas requis par la norme du
C++ mais permet parfois de lever certaines ambiguts, en particulier lorsque les
identificateurs sont trs simples.

3.4.2 Utiliser ses exceptions


Il convient de bien utiliser les gestionnaires dexception, en particulier
linstruction :
catch (TypeException &e)

intercepte non seulement les exceptions de type TypeException, mais


galement de toutes les classes drives de TypeException. Aussi, il est important de
toujours spcifier les gestionnaires des classes drives avant ceux des classes
gnrales. Par exemple, si vous voulez intercepter toutes les erreurs drivant de
TasMax::Erreur mais plus spcifiquement TasMax::ErreurTasVide vous
cririez :
try
{
// bloc de code protg
}
catch (TasMax::ErreurTasVide &e)
{
// code spcifique cette exception
}
catch (TasMax::Erreur &e)
{
cerr << Le Tas Max a provoqu lexception : << e.what() << endl;
cerr << Fin du programme << endl;
exit(1);
}
catch (std::exception &e)
{
cerr << Exception inconnue : << e.what() << endl;
cerr << Fin du programme << endl;

Le C++ pour les Pros

30

exit(1);
}

Programme 3.4 Exemple de gestion des erreurs en provenance du Tas Max


Vous noterez au passage lutilisation de lidentificateur e permettant dappeler
les mthodes des exceptions.
Si vous aviez plac le gestionnaire de TasMax::Erreur avant celui de
TasMax::ErreurTasVide, ce dernier naurait jamais t invoqu car
TasMax::Erreur et une super classe de TasMax::ErreurTasVide et aurait donc
intercept lexception.

3.5 Les spcificateurs dexception


Lecteur attentif, vous naurez pas manqu de vous interroger sur la
signification de la clause throw() la fin du prototype de la mthode what de la
classe std::exception (voir Programme 3.2).
Cest ce que lon appelle un spcificateur dexception et renseigne lutilisateur
sur le type des exceptions que peut renvoyer une mthode. Ceci parat trs attractif,
on sait immdiatement quel type dexceptions lon doit sattendre au moment de les
intercepter, et, le compilateur vous empche de lancer une exception non prvue par
votre spcificateur. Par exemple, le code suivant est interdit :
void TasMax::depiler(void) throw (TasMax::ErreurTasVide)
{

throw TasMax::Erreur(); // exception non prvue dans la spcification


}

Programme 3.5 Tentative de lancement dune exception non prvue


Mais, car il y a un mais, le spcificateur dexception interdit aux autres
mthodes ou fonctions appeles dinvoquer des exceptions non prvues. Hors, ce point
est difficile vrifier lors de la compilation. Plus grave, lexcution, si une exception
non prvue par le spcificateur est lance, la norme prvoit que la fonction
unexpected soit invoque, laquelle, appelle immdiatement terminate avec les
consquences que nous avons dj vues prcdemment. Il est toutefois possible de
modifier ce comportement en modifiant unexpected grce la fonction
set_unexpected.
Aussi, les spcificateurs dexception doivent ils tre rservs au code que vous
matrisez totalement et plus spcifiquement aux mthodes pour lesquelles vous tes
en mesure de prvoir le droulement complet. Par exemple, dans le cas du Tas Max,
vous pourriez crire :
TasMax(int taille=10) throw (TasMax::ErreurAllocation);
void empiler(int valeur) throw (TasMax::ErreurTasPlein);
int meilleurElement(void) throw (TasMax::ErreurTasVide);
void depiler(void) throw (TasMax::ErreurTasVide);

Programme 3.6 Spcificateurs dexceptions pour les mthodes du TasMax

Le C++ pour les Pros

31

4. Les transtypages en C++


Au passage du C au C++, les oprateurs de transtypage (ou conversion de type)
ont subi un vritable lifting, et pas toujours pour le bien du programmeur !
En effet, si la nouvelle syntaxe de loprateur traditionnel et les nouveaux
oprateurs de transtypage explicites sont une vraie bndiction, les oprateurs de
conversion implicites sont dmoniaques !
Examinons une par une ces nouvelles fonctionnalits

4.1 Nouvelle syntaxe de loprateur traditionnel


Mettez vite Procol Harum sur la platine 33 tours de vos parents, coiffez vous
dune perruque hippie, fates passez un Censur dans lassistance et souvenez vous
de loprateur de conversion (cast) du langage C que nous aimons tous tellement. Sa
syntaxe est la suivante :
(nouveau type)(expression transtyper)
o les parenthses autour de lexpression transtyper peuvent tre omises si
celle-ci est trs simple et si vous tre pris de pulsions suicidaires. La nouvelle syntaxe
est la suivante :
type(expression transtyper)
Cest beaucoup plus simple et en outre, lavantage de vous obliger utiliser
typedef pour les types pointeur.
En effet, considrez le transtypage :
Y *y;
X *x=(X *)y

Pour lcrire avec la nouvelle syntaxe, il vous vaut passer par un typedef :
typedef X* PX;
Y *y;
X *x=PX(y)

Bien que simplificatrice, la nouvelle syntaxe ntait pas satisfaisante bien des
points de vue car elle ne fait que simplifier lcriture des transtypages et ne corrige
aucun des dfauts de loprateur traditionnel (par ordre croissant de svrit) :

Elle est dure retrouver dans un code source, car ce nest ni plus ni moins
quun nom de type et une paire de parenthses.
Elle permet de mixer dangereusement les objets constants et non constants
Elle permet de raliser nimporte quel type de promotion, ou transtypage de
pointeur descendant ou encore downcast.

Le C++ pour les Pros

32

Rappelons rapidement ce quest un downcast. Il sagit dune conversion dun


pointeur sur un objet dune classe gnrale vers un objet dune classe spcialise. Si
lopration est lgitime, cela ne posera pas de problme spcifique. En revanche, si
lopration est illgitime, vous essayerez probablement daccder des informations
inexistantes avec tous les problmes des mmoire consquents.
Les nouveaux oprateurs vont permettre de rsoudre ces problmes.

4.2 Nouveaux oprateurs de transtypage


4.2.1 Pourquoi ?
Ces nouveaux oprateurs sont destins rsoudre les problmes soulevs par
loprateur traditionnel hrit du langage C. Le tableau suivant indique le nom des
nouveaux oprateurs ainsi que leur principale fonctionnalit. Notez bien que leur
syntaxe commune permet de rsoudre le problme de la recherche des transtypages
dans le code source
static_cast

Oprateur de transtypage tout faire. Ne permet pas de


supprimer le caractre const ou volatile.

const_cast

Oprateur spcialis et limit au traitement des caractres


const et volatile

dynamic_cast

Oprateur spcialis et limit au traitement des downcast.

reinterpret_cast

Oprateur spcialis dans le traitement des conversions de


pointeurs peu portables. Ne sera pas abord ici

4.2.2 Syntaxe
La syntaxe, si elle est un peu lourde, a lavantage de ne pas tre ambigu :
op_cast<expression type>(expression transtyper)
O, vous laurez compris, op prend lune des valeurs (static, const, dynamic
ou reinterpret).
Cette syntaxe a lavantage de bien sparer le type que lon veut obtenir, qui est
plac entre signes < et >, de lexpression transtyper, place, elle, entre parenthses.
Par exemple, pour transformer un double en int, on aurait la syntaxe
suivante :
int
i;
double d;
i=static_cast<int>(d)

Programme 4.1 exemple minimaliste de static_cast

Le C++ pour les Pros

33

Nous allons maintenant tudier, un par un, les trois principaux nouveaux
oprateurs de transtypage, seul reinterpret_cast dont lusage est encore entach
dambigut sera pass sous silence.

4.2.3 Fonctionnalits
4.2.3.1 Loprateur static_cast
Cest loprateur de transtypage tout faire qui remplace dans la plupart des
cas loprateur hrit du C. Toutefois il est limit dans les cas suivants :

Il ne peut convertir un type constant en type non constant


Il ne peut pas effectuer de promotion (downcast)

4.2.3.2 Loprateur const_cast


Il est spcialis dans lajout ou le retrait des modificateurs const et volatile.
En revanche, il ne peut pas changer le type de donnes de lexpression. Ainsi, les
seules modifications quil peut faire sont les suivantes :
X
const X
X
volatile X
const X
volatile X

const X
X
volatile X
X
volatile X
const X

Tableau 4.1 Transtypages ralisables avec const_cast


Le code suivant illustre lintrt de const_cast, le rsultat de la compilation
(avec gcc) est plac en commentaire.
X t1();
const X t2();
X *t11=&t1;
const X *t22=&t2;
t11=&t2; // warning: assignment to `X *' from `const X *' discards const
t11=static_cast<X *>(&t2); // error: static_cast from `const X *' to `X *'
t11=const_cast<X *>(&t2); // Ok !

Programme 4.2 exemple dutilisation de const_cast

Le C++ pour les Pros

34

4.2.3.3 Loprateur dynamic_cast


Les downcast posent un problme particulier car leur vrification nest possible
qu lexcution. Aussi, contrairement static_cast et const_cast qui ne sont que
des informations ladresse du compilateur, dynamic_cast gnre du code de
vrification.
Supposons que vous disposez dun conteneur agrgeant par pointeur diffrents
objets issus dune ligne dobjets graphiques reprsente par la figure suivante :
Afin d'viter de surcharger le schma
les paramtres des mthodes Crer
ont t omis

ObjetGraphique
#NombreObjetsGraphiques : Entier
#Couleur : TypeCouleur
#X : Entier
#Y : Entier
#Epaisseur : Entier
+Crer()
+Dtruire()
+getX() : Entier
+getY() : Entier
+setX(valeur : Entier)
+setY(valeur : Entier)
+DeplacerVers(versX : Entier, versY : Entier)
+Afficher()
+Effacer()

Ligne

Cercle

#Longueur : Entier
#Angle : Rel
+Crer()
+Detruire()
+getLongueur() : Entier
+setLongueur(valeur : Entier)
+Afficher()
+Effacer()

#Rayon : Entier
+Crer()
+Detruire()
+getRayon() : Entier
+setRayon(valeur : Entier)
+Afficher()
+Effacer()

Figure 4.1 La ligne des objets graphiques


Si le conteneur est bas sur le type ObjetGraphique *, nous navons pas la
possibilit de connatre le type exact dun objet issu du conteneur un instant
quelconque.

Le C++ pour les Pros

35

Sortons un objet du conteneur. Dcidons quil sagit dun Cercle et essayons de


lui appliquer la mthode setRayon typique de la classe Cercle. Cette opration
ncessite la promotion du pointeur depuis ObjetGraphique* vers Cercle*. Nous
allons nous assurer de la lgitimit de lopration grce dynamic_cast par le code
suivant :
ObjetGraphique *g= / recupration dun pointeur dans un conteneur
Cercle
*c=dynamic_cast<Cercle *>(g)

Programme 4.3 utilisation typique de dynamic_cast


A lexcution, le programme va sassurer que lopration est lgitime en utilisant
des informations de types codes dans les objets et connues sous le nom de RTTI (Run
Time Type Information). Cette fonctionnalit sera tudie dans les chapitres consacrs
la Librairie Standard du C++.
4.2.3.4 Exercices sur les nouveaux oprateurs de conversion
Enonc : Commenter les oprations de transtypage suivantes :
ObjetGraphique *o;
const Ligne
ligne;
Cercle
cercle;
const Cercle
*pCercle;
Ligne
*pLigne;
1) pCercle = const_cast<const Cercle *>(&cercle);
2) pLigne = dynamic_cast<Ligne *>(pCercle);
3) o = static_cast <ObjetGraphique *>(&ligne);

Solution :
1) Pas de problme : &cercle de type Cercle* devient un pointeur constant
par lutilisation de const_cast
2) Erreur : il nest pas possible dajouter ou de supprimer le caractre const
avec dynamic_cast. Deux solutions sont possibles :
a)

Convertir pCercle en pointeur non constant puis traverser la


hirarchie avec dynamic_cast, soit :
pLigne=dynamic_cast<Ligne *>(const_cast<Cercle *>(pCercle))

b)

Faire le const_cast la fin, cest--dire :


pLigne=cons_cast<Ligne >(dynamic_cast<const Ligne*>(pCercle))

3) Mme problme que prcdemment : static_cast ne permet pas dajouter


ou de retirer le caractre constant dun pointeur. En outre, afin de sassurer
que la conversion est lgitime, il eut mieux valu utiliser dynamic_cast. On
retrouve alors les deux solutions :
o=const_cast<ObjetGraphique*>(dynamic_cast<const ObjetGraphique*>(&ligne));
o=dynamic_cast<ObjetGraphique *>(const_cast<Ligne *>(&ligne)) ;

Le C++ pour les Pros

36

4.3 Les conversions implicites


Une conversion implicite est un transtypage effectu par le compilateur dans
votre dos, lexemple le plus simple concerne les conversions implicites entre types
numriques.
Il y a toutefois deux cas nettement plus graves o le C++ est en mesure
deffectuer des conversions implicites : les oprateurs de conversion et les
constructeurs un seul paramtre.

4.3.1 Les oprateurs de conversion implicites


4.3.1.1 Syntaxe et utilisation des oprateurs de conversion implicites
Les oprateurs de conversion implicites sont des adjonctions relativement
rcentes au C++. Ils permettent de raliser une conversion dun objet vers un autre
type. Ces oprateurs sont ncessairement dfinis en tant que mthodes et leur syntaxe
est la suivante :
operator type_desire(void)
Par exemple si nous reprenons lexemple de la classe Rationnel, nous
pourrions crer un oprateur de conversion vers le type double de la forme suivante :
class Rationnel
{
//
operator double (void)
{
return double(numerateur)/denominateur;
}
};

Programme 4.4 Dfinition dun oprateur de conversion implicite depuis


Rationnel vers le type double
Muni de cet oprateur, il est possible demployer un rationnel dans des
expressions arithmtiques relles sans effort supplmentaire de programmation :
{
double a=0.0;
double c;
Rationnel r(1,2);
c = a + r;
}

Programme 4.5 Utilisation dun oprateur de conversion implicite


La dernire ligne de cet exemple utilise loprateur de conversion implicite de
Rationnel vers double pour convertir r en nombre rel, ainsi loprateur + sur les
double peut sappliquer.

Le C++ pour les Pros

37

Dans le mme ordre ide, la plupart des bibliothques orientes objet (dont
OWL et VCL dInprise) encapsulant lAPI Windows proposent une conversion implicite
de leur classe Fentre vers le type HANDLE de Windows.
Ces conversions sont assurment trs pratiques car elles permettent dviter un
effort de programmation supplmentaire au cours de la programmation. Elles ont
toutefois des cts particulirement dtestables :

Le code obtenu lors de leur utilisation est certes plus compact mais peut
induire en erreur sur le type rel des objets un programmeur charg de sa
maintenance.
Il faut toujours se mfier de ce que fait le compilateur automatiquement car
cela peut occasionner des erreurs pernicieuses comme le montre la section
suivante.

4.3.1.2 Exemple de problme soulev par leur utilisation


Dans lexemple suivant, nous allons voir quune simple faute de frappe peut se
transformer en bug trs dlicat dtecter. Nous reprenons la classe Rationnel munie
de son oprateur de conversion explicite vers les rels et utilisons le code suivant :
Rationnel r1(1,2);
Rationnel r2(1,3);
// Tentative de comparaison des numrateurs
// Le code devrait tre (r1.num() == r2.num())
if (r1.num() == r2) // faute de frappe
{
.. reste du code omis
}

Programme 4.6 Bug insidieux d un oprateur de conversion implicite


Le but recherch tait de comparer r1.num() avec r2.num().
Malheureusement, et par mgarde, vous avez remplac r2.num() par r2. Hors, le
compilateur ne signale pas derreur, le programme ne plante pas mais donne un
rsultat faux.
Voici donc ce qui se droule :
a) Le compilateur dtecte la comparaison de r1.num() qui est un int avec r2,
objet de classe Rationnel. A priori, la comparaison est illicite et devrait
dclencher une erreur de compilation
b) Le compilateur dtecte la possibilit de convertir r2 en double, ce qui
permettrait deffectuer une comparaison tout fait valable entre un double et
en entier
c) Finalement, le compilateur gnre pour vous un appel loprateur de
conversion implicite, effectue une promotion de r2.num() depuis int vers
double, et code une comparaison sur les double.
Le C++ pour les Pros

38

Ce genre de bug non dtect par le compilateur est particulirement pernicieux


dtecter et doit tre vit autant que possible
4.3.1.3 Parade
Il ny a pas de parade a proprement parler. A mon avis, il est toujours
dangereux dutiliser des oprateurs de conversion implicite. Il vaut mieux utiliser une
mthode pour faire ce travail.
A ce sujet, il est intressant de constater que les concepteurs de la STL du C++
utilisent une mthode nomme c_str pour convertir une chane de classe string
vers un char*, cet exemple est mditer.

4.3.2 Les constructeurs avec un seul paramtre


Il est crit dans la norme du C++ que les constructeurs avec un seul paramtre
peuvent tre utiliss la vole pour construire un objet temporaire.
En outre il faut se souvenir quun constructeur avec plusieurs paramtres tous
avec des valeurs par dfaut ou dont seul le premier na pas de valeur par dfaut peut
se transformer immdiatement en oprateur de conversion implicite.
4.3.2.1 Exemple de problme soulev par les constructeurs avec un seul
paramtre
Dans lexemple suivant, la classe Chaine dispose dun constructeur qui prend
un entier en paramtre, soit :
class Chaine
{
public:
Chaine (int param)

// Cre une chaine de param caractres non


// initialiss

};

Supposons galement que la classe Chaine soit munie dun oprateur de


comparaison ==.
Examinons alors le code suivant :
Chaine c1= new Chaine(tklp);
Chaine c2= new Chaine(tkop);
for (int i=0;i<c1.size();i++)
{
if (c1==c2[i]) // Erreur de frappe ! ce devrait tre c1[i]

Programme 4.7 Exemple derreur lie aux constructeurs un seul paramtre


En thorie, lon sattendrait ce que la faute de frappe soit dtecte par le
compilateur. Hors il faut prendre les lments suivants en compte :
Le C++ pour les Pros

39

Le membre de gauche de la comparaison est une Chaine et il existe un


oprateur de comparaison sur la classe Chaine
c2[i] est un caractre qui est donc assimilable un entier.

Il existe une constructeur un paramtre qui construit une Chaine partir


dun entier
Il va donc se produire le scnario suivant :

Cration dun objet Chaine temporaire laide


Chaine::Chaine(int) avec c2[i] comme paramtre

du

constructeur

Application de loprateur == sur c1 et lobjet temporaire


Destruction de lobjet temporaire

Ce qui nest absolument pas le comportement recherch et conduit des erreurs


pernicieuses et dlicates dtecter.
4.3.2.2 Parade
Ici, la parade est trs simple. En effet, il suffit de prfixer le nom du
constructeur par le mot clef explicit. Ainsi, un constructeur ne peut jamais tre
utilis en tant quoprateur de conversion implicite et lexemple ci-dessus aurait
produit une erreur de compilation
Retenez bien le principe : tout constructeur susceptible dtre appel avec un
seul paramtre doit tre dclar explicit.

5. En guise de conclusion
Je voudrais ddicacer ce manuel de survie la promotion 1999 de lISIMA au
sein de laquelle je compte de nombreux amis. Bien plus que de simples tudiants, ils
ont su me donner le got de lenseignement. Je sais ce que je leur dois et leur souhaite
bonne chance dans la vie.
Bon courage tous -Bruno

Le C++ pour les Pros

40

Вам также может понравиться