Penser en C++

Volume 1


précédentsommairesuivant

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, const a 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 volatile qui est un proche cousin de const(parce qu'ils concernent tout deux le changement) et a une syntaxe identique.

La première motivation pour const semble avoir été d'éliminer l'usage de la directive de pré-processeur #define pour 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 :

 
Sélectionnez

#define BUFSIZE 100

BUFSIZE est 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, BUFSIZE se 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 const pour éliminer ces problèmes en déplaçant la substitution de valeur dans le domaine du compilateur. Maintenant vous pouvez dire :

 
Sélectionnez

const int bufsize = 100;

Vous pouvez utiliser bufsize partout où le compilateur doit connaître la valeur pendant la compilation. Le compilateur peut utiliser bufsize pour 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 :

 
Sélectionnez

char buf[bufsize];

Vous pouvez utiliser const pour 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 const au lieu de la substitution de valeur #define.

Constantes dans les fichiers d'en-tête

Pour utiliser const au 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 const dans un seul endroit et la distribuer aux unités de traduction en incluant le fichier d'en-tête. Un const en 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:

 
Sélectionnez

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 extern avec 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 extern indique "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 extern n'est pas indiqué dans la définition, aucun emplacement n'est alloué. Quand const est 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 const n'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 dans cette unité de compilation. Sinon, des erreurs d'édition de liens se produiraient avec les const s 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 const est à 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.

Consts de sécurité

L'utilisation du const n'est pas limitée à remplacer les #define dans 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 const de façon à ce que le compilateur donne un message d'erreur si vous essayez accidentellement de la changer. Voici un exemple :

 
Sélectionnez

//: 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 i est un const au moment de la compilation, mais j est calculé à partir de i. Cependant, parce que i est un const, la valeur calculée pour j vient encore d'une expression constante et est elle-même une constante au moment de la compilation. La ligne suivante exige l'adresse de j et force donc le compilateur à allouer un emplacement pour j. Pourtant ceci n'empêche pas l'utilisation de j dans la détermination de la taille de buf parce que le compilateur sait que j est un const et 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 const avec la variable c parce 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 c2 est calculé à partir de c et aussi que la portée fonctionne pour des const comme 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.

Aggrégats

Il est possible d'utiliser const pour 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, const signifie « 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 :

 
Sélectionnez

//: 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.

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 const en 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 const occupe toujours un emplacement et son nom est global. Le compilateur C ne peut pas traiter un const comme une constante au moment de la compilation. En C, si vous dites :

 
Sélectionnez

const int bufsize = 100;
char buf[bufsize];

vous obtiendrez une erreur, bien que cela semble une façon de faire rationnelle. Puisque bufsize occupe un emplacement quelque part, le compilateur C ne peut pas connaître la valeur au moment de la compilation. Vous pouvez dire en option :

 
Sélectionnez

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 const s, cela à un sens. C++ utilise la liaison interne pour les const s ainsi si vous voulez accomplir la même chose en C++, vous devez explicitement imposer la liaison externe en utilisant extern:

 
Sélectionnez

extern const int bufsize; // Déclaration seule

Cette ligne fonctionne aussi en C.

en C++, un const ne crée pas nécessairement un emplacement. En C un const crée toujours un emplacement. Qu'un emplacement soit réservé ou non pour un const en C++ dépend de la façon dont il est utilisé. En général, si un const est 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 const qui 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 const du C !) qui ont la liaison externe. Ainsi, si vous déclarez un const du 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 const a 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 const en C++ a la liaison interne, vous ne pouvez pas simplement définir un const dans un fichier et le mettre en référence extern dans un autre fichier. Pour donner à un const une liaison externe afin qu'il puisse être référencé dans un autre fichier, vous devez explicitement le définir comme extern, comme ceci :

 
Sélectionnez

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 :

 
Sélectionnez

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 const possè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 const n'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 oblige presque à utiliser #define du 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 const avec les pointeurs, vous avez deux options : const peut être appliqué à ce qui est pointé, ou bien const peut 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 const est 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 :

 
Sélectionnez
const int* u;

En partant de l'identifiant, nous lisons “ u est un pointeur, qui pointe vers un constint.” Ici, il n'y a pas besoin d'initialisation car vous dites que u peut 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 const de l'autre côté du int comme ceci :

 
Sélectionnez
int const* v;

Ce n'est pas du tout idiot de penser que ceci devait se lire “ v est un pointeur const vers un int.”. Toutefois, la vraie façon de le lire est “ v est un pointeur ordinaire vers un int qui se trouve être const.”. Donc, le const s'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 :

 
Sélectionnez
int d = 1;
int* const w = &amp;d;

A présent on lit : “ w est 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 :

 
Sélectionnez
*w = 2;

Vous pouvez également créer un pointeur const vers un objet const en utilisant une de ces deux syntaxes licites :

 
Sélectionnez
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 const est 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 :

 
Sélectionnez
//: 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 :

 
Sélectionnez
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 :

 
Sélectionnez
int *u = &i, v = 0;

qui crée un int* u, comme précédemment, et un int v qui 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 const parce 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- const car 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 const ance de l'objet ainsi que la promesse de sécurité faite par le const. Par exemple :

 
Sélectionnez
//: 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 const ance stricte n'est pas imposée, est le cas des tableaux de caractères littéraux. Vous pouvez dire :

 
Sélectionnez
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- const car 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 :

 
Sélectionnez
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 const pour 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 const n'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, const garantit 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 const quand vous les passez par valeur, comme dans ce cas

 
Sélectionnez
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, const prend 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érieur de 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 :

 
Sélectionnez
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 const ance 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:

 
Sélectionnez
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 via la valeur retournée.

Au premier abord, cela peut faire paraître la déclaration const dénuée de sens. Vous pouvez constater le manque apparent d'effet de retourner des const par valeurs dans cet exemple :

 
Sélectionnez
//: 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 const quand vous retournez un type prédéfini par valeur.

Retourner un const par valeur devient important quand vous traitez des types définis par l'utilisateur. Si une fonction retourne comme const un 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 :

 
Sélectionnez
//: 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 const peut être utilisée comme une lvalue. Ainsi, il est important d'utiliser const quand vous retournez un objet par valeur si vous voulez éviter son utilisation comme lvalue.

La raison pour laquelle const ne 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 X qui n'est pas const. Mais dans l'expression :

 
Sélectionnez
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 X temporaire 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 X temporaire. 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 X temporaire 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 :

 
Sélectionnez
  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 const ance 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 const que ce soit.

Le choix de retourner un pointeur ou une référence comme const dépend de ce que vous voulez autoriser le programmeur client à faire avec. Voici un exemple qui démontre l'usage de pointeurs const comme arguments de fonction et valeurs retournées :

 
Sélectionnez
//: 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 const est illicite, mais vous pouvez bien sûr copier l'information dans une variable non const ante. Le compilateur vous empêche également de créer un pointeur non- const ant 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- const comme 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 const est 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 const dans const int* const n'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 adresse passé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 :

 
Sélectionnez
//: 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 const avec les classes. Vous pourriez vouloir créer un const local dans une classe pour l'utiliser dans des expressions constantes qui seront évaluées à la compilation. Toutefois, le sens de const est différent au sein des classes, et vous devez donc comprendre les options offertes afin de créer des données membres const d'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 const ance d'un objet est plus complexe. Le compilateur peut garantir la const ance d'un type prédéfini mais il ne peut pas gérer les intrications d'une classe. Pour garantir la const ance d'un objet d'une classe, la fonction membre const est introduite : seule une fonction membre const peut être appelée pour un objet const.

8.4.1. const dans les classes

Un des endroits où vous aimeriez utiliser un const pour 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 #define pour 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 #define comme 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 const dans la classe. Ceci ne produit pas le résultat escompté. Dans une classe, const revient 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 const dans 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 const ordinaire (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 const doit ê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 const resterait 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 const dans une classe est montrée ici :

 
Sélectionnez
//: 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 :

 
Sélectionnez
//: 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 const car 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) :

 
Sélectionnez
//: 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' Integer dans main( ) est entièrement initialisé à zéro automatiquement. Cette initialisation n'est pas nécessairement plus coûteuse qu'une boucle for ou 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 const est 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 const d'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 const appelé size dans une classe qui représente une pile de pointeur vers des chaines. (44):

 
Sélectionnez
//: 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 size est 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 StringStack contient const string*. Si ce n'était pas le cas, vous ne pourriez pas utiliser un StringStack pour 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 const n'é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 enum non 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 :

 
Sélectionnez
//: 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 enum ici 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 :

 
Sélectionnez
enum { one = 1, two = 2, three };

Avec des types enum intégraux, le compilateur continuera de compter à partir de la dernière valeur, si bien que l'énumérateur three recevra la valeur 3.

Dans l'exemple StringStack.cpp ci-dessus, la ligne:

 
Sélectionnez
static const int size = 100;

deviendrait :

 
Sélectionnez
enum { size = 100 };

Bien que vous verrez souvent la technique enum dans le code ancien, static const a é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 const plutô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 const est 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 :

 
Sélectionnez
const int i = 1;
const blob b(2);

Ici, b est un objet const de type blob. Son constructeur est appelé avec l'argument 2. Pour que le compilateur impose la const ance, 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 const est 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. Dire simplement qu'une fonction membre est const ne garantit pas qu'elle se comportera ainsi, si bien que le compilateur vous force à définir la spécification const quand vous définissez la fonction. (Le const fait partie de la signature de la fonction, si bien que le compilateur comme l'éditeur de liens vérifient la const ance.) Ensuite, il garantit la const ance pendant la définition de la fonction en émettant un message d'erreur si vous essayez de changer un membre de l'objet ou d'appeler une fonction membre non- const. Ainsi on garantit que toute fonction membre que vous déclarez const se 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 const avant 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ès la liste des arguments. Par exemple :

 
Sélectionnez
//: 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 const doit ê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 i de quelle que façon que ce soit ou d'appeler une autre fonction membre qui ne soit pas const, le compilateur le signale comme une erreur.

Vous pouvez constater qu'une fonction membre const peut être appelée en toute sécurité que les objets soient const ou 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 const par 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 const et non const:

 
Sélectionnez
//: 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 const parce 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 const parce qu'elle modifie la donnée membre lastquote(cf. l'instruction return). Toutefois, lastQuote( ) ne réalise aucune modification et peut donc être const et peut être appelée par l'objet const cq en toute sécurité.

mutable : const logique vs. const de bit

Que se passe-t-il si vous voulez créer une fonction membre const mais que vous voulez toujours changer certaines données de l'objet ? Ceci est parfois appelé const de bit et constlogique(parfois également constde membre). const de bit signifie que chaque bit dans l'objet est permanent, si bien qu'une image par bit de l'objet ne changera jamais. const logique 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 const ance de bit. Pour réaliser la const ance 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 constness en 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 this est déjà un pointeur de ce type. Toutefois, dans une fonction membre const c'est en fait un pointeur const. Le transtypage permet de supprimer la const ance pour cette opération. Voici un exemple :

 
Sélectionnez
//: 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 const ance 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 const ance est contournée par transtypage, et chercher cette opération). Pour mettre les choses au clair, vous devriez utiliser le mot-clef mutable dans la déclaration de la classe pour spécifier qu'une donnée membre particulière peut changer dans un objet const:

 
Sélectionnez
//: 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 const ance de bit, plutôt que logique. Ceci est facile à voir si la const ance logique est implémentée uniquement par le mot-clef mutable, mais probablement indétectable par le compilateur si la const ance est contournée par transtypage dans une fonction membre const. En outre,

  1. La class e ou la struct ure ne doivent pas avoir de constructeur ou de destructeur définis par l'utilisateur.
  2. 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 const de 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 volatile est identique à celle de const, mais volatile signifie "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 volatile dit 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 volatile en 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 :

 
Sélectionnez
//: 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 volatile pour les données membres, les fonctions membres et les objets eux-mêmes. Vous pouvez appeler des fonctions membres volatile uniquement 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 volatile est 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 const vous 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 const partout où c'est possible) peut sauver la vie de projets entiers.

Bien que vous puissiez ignorer const et 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 const avec 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.

  1. 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).
  2. Démontrez-vous que les compilateurs C et C++ traitent réellement les constantes différemment. Créer un const global et utilisez-le dans une expression globale constante, puis compilez-le en C et en C++.
  3. Créez des exemples de définitions const pour tous les types prédéfinis et leurs variantes. Utilisez les dans des expressions avec d'autres const pour créer de nouvelles définitions const. Assurez qu'elles se compilent correctement.
  4. 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.
  5. Créez un const dont 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 const et regardez ce qu'il se produit.
  6. Créez un tableau const de char, puis essayez de changer l'un des char.
  7. Créez une instruction extern const dans un fichier, et placez un main( ) dans ce fichier qui imprime la valeur de l' extern const. Donnez une définition extern const dans un autre fichier, puis compilez et liez les deux fichiers ensemble.
  8. Ecrivez deux pointeurs vers constlong en 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.
  9. Ecrivez un pointeur const vers 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.
  10. Ecrivez un pointeur const vers 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.
  11. Supprimez le commentaire sur la ligne de code générant une erreur dans PointerAssignment.cpp pour voir l'erreur que génère votre compilateur.
  12. 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 ?
  13. Créez une fonction qui prend un argument par valeur comme const; essayez de modifier cet argument dans le corps de la fonction.
  14. Créez une fonction qui prenne un float par 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é.
  15. Modifiez ConstReturnValues.cpp en 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.
  16. Modifiez ConstPointer.cpp en 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.
  17. Faites une nouvelle version de ConstPointer.cpp appelée ConstReference.cpp qui utilise les références au lieu des pointeurs (vous aurez peut-être besoin de consulter le Chapitre 11, plus loin).
  18. Modifiez ConstTemporary.cpp en supprimant le commentaire de la ligne provoquant une erreur, pour voir quel message d'erreur votre compilateur génère.
  19. Créez une classe contenant à la fois un floatconst et non- const. Initialisez-les en utilisant la liste d'initialisation du constructeur.
  20. Créez une classe MyString qui contient un string et possède un constructeur qui initialise le string, et une fonction print( ). Modifiez StringStack.cpp afin que le conteneur contienne des objets MyString, et main( ) afin qu'il les affiche.
  21. Créez une classe contenant un membre const que 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.
  22. Dans ConstMember.cpp, supprimez l'instruction const sur 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.
  23. Créez une classe avec à la fois des fonctions membres const et non- const. Créez des objets const et non- const de cette classe, et essayez d'appeler les différents types de fonctions membres avec les différents types d'objets.
  24. Créez une classe avec des fonctions membres const et non- const. Essayez d'appeler une fonction membre non- const depuis une fonction membre const pour voir quel est le genre de messages d'erreur du compilateur que vous obtenez.
  25. Dans Mutable.cpp, supprimez le commentaire de la ligne provoquant une erreur, pour voir quel message d'erreur votre compilateur génère.
  26. Modifiez Quoter.cpp en faisant de quote( ) une fonction membre const et de lastquote un mutable.
  27. Créez une classe avec une donnée membre volatile. Créez des fonctions membres volatile et non- volatile qui modifient la donnée membre volatile, et voyez ce que dit le compilateur. Créez des objets de votre classe, volatile et non- volatile, et essayez d'appeler des fonctions membres volatile et non- volatile pour voir ce qui fonctionne et les messages d'erreur que produit le compilateur.
  28. Créez une classe appelée oiseau qui peut voler( ) et une classe caillou qui 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 ?

précédentsommairesuivant
Certaines personnes vont jusqu'à dire que tout en C est passé par valeur, puisque quand vous passez un pointeur une copie est réalisée (donc vous passez le pointeur par valeur). Aussi précis que cela puisse être, je pense que cela brouille la situation.
Au moment de la rédaction, tous les compilateurs ne supportent pas cette caractéristique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.