8. Constantes▲
Le concept de constante(exprimé par le mot-clef const) a été créé pour permettre au programmeur de séparer ce qui change de ce qui ne change pas. Cela assure sécurité et contrôle dans un projet C++.
Depuis son apparition, consta eu différentes raisons d'être. Dans l'intervalle il est passé au C où son sens a été modifié. Tout ceci peut paraître un peu confus au premier abord, et dans ce chapitre vous apprendrez quand, pourquoi et comment utiliser le mot-clef const. A la fin, se trouve une discussion de volatilequi est un proche cousin de const(parce qu'ils concernent tout deux le changement) et a une syntaxe identique.
La première motivation pour constsemble avoir été d'éliminer l'usage de la directive de pré-processeur #definepour la substitution de valeurs. Il a depuis été utilisé pour les pointeurs, les arguments de fonction, les types retournés, les objets de classe et les fonctions membres. Tous ces emplois ont des sens légèrement différents mais conceptuellement compatibles et seront examinés dans différentes sections de ce chapitre.
8-1. Substitution de valeurs▲
En programmant en C, le préprocesseur est utilisé sans restriction pour créer des macros et pour substituer des valeurs. Puisque le préprocesseur fait simplement le remplacement des textes et n'a aucune notion ni service de vérification de type, la substitution de valeur du préprocesseur présente les problèmes subtiles qui peuvent être évités en C++ en utilisant des variables const.
L'utilisation typique du préprocesseur pour substituer des valeurs aux noms en C ressemble à ceci :
#define BUFSIZE 100
BUFSIZEest un nom qui existe seulement pendant le prétraitement, donc il n'occupe pas d'emplacement mémoire et peut être placé dans un fichier d'en-tête pour fournir une valeur unique pour toutes les unités de traduction qui l'utilisent. Il est très important pour l'entretien du code d'utiliser la substitution de valeur au lieu de prétendus « nombres magiques. » Si vous utilisez des nombres magiques dans votre code, non seulement le lecteur n'a aucune idée d'où ces nombres proviennent ou de ce qu'ils représentent, mais si vous décidez de changer une valeur, vous devrez le faire à la main, et vous n'avez aucune façon de savoir si vous n'oubliez pas une de vos valeurs (ou que vous en changez accidentellement une que vous ne devriez pas changer).
La plupart du temps, BUFSIZEse comportera comme une variable ordinaire, mais pas toujours. En outre, il n'y a aucune information sur son type. Cela peut masquer des bogues qu'il est très difficile de trouver. C++ emploie constpour éliminer ces problèmes en déplaçant la substitution de valeur dans le domaine du compilateur. Maintenant vous pouvez dire :
const
int
bufsize =
100
;
Vous pouvez utiliser bufsizepartout où le compilateur doit connaître la valeur pendant la compilation. Le compilateur peut utiliser bufsizepour exécuter le remplacement des constantes, ça signifie que le compilateur ramènera une expression constante complexe à une simple valeur en exécutant les calculs nécessaires lors de la compilation. C'est particulièrement important pour des définitions de tableau :
char
buf[bufsize];
Vous pouvez utiliser constpour tous les types prédéfinis ( char, int, float, et double) et leurs variantes (aussi bien que des objets d'une classe, comme vous le verrez plus tard dans ce chapitre). En raison des bogues subtiles que le préprocesseur pourrait présenter, vous devriez toujours employer constau lieu de la substitution de valeur #define.
8-1-1. Constantes dans les fichiers d'en-tête▲
Pour utiliser constau lieu de #define, vous devez pouvoir placer les définitions constà l'intérieur des fichiers d'en-tête comme vous pouvez le faire avec #define. De cette façon, vous pouvez placer la définition d'un constdans un seul endroit et la distribuer aux unités de traduction en incluant le fichier d'en-tête. Un consten C++ est par défaut à liaison interne ; c'est-à-dire qu'il est visible uniquement dans le fichier où il est défini et ne peut pas être vu par d'autres unités de traduction au moment de l'édition de lien. Vous devez toujours affecter une valeur à un const quand vous le définissez, exceptéquand vous utilisez une déclaration explicite en utilisant extern:
extern
const
int
bufsize;
Normalement, le compilateur de C++ évite de créer un emplacement pour un const, mais garde plutôt la définition dans sa table de symbole. Toutefois quand vous utilisez externavec const, vous forcez l'allocation d'un emplacement de stockage (cela vaut également pour certains autres cas, tels que si vous prennez l'adresse d'un const). L'emplacement doit être affecté parce que externindique "utiliser la liaison externe", ce qui signifie que plusieurs unités de traduction doivent pouvoir se rapporter à l'élément, ce qui lui impose d'avoir un emplacement.
Dans le cas ordinaire, si externn'est pas indiqué dans la définition, aucun emplacement n'est alloué. Quand constest utilisé, il subit simplement le remplacement au moment de la compilation.
L'objectif de ne jamais allouer un emplacement pour un constéchoue également pour les structures complexes. A chaque fois que le compilateur doit allouer un emplacement, le remplacement des constn'a pas lieu (puisque le compilateur n'a aucun moyen pour connaître la valeur de cette variable - si il pouvait le savoir, il n'aurait pas besoin d'allouer l'emplacement).
Puisque le compilateur ne peut pas toujours éviter d'allouer l'emplacement d'un const, les définitions de constdoiventêtre à liaison interne, c'est-à-dire, la liaison uniquement danscette unité de compilation. Sinon, des erreurs d'édition de liens se produiraient avec les consts complexes parce qu'elles entraineraient l'allocation de l'emplacement dans plusieurs fichiers cpp. L'éditeur de liens verrait alors la même définition dans plusieurs fichiers, et se plaindrait. Puisqu'un constest à liaison interne, l'éditeur de liens n'essaye pas de relier ces définitions à travers différentes unités de compilation, et il n'y a aucune collision. Avec les types prédéfinis, qui sont employés dans la majorité de cas impliquant des expressions constantes, le compilateur peut toujours exécuter le remplacement des const.
8-1-2. Consts de sécurité▲
L'utilisation du constn'est pas limitée à remplacer les #definedans des expressions constantes. Si vous initialisez une variable avec une valeur qui est produite au moment de l'exécution et vous savez qu'elle ne changera pas pour la durée de vie de cette variable, c'est une bonne pratique de programmation que de la déclarer constde façon à ce que le compilateur donne un message d'erreur si vous essayez accidentellement de la changer. Voici un exemple :
//: C08:Safecons.cpp
// Usage de const pour la sécurité
#include
<iostream>
using
namespace
std;
const
int
i =
100
; // constante typique
const
int
j =
i +
10
; // valeur issue d'une expression constante
long
address =
(long
)&
j; // force l'allocation
char
buf[j +
10
]; // encore une expression constante
int
main() {
cout <<
"type a character & CR:"
;
const
char
c =
cin.get(); // ne peut pas changer
const
char
c2 =
c +
'a'
;
cout <<
c2;
// ...
}
///
:~
Vous pouvez voir que iest un constau moment de la compilation, mais jest calculé à partir de i. Cependant, parce que iest un const, la valeur calculée pour jvient encore d'une expression constante et est elle-même une constante au moment de la compilation. La ligne suivante exige l'adresse de jet force donc le compilateur à allouer un emplacement pour j. Pourtant ceci n'empêche pas l'utilisation de jdans la détermination de la taille de bufparce que le compilateur sait que jest un constet que la valeur est valide même si un emplacement a été alloué pour stocker cette valeur à un certain endroit du programme.
Dans le main(), vous voyez un genre différent de constavec la variable cparce que la valeur ne peut pas être connue au moment de la compilation. Ceci signifie que l'emplacement est exigé, et le compilateur n'essaye pas de conserver quoi que ce soit dans sa table de symbole (le même comportement qu'en C). L'initialisation doit avoir lieu à l'endroit de la définition et, une fois que l'initialisation a eu lieu, la valeur peut plus être changée. Vous pouvez voir que c2est calculé à partir de cet aussi que la portée fonctionne pour des constcomme pour n'importe quel autre type - encore une autre amélioration par rapport à l'utilisation du #define.
En pratique, si vous pensez qu'une valeur ne devrait pas être changée, vous devez la rendre const. Ceci fournit non seulement l'assurance contre les changements accidentels, mais en plus il permet également au compilateur de produire un code plus efficace par l'élimination de l'emplacement de la variable et la suppression de lectures mémoire.
8-1-3. Aggrégats▲
Il est possible d'utiliser constpour des agrégats, mais vous êtes pratiquement assurés que le compilateur ne sera pas assez sophistiqué pour maintenir un agrégat dans sa table de symbole, ainsi l'emplacement sera créé. Dans ces situations, constsignifie « un emplacement mémoire qui ne peut pas être changé ». Cependant, la valeur ne peut pas être utilisée au moment de la compilation parce que le compilateur ne connait pas forcement la valeur au moment de la compilation. Dans le code suivant, vous pouvez voir les instructions qui sont illégales :
//: C08:Constag.cpp
// Constantes et aggrégats
const
int
i[] =
{
1
, 2
, 3
, 4
}
;
//!
float f[i[3]]; // Illégal
struct
S {
int
i, j; }
;
const
S s[] =
{
{
1
, 2
}
, {
3
, 4
}
}
;
//!
double d[s[1].j]; // Illégal
int
main() {}
///
:~
Dans une définition de tableau, le compilateur doit pouvoir produire du code qui déplace le pointeur de pile selon le tableau. Dans chacune des deux définitions illégales ci-dessus, le compilateur se plaint parce qu'il ne peut pas trouver une expression constante dans la définition du tableau.
8-1-4. différences avec le C▲
Les constantes ont été introduites dans les premières versions du C++ alors que les spécifications du standard du C étaient toujours en cours de finition. Bien que le comité du C ait ensuite décidé d'inclure consten C, d'une façon ou d'une autre cela prit la signification de « une variable ordinaire qui ne peut pas être changée. » En C, un constoccupe toujours un emplacement et son nom est global. Le compilateur C ne peut pas traiter un constcomme une constante au moment de la compilation. En C, si vous dites :
const
int
bufsize =
100
;
char
buf[bufsize];
vous obtiendrez une erreur, bien que cela semble une façon de faire rationnelle. Puisque bufsizeoccupe un emplacement quelque part, le compilateur C ne peut pas connaître la valeur au moment de la compilation. Vous pouvez dire en option :
const
int
bufsize;
En C, mais pas en C++, et le compilateur C l'accepte comme une déclaration indiquant qu'il y a un emplacement alloué ailleurs. Puisque C utilise la liaison externe pour les consts, cela à un sens. C++ utilise la liaison interne pour les consts ainsi si vous voulez accomplir la même chose en C++, vous devez explicitement imposer la liaison externe en utilisant extern:
extern
const
int
bufsize; // Déclaration seule
Cette ligne fonctionne aussi en C.
en C++, un constne crée pas nécessairement un emplacement. En C un constcrée toujours un emplacement. Qu'un emplacement soit réservé ou non pour un consten C++ dépend de la façon dont il est utilisé. En général, si un constest simplement utilisé pour remplacer un nom par une valeur (comme vous le feriez avec #define), alors un emplacement n'a pas besoin d'être créé pour le const. Si aucun emplacement n'est créé (cela dépend de la complexité du type de données et de la sophistication du compilateur), les valeurs peuvent ne pas être intégrées dans le code pour une meilleure efficacité après la vérification de type, pas avant comme pour #define. Si, toutefois, vous prenez l'adresse d'un const(même sans le savoir, en le passant à une fonction qui prend en argument une référence) ou si vous le définissez comme extern, alors l'emplacement est créé pour le const.
En C++, un constqui est en dehors de toutes les fonctions possède une portée de fichier (c.-à-d., qu'il est invisible en dehors du fichier). Cela signifie qu'il est à liaison interne. C'est très différent de toutes autres identifieurs en C++ (et des constdu C !) qui ont la liaison externe. Ainsi, si vous déclarez un constdu même nom dans deux fichiers différents et vous ne prenez pas l'adresse ou ne définissez pas ce nom comme extern, le compilateur C++ idéal n'allouera pas d'emplacement pour le const, mais le remplace simplement dans le code. Puisque le consta impliqué une portée fichier, vous pouvez la mettre dans les fichiers d'en-tête C++ sans conflits au moment de l'édition de liens.
Puisqu'un consten C++ a la liaison interne, vous ne pouvez pas simplement définir un constdans un fichier et le mettre en référence externdans un autre fichier. Pour donner à un constune liaison externe afin qu'il puisse être référencé dans un autre fichier, vous devez explicitement le définir comme extern, comme ceci :
extern
const
int
x =
1
;
Notez qu'en lui donnant une valeur d'initialisation et en la déclarant externe vous forcez la création d'un emplacement pour le const(bien que le compilateur a toujours l'option de faire le remplacement de constante ici). L'initialisation établit ceci comme une définition, pas une déclaration. La déclaration :
extern
const
int
x;
En C++ signifie que la définition existe ailleurs (encore un fois, ce n'est pas nécessairement vrai en C). Vous pouvez maintenant voir pourquoi C++ exige qu'une définition de constpossède son initialisation : l'initialisation distingue une déclaration d'une définition (en C c'est toujours une définition, donc aucune initialisation n'est nécessaire). Avec une déclaration extern const, le compilateur ne peut pas faire le remplacement de constante parce qu'il ne connait pas la valeur.
L'approche du C pour les constn'est pas très utile, et si vous voulez utiliser une valeur nommée à l'intérieur d'une expression constante (une qui doit être évalué au moment de la compilation), le C vous obligepresque à utiliser #definedu préprocesseur.
8-2. Les pointeurs▲
Les pointeurs peuvent être rendus const. Le compilateur s'efforcera toujours d'éviter l'allocation de stockage et remplacera les expressions constantes par la valeur appropriée quand il aura affaire à des pointeurs const, mais ces caractéristiques semblent moins utiles dans ce cas. Plus important, le compilateur vous signalera si vous essayez de modifier un pointeur const, ce qui améliore grandement la sécurité.
Quand vous utilisez constavec les pointeurs, vous avez deux options : constpeut être appliqué à ce qui est pointé, ou bien constpeut concerner l'adresse stockée dans le pointeur lui-même. La syntaxe, ici, paraît un peu confuse au début mais devient commode avec la pratique.
8-2-1. Pointeur vers const▲
L'astuce avec la définition de pointeur, comme avec toute définition compliquée, est de la lire en partant de l'identifiant jusque vers la fin de la définition. Le mot-clef constest lié à l'élément dont il est le "plus proche". Donc, si vous voulez éviter tout changement à l'objet pointé, vous écrivez une définition ainsi :
const
int
*
u;
En partant de l'identifiant, nous lisons “ uest un pointeur, qui pointe vers un constint.” Ici, il n'y a pas besoin d'initialisation car vous dites que upeut pointer vers n'importe quoi (c'est-à-dire qu'il n'est pas const), mais l'élément vers lequel il pointe ne peut pas être changé.
Voici la partie un peu déroutante. Vous pourriez pensez que pour rendre le pointeur lui-même inmodifiable, c'est-à-dire pour éviter toute modification de l'adresse contenue dans u, il suffit de déplacer le constde l'autre côté du intcomme ceci :
int
const
*
v;
Ce n'est pas du tout idiot de penser que ceci devait se lire “ vest un pointeur constvers un int.”. Toutefois, la vraie façon de le lire est “ vest un pointeur ordinaire vers un intqui se trouve être const.”. Donc, le consts'est lié au intà nouveau, et l'effet est le même que dans la définition précédente. Le fait que ces deux définitions soient les mêmes est un peu déroutant ; pour éviter cette confusion au lecteur, vous devriez probablement n'utiliser que la première forme.
8-2-2. Pointeur const▲
Pour rendre le pointeur lui-même const, vous devez placer le mot-clef constà droite de l'étoile *, comme ceci :
int
d =
1
;
int
*
const
w =
&
amp;d;
A présenton lit : “ west un pointeur de type const, qui pointe vers un int.”. Comme le pointeur lui-même est à présent le const, le compilateur impose qu'il lui soit assignée une valeur initiale qui restera inchangée durant toute la vie de ce pointeur. Il est possible, par contre, de modifier ce vers quoi pointe cette valeur en disant :
*
w =
2
;
Vous pouvez également créer un pointeur constvers un objet consten utilisant une de ces deux syntaxes licites :
int
d =
1
;
const
int
*
const
x =
&
d; // (1)
int
const
*
const
x2 =
&
d; // (2)
A présent, ni le pointeur ni l'objet ne peuvent être modifiés.
Certaines personnes affirment que la deuxième forme est plus cohérente parce que le constest toujours placé à droite de ce qu'il modifie. Vous n'avez qu'à décider ce qui est le plus clair pour votre manière de coder.
Voici les lignes ci-dessus dans un fichier compilable :
//: C08:ConstPointers.cpp
const
int
*
u;
int
const
*
v;
int
d =
1
;
int
*
const
w =
&
d;
const
int
*
const
x =
&
d; // (1)
int
const
*
const
x2 =
&
d; // (2)
int
main() {}
///
:~
Formatage
Ce livre insiste sur le fait de ne mettre qu'une définition de pointeur par ligne, et d'initialiser chaque pointeur au point de définition lorsque c'est possible. A cause de cela, le style de formatage qui consiste à “attacher” le ‘ *' au type de donnée est possible :
int
*
u =
&
amp;i;
comme siint*était un type en soi. Ceci rend le code plus facile à comprendre, mais malheureusement ce n'est pas comme cela que les choses fonctionnent vraiment. Le ‘ *' est en fait attaché à l'identifiant, pas au type. Il peut être placé n'importe où entre le nom et l'identifiant. Vous pourriez donc écrire cela :
int
*
u =
&
i, v =
0
;
qui crée un int* u, comme précédemment, et un int vqui n'est pas un pointeur. Comme les lecteurs trouvent souvent cela déroutant, il est recommandable de suivre la forme proposée dans ce livre.
8-2-3. Assignation et vérification de type▲
Le C++ est très spécial en ce qui concerne la vérification de type, et ce jusqu'à l'assignation de pointeur. Vous pouvez assigner l'adresse d'un objet non- constà un pointeur constparce que vous promettez simplement de ne pas changer quelque chose qui peut se changer. Par contre, vous ne pouvez pas assigner l'adresse d'un objet constà un pointeur non- constcar alors vous dites que vous pourriez changer l'objet via le pointeur. Bien sûr, vous puvez toujours utiliser la conversion de type ( cast, ndt) pour forcer une telle assignation, mais c'est une mauvaise habitude de programmation car vous brisez la constance de l'objet ainsi que la promesse de sécurité faite par le const. Par exemple :
//: C08:PointerAssignment.cpp
int
d =
1
;
const
int
e =
2
;
int
*
u =
&
d; // OK -- d n'est pas const
//!
int* v = &e; // Illicite -- e est const
int
*
w =
(int
*
)&
e; // Licite mais mauvaise habitude
int
main() {}
///
:~
Bien que C++ aide à prévenir les erreurs, il ne vous protège pas de vous-même si vous voulez enfreindre les mécanismes de sécurité.
Les tableaux de caractères littéraux
Le cas où la constance stricte n'est pas imposée, est le cas des tableaux de caractères littéraux. Vous pouvez dire :
char
*
cp =
"Salut"
;
et le compilateur l'acceptera sans se plaindre. C'est techniquement une erreur car un tableau de caractères littéraux (ici, “Salut”) est créé par le compilateur comme un tableau de caractères constant, et le résultat du tableau de caractère avec des guillemets est son adresse de début en mémoire. Modifier n'importe quel caractère dans le tableau constitue une erreur d'exécution, bien que tous les compilateurs n'imposent pas cela correctement.
Ainsi, les tableaux de caractères littéraux sont réellement des tableaux de caractères constants. Evidemment, le compilateur vous laisse vous en tirer en les traitant comme des non- constcar il y a beaucoup de code C qui repose là-dessus. Toutefois, si vous tentez de modifier la valeur dans un tableau de caractères littéral, le comportement n'est pas défini, bien que cela fonctionnera probablement sur beaucoup de machines.
Si vous voulez être capable de modifier la chaîne, mettez-là dans un tableau :
char
cp[] =
"howdy"
;
Comme les compilateurs ne font généralement pas la différence, on ne vous rappelera pas d'utiliser cette dernière forme et l'argument devient relativement subtil.
8-3. Arguments d'une fonction & valeurs retournées▲
L'utilisation de constpour spécifier les arguments d'une fonction et les valeurs retournées peut rendre confus le concept de constantes. Si vous passez des objets par valeur, spécifier constn'a aucune signification pour le client (cela veut dire que l'argument passé ne peut pas être modifié dans la fonction). Si vous retournez par valeur un objet d'une type défini par l'utilisateur en tant que const, cela signifie que la valeur retournée ne peut pas être modifiée. Si vous passez et retournez des adresses, constgarantit que la destination de l'adresse ne sera pas modifiée.
8-3-1. Passer par valeur const▲
Vous pouvez spécifier que les arguments de fonction soient constquand vous les passez par valeur, comme dans ce cas
void
f1(const
int
i) {
i++
; // Illégal - erreur à la compilation
}
mais qu'est-ce que cela veut dire ? Vous faites la promesse que la valeur originale de la variable ne sera pas modifiée par la fonction f1( ). Toutefois, comme l'argument est passé par valeur, vous faites immédiatement une copie de la variable originale, donc la promesse au client est implicitement tenue.
Dans la fonction, constprend le sens : l'argument ne peut être changé. C'est donc vraiment un outil pour le créateur de la fonction, et pas pour l'appelant.
Pour éviter la confusion à l'appelant, vous pouvez rendre l'argument constà l'intérieurde la fonction, plutôt que dans la liste des arguments. Vous pourriez faire cela avec un pointeur, mais une syntaxe plus jolie est utilisable avec les références, sujet qui sera développé en détails au Chapitre 11. Brièvement, une référence est comme un pointeur constant qui est automatiquement dé-référencé, et qui a donc l'effet d'être un alias vers un objet. Pour créer une référence, on utilise le &dans la définition. Finalement, la définition de fonction claire ressemble à ceci :
void
f2(int
ic) {
const
int
&
i =
ic;
i++
; // Illégal - erreur à la compilation}
}
Une fois encore, vous obtiendrez un message d'erreur, mais cette foi-ci la constance de l'objet local ne fait pas partie de la signature de la fonction ; elle n'a de sens que pour l'implémentation de la fonction et est ainsi dissimulée au client.
8-3-2. Retour par valeur const▲
Une réalité similaire s'applique aux valeurs retournées. Si vous dites qu'une valeur retournée par une fonction est const:
const
int
g();
vous promettez que la valeur originale (dans la portée de la fonction) ne sera pas modifiée. Une fois encore, comme vous la retournez par valeurs, elle est copiée si bien que la valeur originale ne pourra jamais être modifiée viala valeur retournée.
Au premier abord, cela peut faire paraître la déclaration constdénuée de sens. Vous pouvez constater le manque apparent d'effet de retourner des constpar valeurs dans cet exemple :
//: C08:Constval.cpp
// Retourner des const par valeur
// n'a pas de sens pour les types prédéfinis
int
f3() {
return
1
; }
const
int
f4() {
return
1
; }
int
main() {
const
int
j =
f3(); // Marche bien
int
k =
f4(); // Mais celui-ci marche bien également !
}
///
:~
Pour les types prédéfinis, cela n'a aucune importance que vous retourniez une valeur come const, donc vous devriez éviter la confusion au programmeur client et oublier constquand vous retournez un type prédéfini par valeur.
Retourner un constpar valeur devient important quand vous traitez des types définis par l'utilisateur. Si une fonction retourne comme constun objet de classe , la valeur de retour de cette fonction ne peut pas être une lvalue (c'est-à-dire, elle ne peut être assignée ou modifiée autrement). Par exemple :
//: C08:ConstReturnValues.cpp
// Constante retournée par valeur
// Le résultat ne peut être utilisé comme une lvalue
class
X {
int
i;
public
:
X(int
ii =
0
);
void
modify();
}
;
X::
X(int
ii) {
i =
ii; }
void
X::
modify() {
i++
; }
X f5() {
return
X();
}
const
X f6() {
return
X();
}
void
f7(X&
x) {
// Passé par référence pas const
x.modify();
}
int
main() {
f5() =
X(1
); // OK -- valeur de retour non const
f5().modify(); // OK
//!
f7(f5()); // Provoque warning ou erreur
// Cause des erreurs de compilation :
//!
f7(f5());
//!
f6() = X(1);
//!
f6().modify();
//!
f7(f6());
}
///
:~
f5( )renvoie un objet non- const, alors que f6( )renvoie un objet const X. Seule la valeur retournée qui n'est pas constpeut être utilisée comme une lvalue. Ainsi, il est important d'utiliser constquand vous retournez un objet par valeur si vous voulez éviter son utilisation comme lvalue.
La raison pour laquelle constne veut rien dire quand vous renvoyez un type prédéfini par valeur est que le compilateur empêche déjà qu'il soit utilisé comme lvalue (parce que c'est toujours une valeur, et pas une variable). C'est seulement lorsque que vous renvoyez des types définis par l'utilisateur par valeur que cela devient un problème.
La fonction f7( )prend son argument comme une référence(un moyen supplémentaire de manipuler les adresses en C++ qui sera le sujet du chapitre 11) qui n'est pas de type const. C'est en fait la même chose que d'utiliser un pointeur qui ne soit pas non plus de type const; seule la syntaxe est différente. La raison pour laquelle ce code ne compilera pas en C++ est qu'il y a création d'une variable temporaire.
Les variables temporaires
Parfois, pendant l'évaluation d'une expression, le compilateur doit créer des objets temporaires. Ce sont des objets comme n'importe quels autres : ils nécessitent un stockage et doivent être construits et détruits. La différence est que vous ne les voyez jamais ; le compilateur a la charge de décider s'ils sont nécessaires et de fixer les détails de leur existence. Mais il faut noter une chose en ce qui concerne les variables temporaires : elles sont automatiquement const. Comme vous ne serez généralement pas capable de manipuler un objet temporaire, dire de faire quelque chose qui le modifierait est presque à coup sûr une erreur parce que vous ne serez pas capable d'utiliser cette information. En rendant tous les temporaires automatiquement const, le compilateur vous informe quand vous commettez cette erreur.
Dans l'exemple ci-dessus, f5( )renvoie un objet Xqui n'est pas const. Mais dans l'expression :
f7(f5());
Le compilateur doit créer un objet temporaire pour retenir la valeur de f5( )afin qu'elle soit passée à f7( ). Cela serait correct si f7( )prenait ses arguments par valeur ; alors, l'objet temporaire serait copié dans f7( )et ce qui arriverait au Xtemporaire n'aurait aucune importance. Cependant, f7( )prend ses arguments par référence, ce qui signifie dans cette exemple qu'il prend l'adresse du Xtemporaire. Comme f7( )ne prend pas ses arguments par référence de type const, elle a la permission de modifier l'objet temporaire. Mais le compilateur sait que l'objet temporaire disparaîtra dès que l'évaluation de l'expression sera achevée, et ainsi toute modification que vous ferez au Xtemporaire sera perdue. En faisant de tous les objets temporaires des const, cete situation produit un message d'erreur de compilation, afin que vous ne rencontriez pas un bug qui serait très difficile à trouver.
Toutefois, notez les expressions licites :
f5() =
X(1
);
f5().modify();
Bien que ces passages fassent appel au compilateur, ils sont problématiques. f5( )renvoie un objet X, et pour que le compilateur satisfasse les expressions ci-dessus il doit créer un objet temporaire pour retenir cette valeur temporaire. Ainsi, dans les deux expressions l'objet temporaire est modifié, et dès que l'expression est terminée, l'objet temporaire est détruit. En conséquence, les modifications sont perdues et ce code est probablement un bug ; mais le compilateur ne vous dit rien. Des expressions comme celles-ci sont suffisamment simples pour que vous détectiez le problème, mais quand les choses deviennent plus compliquées, il est possible qu'un bug se glisse à travers ces fissures.
La façon dont la constance des objets d'une classe est préservée est montrée plus loin dans ce chapitre.
8-3-3. Passer et retourner des adresses▲
Si vous passez ou retournez une adresse (un pointeur ou une référence), le programmeur client peut la prendre et modifier sa valeur originale. Si vous rendez le pointeur ou la référence const, vous l'en empêcher, ce qui peut éviter quelques douleurs. En fait, lorsque vous passez une adresse à une fonction, vous devriez, autant que possible, en faire un const. Si vous ne le faites pas, vous excluez la possibilité d'utiliser cette fonction avec quelque type constque ce soit.
Le choix de retourner un pointeur ou une référence comme constdépend de ce que vous voulez autoriser le programmeur client à faire avec. Voici un exemple qui démontre l'usage de pointeurs constcomme arguments de fonction et valeurs retournées :
//: C08:ConstPointer.cpp
// Pointeur constants comme arguments ou retournés
void
t(int
*
) {}
void
u(const
int
*
cip) {
//!
*cip = 2; // Illicite - modifie la valeur
int
i =
*
cip; // OK -- copie la valeur
//!
int* ip2 = cip; // Illicite : pas const
}
const
char
*
v() {
// Retourne l'adresse du tableau de caractère static :
return
"result of function v()"
;
}
const
int
*
const
w() {
static
int
i;
return
&
i;
}
int
main() {
int
x =
0
;
int
*
ip =
&
x;
const
int
*
cip =
&
x;
t(ip); // OK
//!
t(cip); // Pas bon
u(ip); // OK
u(cip); // Egalement OK
//!
char* cp = v(); // Pas bon
const
char
*
ccp =
v(); // OK
//!
int* ip2 = w(); // Pas bon
const
int
*
const
ccip =
w(); // OK
const
int
*
cip2 =
w(); // OK
//!
*w() = 1; // Pas bon
}
///
:~
La fonction t( )prend un pointeur ordinaire (non- const) comme argument, et u( )prend un pointeur const. Dans u( )vous pouvez constater qu'essayer de modifier la destination du pointeur constest illicite, mais vous pouvez bien sûr copier l'information dans une variable non constante. Le compilateur vous empêche également de créer un pointeur non- constant en utilisant l'adresse stockée dans un pointeur de type const.
Les fonctions v( )et w( )testent la sémantique d'une valeur de retour. v( )retourne un constchar*créé à partir d'un tableau de caractères. Cette déclaration produit vraiment l'adresse du tableau de caractères, après que le compilateur l'ait créé et stocké dans l'aire de stockage statique. Comme mentionné plus haut, ce tableau de caractères est techniquement une constante, ce qui est correctement exprimé par la valeur de retour de v( ).
La valeur retournée par w( )requiert que le pointeur et ce vers quoi il pointe soient des const. Comme avec v( ), la valeur retournée par w( )est valide après le retour de la fonction uniquement parce qu'elle est static. Vous ne devez jamais renvoyer des pointeurs vers des variables de pile locales parce qu'ils seront invalides après le retour de la fonction et le nettoyage de la pile. (Un autre pointeur habituel que vous pouvez renvoyer est l'adresse du stockage alloué sur le tas, qui est toujours valide après le retour de la fonction).
Dans main( ), les fonctions sont testées avec divers arguments. Vous pouvez constater que t( )acceptera un pointeur non- constcomme argument, mais si vous essayez de lui passer un pointeur vers un const, il n'y a aucune garantie que t( )laissera le pointeur tranquille, ce qui fait que le compilateur génère un message d'erreur. u( )prend un pointeur const, et acceptera donc les deux types d'arguments. Ainsi, une fonction qui prend un pointeur constest plus générale qu'une fonction qui n'en prend pas.
Comme prévu, la valeur de retour de v( )peut être assignée uniquement à un pointeur vers un const. Vous vous attendriez aussi à ce que le compilateur refuse d'assigner la valeur de retour de w( )à un pointeur non- const, et accepte un const int* const, mais il accepte en fait également un const int*, qui ne correspond pas exactement au type retourné. Encore une fois, comme la valeur (qui est l'adresse contenue dans le pointeur) est copiée, la promesse que la variable originale ne sera pas atteinte est automatiquement tenue. Ainsi, le second constdans const int* constn'a de sens que quand vous essayez de l'utiliser comme une lvalue, auquel cas le compilateur vous en empêche.
Passage d'argument standard
En C, il est très courant de passer par valeur, et quand vous voulez passer une adresse votre seul possibilité est d'utiliser un pointeur (43). Toutefois, aucune de ces approches n'est préférée en C++. Au lieu de cela, votre premier choix quand vous passez un argument est de le passer par référence, et par référence de type const, qui plus est. Pour le programmeur client, la syntaxe est identique à celle du passage par valeur, et il n'y a donc aucune confusion à propos des pointeurs ; ils n'ont même pas besoin de penser aux pointeurs. Pour le créateur de la fonction, passer une adresse est pratiquement toujours plus efficace que passer un objet de classe entier, et si vous passez par référence de type const, cela signifie que votre fonction ne changera pas la destination de cette adresse, ce qui fait que l'effet du point de vue du programmeur client est exactement le même que de passer par valeur (c'est juste plus efficace).
A cause de la syntaxe des références (cela ressemble à un passage par valeur pour l'appelant) il est possible de passer un objet temporaire à une fonction qui prend une référence de type const, alors que vous ne pouvez jamais passer un objet temporaire à une fonction qui prend un pointeur ; avec un pointeur, l'adresse doit être prise explicitement. Ainsi, le passage par référence crée une nouvelle situation qui n'existe jamais en C : un objet temporaire, qui est toujours const, peut voir son adressepassée à une fonction. C'est pourquoi, pour permettre aux objets temporaires d'être passés aux fonctions par référence, l'argument doit être une référence de type const. L'exemple suivant le démontre :
//: C08:ConstTemporary.cpp
// Les temporaires sont des <b>const</b>
class
X {}
;
X f() {
return
X(); }
// Retour par valeur
void
g1(X&
) {}
// Passage par référence de type non-const
void
g2(const
X&
) {}
// Passage par référence de type const
int
main() {
// Erreur : const temporaire créé par f()
//!
g1(f());
// OK: g2 prend une référence const
g2(f());
}
///
:~
f( )retourne un objet de class Xpar valeur. Cela signifie que quand vous prenez immédiatement la valeur de retour de f( )et que vous la passez à une autre fonction comme dans l'appel de g1( )et g2( ), un objet temporaire est créé et cet objet temporaire est de type const. Ainsi, l'appel dans g1( )est une erreur parce que g1( )ne prend pas de référence de type const, mais l'appel à g2( )est correct.
8-4. Classes▲
Cette section montre de quelles façons vous pouvez utiliser constavec les classes. Vous pourriez vouloir créer un constlocal dans une classe pour l'utiliser dans des expressions constantes qui seront évaluées à la compilation. Toutefois, le sens de constest différent au sein des classes, et vous devez donc comprendre les options offertes afin de créer des données membres constd'une classe.
Vous pouvez aussi rendre un objet entier const(et comme vous venez de le voir, le compilateur crée toujours des objets temporaires const). Mais préserver la constance d'un objet est plus complexe. Le compilateur peut garantir la constance d'un type prédéfini mais il ne peut pas gérer les intrications d'une classe. Pour garantir la constance d'un objet d'une classe, la fonction membre constest introduite : seule une fonction membre constpeut être appelée pour un objet const.
8-4-1. const dans les classes▲
Un des endroits où vous aimeriez utiliser un constpour les expressions constantes est à l'intérieur des classes. L'exemple type est quand vous créez un tableau dans une classe et vous voulez utiliser un constà la place d'un #definepour définir la taille du tableau et l'utiliser dans des calculs impliquant le tableau. La taille du tableau est quelque chose que vous aimeriez garder caché dans la classe, de telle sorte que si vous utilisiez un nom comme taille, par exemple, vous puissiez l'utiliser également dans une autre classe sans qu'il y ait de conflit. Le préprocesseur traite tous les #definecomme globaux à partir du point où ils sont définis, donc ils n'auront pas l'effet désiré.
Vous pourriez supposer que le choix logique est de placer un constdans la classe. Ceci ne produit pas le résultat escompté. Dans une classe, constrevient partiellement à son sens en C. Il alloue un espace de stockage dans chaque objet et représente une valeur qui ne peut être initialisée qu'une fois et ne peut changer par la suite. L'usage de constdans une classe signifie "Ceci est constant pour toute la durée de vie de l'objet". Toutefois, chaque objet différent peut contenir une valeur différente pour cette constante.
Ainsi, quand vous créez un constordinaire (pas static) dans une classe, vous ne pouvez pas lui donner une valeur initiale. Cette initialisation doit avoir lieu dans le constructeur, bien sûr, mais à un endroit spécial du constructeur. Parce qu'un constdoit être initialisé au point où il est créé, il doit déjà l'être dans le corps du constructeur. Autrement vous risqueriez d'avoir à attendre jusqu'à un certain point du constructeur et le constresterait non initialisé quelques temps. En outre, il n'y aurait rien qui vous empêcherait de modifier la valeur du constà différents endroits du corps du constructeur.
La liste d'initialisation du constructeur
Le point d'initialisation spécial est appelé liste d'initialisation du constructeur, et a été initialement développée pour être utilisée dans l'héritage (couvert au Chapitre 14). La liste d'initialisation du constructeur - qui, comme son nom l'indique, n'intervient que dans la définition du constructeur - est une liste "d'appels de constructeurs" qui a lieu après la liste des arguments et deux points, mais avant l'accolade ouvrante du corps du constructeur. Ceci pour vous rappeler que l'initialisation dans la liste a lieu avant que tout code du corps du constructeur ne soit exécuté. C'est là qu'il faut mettre toutes les initialisations de const. La forme correcte pour constdans une classe est montrée ici :
//: C08:ConstInitialization.cpp
// Initialiser des const dans les classes
#include
<iostream>
using
namespace
std;
class
Fred {
const
int
size;
public
:
Fred(int
sz);
void
print();
}
;
Fred::
Fred(int
sz) : size(sz) {}
void
Fred::
print() {
cout <<
size <<
endl; }
int
main() {
Fred a(1
), b(2
), c(3
);
a.print(), b.print(), c.print();
}
///
:~
La forme de la liste d'initialisation du constructeur montrée ci-dessus est déroutante au début parce que vous n'êtes pas habitués à voir un type prédéfini traité comme s'il avait un constructeur.
“Constructeurs” pour types prédéfinis
Comme le langage se développait et que plus d'effort était fourni pour rendre les types définis par l'utilisatur plus ressemblant aux types prédéfinis, il devint clair qu'il y avait des fois où il était utile de rendre des types prédéfinis semblables aux types définis par l'utilisateur. Dans la liste d'initialisation du constructeur, vous pouvez traiter un type prédéfini comme s'il avait un constructeur, comme ceci :
//: C08:BuiltInTypeConstructors.cpp
#include
<iostream>
using
namespace
std;
class
B {
int
i;
public
:
B(int
ii);
void
print();
}
;
B::
B(int
ii) : i(ii) {}
void
B::
print() {
cout <<
i <<
endl; }
int
main() {
B a(1
), b(2
);
float
pi(3.14159
);
a.print(); b.print();
cout <<
pi <<
endl;
}
///
:~
C'est critique quand on initialise des données membres constcar elles doivent être initialisées avant l'entrée dans le corps de la fonction.
Il était logique d'étendre ce "constructeur" pour types prédéfinis (qui signifie simplement allocation) au cas général, ce qui explique pourquoi la définition float pi(3.14159)fonctionne dans le code ci-dessus.
Il est souvent utile d'encapsuler un type prédéfini dans une classe pour garantir son initisalisation par le constructeur. Par exemple, ici une classe Integer(entier, ndt) :
//: C08:EncapsulatingTypes.cpp
#include
<iostream>
using
namespace
std;
class
Integer {
int
i;
public
:
Integer(int
ii =
0
);
void
print();
}
;
Integer::
Integer(int
ii) : i(ii) {}
void
Integer::
print() {
cout <<
i <<
' '
; }
int
main() {
Integer i[100
];
for
(int
j =
0
; j <
100
; j++
)
i[j].print();
}
///
:~
Le tableau d' Integerdans main( )est entièrement initialisé à zéro automatiquement. Cette initialisation n'est pas nécessairement plus coûteuse qu'une boucle forou que memset( ). Beaucoup de compilateurs l'optimise très bien.
8-4-2. Constantes de compilation dans les classes▲
L'utilisation vue ci-dessus de constest intéressante et probablement utile dans certains cas, mais elle ne résoud pas le problème initial qui était : "comment créer une constante de compilation dans une classe ?" La réponse impose l'usage d'un mot-clef supplémentaire qui ne sera pas pleinement introduit avant le Chapitre 10 : static. Ce mot-clef, selon les situations, signifie "une seule instante, indépendamment du nombre d'objets créés", qui est précisément ce dont nous avons besoin ici : un membre d'une classe constant et qui ne peut changer d'un objet de la classe à un autre. Ainsi, un static constd'un type prédéfini peut être traité comme une constante de compilation.
Il y a un aspect de static const, quand on l'utilise dans une classe, qui est un peu inhabituel : vous devez fournir l'initialiseur au point de définition du static const. C'est quelque chose qui ne se produit qu'avec static const; Autant que vous aimeriez le faire dans d'autres situations, cela ne marchera pas car toutes les autres données membres doivent être initialisées dans le constructeur ou dans d'autres fonctions membres.
Voici un exemple qui montre la création et l'utilisation d'un static constappelé sizedans une classe qui représente une pile de pointeur vers des chaines. (44):
//: C08:StringStack.cpp
// Utilisation de static const pour créer une
// constante de compilation dans une classe
#include
<string>
#include
<iostream>
using
namespace
std;
class
StringStack {
static
const
int
size =
100
;
const
string*
stack[size];
int
index;
public
:
StringStack();
void
push(const
string*
s);
const
string*
pop();
}
;
StringStack::
StringStack() : index(0
) {
memset(stack, 0
, size *
sizeof
(string*
));
}
void
StringStack::
push(const
string*
s) {
if
(index <
size)
stack[index++
] =
s;
}
const
string*
StringStack::
pop() {
if
(index >
0
) {
const
string*
rv =
stack[--
index];
stack[index] =
0
;
return
rv;
}
return
0
;
}
string iceCream[] =
{
"pralines & cream"
,
"fudge ripple"
,
"jamocha almond fudge"
,
"wild mountain blackberry"
,
"raspberry sorbet"
,
"lemon swirl"
,
"rocky road"
,
"deep chocolate fudge"
}
;
const
int
iCsz =
sizeof
iceCream /
sizeof
*
iceCream;
int
main() {
StringStack ss;
for
(int
i =
0
; i <
iCsz; i++
)
ss.push(&
iceCream[i]);
const
string*
cp;
while
((cp =
ss.pop()) !=
0
)
cout <<
*
cp <<
endl;
}
///
:~
Comme sizeest utilisé pour déterminer la taille (size en anglais, ndt) du tableau stack, c'est de fait une constante de compilation, mais une qui est masquée au sein de la classe.
Remarquez que push( )prend un conststring*comme argument, que pop( )renvoie un conststring*, et StringStackcontient const string*. Si ce n'était pas le cas, vous ne pourriez pas utiliser un StringStackpour contenir les pointeurs dans iceCream. Toutefois, cela vous empêche également de faire quoi que ce soit qui modifierait les objets contenus dans StringStack. Bien sûr, tous les conteneurs ne sont pas conçus avec cette restriction.
Le “enum hack” dans le vieux code
Dans les versions plus anciennes de C++, static constn'était pas supporté au sein des classes. Cela signifiait que constétait inutile pour les expressions constantes dans les classes. Toutefois, les gens voulaient toujours le faire si bien qu'une solution typique (généralement dénommée “enum hack”) consistait à utiliser un enumnon typé et non instancié. Une énumération doit avoir toutes ses valeurs définies à la compilation, c'est local à la classe, et ses valeurs sont disponibles pour les expressions constantes. Ainsi, vous verrez souvent :
//: C08:EnumHack.cpp
#include
<iostream>
using
namespace
std;
class
Bunch {
enum
{
size =
1000
}
;
int
i[size];
}
;
int
main() {
cout <<
"sizeof(Bunch) = "
<<
sizeof
(Bunch)
<<
", sizeof(i[1000]) = "
<<
sizeof
(int
[1000
]) <<
endl;
}
///
:~
L'utilisation de enumici n'occupera aucune place dans l'objet, et les énumérateurs sont tous évalués à la compilation. Vous pouvez également explicitement établir la valeur des énumérateurs :
enum
{
one =
1
, two =
2
, three }
;
Avec des types enumintégraux, le compilateur continuera de compter à partir de la dernière valeur, si bien que l'énumérateur threerecevra la valeur 3.
Dans l'exemple StringStack.cppci-dessus, la ligne:
static
const
int
size =
100
;
deviendrait :
enum
{
size =
100
}
;
Bien que vous verrez souvent la technique enumdans le code ancien, static consta été ajouté au langage pour résoudre précisément ce problème. Cependant, il n'y a pas de raison contraignante qui impose d'utiliser static constplutôt que le hack de enum, et dans ce livre c'est ce dernier qui sera utilisé parce qu'il est supporté par davantage de compilateurs au moment de sa rédaction.
8-4-3. objets cont & fonctions membres▲
Les fonctions membres d'une classe peuvent être rendues const. Qu'est-ce que cela veut dire ? Pour le comprendre, vous devez d'abord saisir le concept d'objets const.
Un objet constest défini de la même façon pour un type défini par l'utilisateur ou pour un type prédéfini. Par exemple :
const
int
i =
1
;
const
blob b(2
);
Ici, best un objet constde type blob. Son constructeur est appelé avec l'argument 2. Pour que le compilateur impose la constance, il doit s'assurer qu'aucune donnée membre de l'objet n'est changée pendant la durée de vie de l'objet. Il peut facilement garantir qu'aucune donnée publique n'est modifiée, mais comment peut-il savoir quelles fonctions membres modifieront les données et lesquelles sont "sûres" pour un objet const?
Si vous déclarez une fonction membre const, vous dites au compilateur que la fonction peut être appelée pour un objet const. Une fonction membre qui n'est pas spécifiquement déclarée constest traitée comme une fonction qui modifiera les données membres de l'objet, et le compilateur ne vous permettra pas de l'appeler pour un objet const.
Cela ne s'arrête pas ici, cependant. Diresimplement qu'une fonction membre est constne garantit pas qu'elle se comportera ainsi, si bien que le compilateur vous force à définir la spécification constquand vous définissez la fonction. (Le constfait partie de la signature de la fonction, si bien que le compilateur comme l'éditeur de liens vérifient la constance.) Ensuite, il garantit la constance pendant la définition de la fonction en émettant un message d'erreur si vous essayez de changer un membre de l'objet oud'appeler une fonction membre non- const. Ainsi on garantit que toute fonction membre que vous déclarez constse comportera de cette façon.
Pour comprendre la syntaxe de déclaration des fonctions membres const, remarquez tout d'abord que placer la déclaration constavant la fonction signifie que la valeur de retour est const: cela ne produit pas l'effet désiré. A la place, vous devez spécifier le constaprèsla liste des arguments. Par exemple :
//: C08:ConstMember.cpp
class
X {
int
i;
public
:
X(int
ii);
int
f() const
;
}
;
X::
X(int
ii) : i(ii) {}
int
X::
f() const
{
return
i; }
int
main() {
X x1(10
);
const
X x2(20
);
x1.f();
x2.f();
}
///
:~
Remarquez que le mot-clef constdoit être répété dans la définition ou bien que le compilateur la verra comme une fonction différente. Comme f( )est une fonction membre const, si elle essaie de modifier ide quelle que façon que ce soit oud'appeler une autre fonction membre qui ne soit pas const, le compilateur le signale comme une erreur.
Vous pouvez constater qu'une fonction membre constpeut être appelée en toute sécurité que les objets soient constou non. Ainsi, vous pouvez le considérer comme la forme la plus générale de fonction membre (et à cause de cela, il est regrettable que les fonctions membres ne soient pas constpar défaut). Toute fonction qui ne modifie pas les données membres devrait être déclarée const, afin qu'elle puisse être utilisée avec des objets const.
Voici un exemple qui compare les fonctions membres constet non const:
//: C08:Quoter.cpp
// Sélection aléatoire de citation
#include
<iostream>
#include
<cstdlib>
// Générateur de nombres aléatoires
#include
<ctime>
// Comme germe du générateur
using
namespace
std;
class
Quoter {
int
lastquote;
public
:
Quoter();
int
lastQuote() const
;
const
char
*
quote();
}
;
Quoter::
Quoter(){
lastquote =
-
1
;
srand(time(0
)); // Germe du générateur de nombres aléatoires
}
int
Quoter::
lastQuote() const
{
return
lastquote;
}
const
char
*
Quoter::
quote() {
static
const
char
*
quotes[] =
{
"Are we having fun yet?"
,
"Doctors always know best"
,
"Is it ... Atomic?"
,
"Fear is obscene"
,
"There is no scientific evidence "
"to support the idea "
"that life is serious"
,
"Things that make us happy, make us wise"
,
}
;
const
int
qsize =
sizeof
quotes/
sizeof
*
quotes;
int
qnum =
rand() %
qsize;
while
(lastquote >=
0
&&
qnum ==
lastquote)
qnum =
rand() %
qsize;
return
quotes[lastquote =
qnum];
}
int
main() {
Quoter q;
const
Quoter cq;
cq.lastQuote(); // OK
//!
cq.quote(); // Pas OK; fonction pas const
for
(int
i =
0
; i <
20
; i++
)
cout <<
q.quote() <<
endl;
}
///
:~
Ni les constructeurs ni les destructeurs ne peuvent être constparce qu'ils effectuent pour ainsi dire toujours des modifications dans l'objet pendant l'initialisation et le nettoyage. La fonction membre quote( )ne peut pas non plus être constparce qu'elle modifie la donnée membre lastquote(cf. l'instruction return). Toutefois, lastQuote( )ne réalise aucune modification et peut donc être constet peut être appelée par l'objet const cqen toute sécurité.
mutable : const logique vs. const de bit
Que se passe-t-il si vous voulez créer une fonction membre constmais que vous voulez toujours changer certaines données de l'objet ? Ceci est parfois appelé constde bitet constlogique(parfois également constde membre). constde bit signifie que chaque bit dans l'objet est permanent, si bien qu'une image par bit de l'objet ne changera jamais. constlogique signifique que, bien que l'objet entier soit conceptuellement constant, il peut y avoir des changements sur une base membre à membre. Toutefois, si l'on dit au compilateur qu'un objet est const, il préservera jalousement cet objet pour garantir une constance de bit. Pour réaliser la constance logique, il y a deux façons de modifier une donnée membre depuis une fonction membre const.
La première approche est historique est appelée éviction de la constance par transtypage( casting away constnessen anglais, ndt) C'est réalisé de manière relativement bizarre. Vous prenez this(le mot-clef qui donne l'adresse de l'objet courant) et vous le transtypé vers un pointeur vers un objet du type courant. Il semble que thisest déjàun pointeur de ce type. Toutefois, dans une fonction membre constc'est en fait un pointeur const. Le transtypage permet de supprimer la constance pour cette opération. Voici un exemple :
//: C08:Castaway.cpp
// Constance contournée par transtypage
class
Y {
int
i;
public
:
Y();
void
f() const
;
}
;
Y::
Y() {
i =
0
; }
void
Y::
f() const
{
//!
i++; // Erreur -- fonction membre const
((Y*
)this
)->
i++
; // OK : contourne la constance
// Mieux : utilise la syntaxe de transtypage expicite du :
(const_cast
<
Y*>
(this
))->
i++
;
}
int
main() {
const
Y yy;
yy.f(); // Le modifie réellement !
}
///
:~
Cette approche fonctionne et vous la verrez utilisée dans le code ancien, mais ce n'est pas la technique préférée. Le problème est que ce manque de constance est dissimulé dans la définition de la fonction membre, et vous ne pouvez pas savoir grâce à l'interface de la classe que la donnée de l'objet est réellement modifiée à moins que vous n'ayez accès au code source (et vous devez suspecter que la constance est contournée par transtypage, et chercher cette opération). Pour mettre les choses au clair, vous devriez utiliser le mot-clef mutabledans la déclaration de la classe pour spécifier qu'une donnée membre particulière peut changer dans un objet const:
//: C08:Mutable.cpp
// Le mot-clef "mutable"
class
Z {
int
i;
mutable
int
j;
public
:
Z();
void
f() const
;
}
;
Z::
Z() : i(0
), j(0
) {}
void
Z::
f() const
{
//!
i++; // Erreur -- fonction membre const
j++
; // OK: mutable
}
int
main() {
const
Z zz;
zz.f(); // Le modifie réellement !
}
///
:~
Ainsi, l'utilisateur de la classe peut voir par la déclaration quels membres sont susceptibles d'être modifiés dans une fonction membre const.
ROMabilité
Si un objet est défini const, c'est un candidat pour être placé dans la mémoire morte (ROM = Read Only Memory, ndt), ce qui est souvent une question importante dans la programmation des systèmes embarqués. Le simple fait de rendre un objet const, toutefois, ne suffit pas - les conditions nécessaires pour la ROMabilité sont bien plus strictes. Bien sûr, l'objet doit avoir la constance de bit, plutôt que logique. Ceci est facile à voir si la constance logique est implémentée uniquement par le mot-clef mutable, mais probablement indétectable par le compilateur si la constance est contournée par transtypage dans une fonction membre const. En outre,
- La classe ou la structure ne doivent pas avoir de constructeur ou de destructeur définis par l'utilisateur.
- Il ne peut pas y avoir de classe de base (cf. Chapitre 14) ou d'objet membre avec un constructeur ou un destructeur défini par l'utilisateur.
L'effet d'une opération d'écriture sur toute partie d'un obet constde type ROMable est indéfini. Bien qu'un objet correctement conçu puisse être placé dans la ROM, aucun nobjet n'est jamais obligéd'être placé dans la ROM.
8-5. volatile▲
La syntaxe de volatileest identique à celle de const, mais volatilesignifie "Cette donnée pourrait changer sans que le compilateur le sache". D'une façon ou d'une autre, l'environnement modifie la donnée (potentiellement par du multitâche, du multithreading ou des interruptions), et volatiledit au compilateur de ne faire aucune hypothèse à propos de cette donnée, spécialement pendant l'optimisation.
Si le compilateur dit "J'ai lu cette donnée dans un registre plus tôt, et je n'y ai pas touché", normalement, il ne devrait pas lire la donnée à nouveau. Mais si la donnée est volatile, le compilateur ne peut pas faire ce genre d'hypohèse parce que la donnée pourrait avoir été modifiée par un autre processus, et il doit lire à nouveau cette donnée plutôt qu'optimiser le code en supprimant ce qui serait normalement une lecture redondante.
Vous créez des objets volatileen utilisant la même syntaxe que vous utilisez pour créer des objets const. Vous pouvez aussi créer des objets constvolatile, qui ne peuvent pas être modifiés par le programmeur client mais changent plutôt sous l'action d'organismes extérieurs. Voici un exemple qui pourrait représenter une classe associée à un fragment de communication avec le hardware :
//: C08:Volatile.cpp
// Le mot-clef volatile
class
Comm {
const
volatile
unsigned
char
byte;
volatile
unsigned
char
flag;
enum
{
bufsize =
100
}
;
unsigned
char
buf[bufsize];
int
index;
public
:
Comm();
void
isr() volatile
;
char
read(int
index) const
;
}
;
Comm::
Comm() : index(0
), byte(0
), flag(0
) {}
// Juste une démonstration ; ne fonctionnera pas vraiment
// comme routine d'interruption :
void
Comm::
isr() volatile
{
flag =
0
;
buf[index++
] =
byte;
// Repart au début du buffer :
if
(index >=
bufsize) index =
0
;
}
char
Comm::
read(int
index) const
{
if
(index <
0
||
index >=
bufsize)
return
0
;
return
buf[index];
}
int
main() {
volatile
Comm Port;
Port.isr(); // OK
//!
Port.read(0); // Erreur, read() n'est pas volatile
}
///
:~
Comme avec const, vous pouvez utiliser volatilepour les données membres, les fonctions membres et les objets eux-mêmes. Vous pouvez appeler des fonctions membres volatileuniquement pour des objets volatile.
La raison pour laquelle isr( )ne peut pas vraiment être utilisée comme routine d'interruption est que dans une fonction membre, l'adresse de l'objet courant ( this) doit être passée secrètement, et une routine d'interruption ne prend généralement aucun argument. Pour résoudre ce problème, vous pouvez faire de isr( )une fonction membre static, sujet couvert au Chapitre 10.
La syntaxe de volatileest identique à celle de const, si bien que les discussions de ces deux mots-clefs sont souvent menées ensembles. Les deux sont dénommés qualificateurs c-v.
8-6. Résumé▲
Le mot-clef constvous donne la possibilité de définir comme constants des objets, des arguments de fonctions, des valeurs retournées et des fonctions membres, et d'éliminer le préprocesseur pour la substitution de valeurs sans perdre les bénéfices du préprocesseur. Tout cela fournit un moyen supplémentaire pour la vérification des types et la sécurité de vos programmes. L'usage de la technique dite de const conformité(l'utilisation de constpartout où c'est possible) peut sauver la vie de projets entiers.
Bien que vous puissiez ignorer constet utiliser les vieilles pratiques du C, ce mot-clef est ici pour vous aider. Les Chapitres 11 et suivants commencent à utiliser abondamment les références, et ainsi vous verrez encore mieux à quel point il est critique d'utiliser constavec les arguments de fonction.
8-7. Exercices▲
La solution de certains exercices sélectionnés peut se trouver dans le document électronique The Thinking in C++ Annotated Solution Guide, disponible pour une somme modique à www.BruceEckel.com.
- Créer trois valeurs constint, puis additionez les pour produire une valeur qui détermine la taille d'un tableau dans la définition d'un tableau. Essayez de compiler le même code en C et regardez ce qu'il se produit (vous pouvez généralement forcer votre compilateur C++ à fonctionner comme un compilateur C en utilisant un argument de la ligne de commande).
- Démontrez-vous que les compilateurs C et C++ traitent réellement les constantes différemment. Créer un constglobal et utilisez-le dans une expression globale constante, puis compilez-le en C et en C++.
- Créez des exemples de définitions constpour tous les types prédéfinis et leurs variantes. Utilisez les dans des expressions avec d'autres constpour créer de nouvelles définitions const. Assurez qu'elles se compilent correctement.
- Créez une définition const dans un fichier d'en-tête, incluez ce fichier dans deux fichiers .cpp, puis compiler et faites l'édition de liens de ces fichiers. Vous ne devriez avoir aucune erreur. Maintenant, faites la même chose en C.
- Créez un constdont la valeur est déterminée à l'exécution en lisant l'heure à laquelle démarre le programme (vous devrez utiliser l'en-tête standard <ctime>). Plus loin dans le programme, essayez de lire une deuxième valeur de l'heure dans votre constet regardez ce qu'il se produit.
- Créez un tableau constde char, puis essayez de changer l'un des char.
- Créez une instruction extern constdans un fichier, et placez un main( )dans ce fichier qui imprime la valeur de l' extern const. Donnez une définition extern constdans un autre fichier, puis compilez et liez les deux fichiers ensemble.
- Ecrivez deux pointeurs vers constlongen utilisant les deux formes de déclaration. Faites pointer l'un d'entre eux vers un tableau de long. Montrez que vous pouvez incrémenter et décrémenter le pointeur, mais que vous ne pouvez pas changer ce vers quoi il pointe.
- Ecrivez un pointeur constvers un double, et faites le pointer vers un tableau de double. Montrez que vous pouvez modifier ce vers quoi il pointe, mais que vous ne pouvez incrémenter ou décrémenter le pointeur.
- Ecrivez un pointeur constvers un objet const. Montrez que vous ne pouvez que lire la valeur vers laquelle pointe le pointeur, mais que vous ne pouvez modifier ni le pointeur ni ce vers quoi il pointe.
- Supprimez le commentaire sur la ligne de code générant une erreur dans PointerAssignment.cpppour voir l'erreur que génère votre compilateur.
- Créez un tableau de caractères littéral avec un pointeur qui pointe vers le début du tableau. A présent, utilisez le pointeur pour modifier des éléments dans le tableau. Est-ce que votre compilateur signale cela comme ue erreur ? Le devrait-il ? S'il ne le fait pas, pourquoi pensez vous que c'est le cas ?
- Créez une fonction qui prend un argument par valeur comme const; essayez de modifier cet argument dans le corps de la fonction.
- Créez une fonction qui prenne un floatpar valeur. Dans la fonction, liez un const float&à l'argument, et à partir de là, utilisez uniquement la référence pour être sûr que l'argument n'est pas modifié.
- Modifiez ConstReturnValues.cppen supprimant les commentaires des lignes provoquant des erreurs l'une après l'autre, pour voir quels messages d'erreurs votre compilateur génère.
- Modifiez ConstPointer.cppen supprimant les commentaires des lignes provoquant des erreurs l'une après l'autre, pour voir quels messages d'erreurs votre compilateur génère.
- Faites une nouvelle version de ConstPointer.cppappelée ConstReference.cppqui utilise les références au lieu des pointeurs (vous aurez peut-être besoin de consulter le Chapitre 11, plus loin).
- Modifiez ConstTemporary.cppen supprimant le commentaire de la ligne provoquant une erreur, pour voir quel message d'erreur votre compilateur génère.
- Créez une classe contenant à la fois un floatconstet non- const. Initialisez-les en utilisant la liste d'initialisation du constructeur.
- Créez une classe MyStringqui contient un stringet possède un constructeur qui initialise le string, et une fonction print( ). Modifiez StringStack.cppafin que le conteneur contienne des objets MyString, et main( )afin qu'il les affiche.
- Créez une classe contenant un membre constque vous intialisez dans la liste d'initialisation du constructeur et une énumération sans label que vous utilisez pour déterminer une taille de tableau.
- Dans ConstMember.cpp, supprimez l'instruction constsur la définition de la fonction membre, mais laissez le dans la déclaration, pour voir quel genre de message d'erreur vous obtenez du compilateur.
- Créez une classe avec à la fois des fonctions membres constet non- const. Créez des objets constet non- constde cette classe, et essayez d'appeler les différents types de fonctions membres avec les différents types d'objets.
- Créez une classe avec des fonctions membres constet non- const. Essayez d'appeler une fonction membre non- constdepuis une fonction membre constpour voir quel est le genre de messages d'erreur du compilateur que vous obtenez.
- Dans Mutable.cpp, supprimez le commentaire de la ligne provoquant une erreur, pour voir quel message d'erreur votre compilateur génère.
- Modifiez Quoter.cppen faisant de quote( )une fonction membre constet de lastquoteun mutable.
- Créez une classe avec une donnée membre volatile. Créez des fonctions membres volatileet non- volatilequi modifient la donnée membre volatile, et voyez ce que dit le compilateur. Créez des objets de votre classe, volatileet non- volatile, et essayez d'appeler des fonctions membres volatileet non- volatilepour voir ce qui fonctionne et les messages d'erreur que produit le compilateur.
- Créez une classe appelée oiseauqui peut voler( )et une classe caillouqui ne le peut pas. Créez un objet caillou, prenez son adresse, et assignez-là à un void*. Maintenant, prenez le void*et assignez-le à un oiseau(vous devrez utiliser le transtypage), et appelez voler( )grâce au pointeur. La raison pour laquelle la permission du C d'assigner ouvertement via un void*(sans transtypage) est un "trou" dans le langage qui ne pouvait pas être propagé au C++ est-elle claire ?