Vous êtes sur la page 1sur 11

C++ : Leçon 11

Opérateurs, constance et assertions


Centre Informatique pour les Lettres
et les Sciences Humaines

1 - Opérateurs arithmétiques ..................................................................................................2

Le reste de la division entière : % ............................................................................... 2


Les opérateurs dyadiques à effet : +=, -=, *=, /= et %= ............................................... 2
Les opérateurs d'incrémentation et de décrémentation : ++ et -- ............................... 3

2 - Constance .........................................................................................................................4

Constantes symboliques ........................................................................................... 5


Pointeurs sur objets constants.................................................................................. 7
Fonctions membre constantes .................................................................................. 8
Mutabilité ................................................................................................................ 8
Eloge de la constance ............................................................................................... 9

3 - Assertions .........................................................................................................................9

4 - Bon, c'est gentil tout ça, mais ça fait quand même 10 pages. Qu'est-ce que je dois
vraiment en retenir ? .......................................................................................................11

5 - J'ai rien compris, est-ce que quelqu'un d'autre pourrait m'expliquer ça un peu plus
clairement ? ....................................................................................................................11

Document du 18/03/03 - Retrouvez la version la plus récente sur http://www.up.univ-mrs.fr/wcpp/lecons.htm


C++ - Leçon 11 Opérateurs, constance et assertions 2/11

Avant de poursuivre notre exploration des possibilités offertes par le mécanisme des classes,
il est sans doute temps d'introduire un certain nombre d'éléments du langage que nous
avons jusqu'à présent négligés. Ces éléments ne sont pas conceptuellement essentiels (ce qui
explique qu'ils n'aient pas encore été présentés) mais sont (ou devraient être) d'un usage très
fréquent. Il serait, par exemple, assez difficile de comprendre le moindre exemple de code
figurant dans un livre ou une revue sans connaître les opérateurs arithmétiques "exotiques"
dont dispose C++, et l'usage des mots const et assert() est un moyen essentiel pour
essayer de garantir la qualité des programmes.
Cette Leçon ne présente pas une unité thématique très forte1, et il est possible de la voir
comme la simple juxtaposition de trois brèves Leçons n'ayant que peu de rapports entre elles.

1 - Opérateurs arithmétiques
Dans la Leçon 03, nous avons introduit les opérateurs correspondant aux quatre opérations
arithmétiques habituelles : +, -, * et /. Le langage C++ propose des opérateurs un peu moins
banals, dont l'usage se révèle parfois fort pratique.

Le reste de la division entière : %

Lorsqu'une division est effectuée entre des valeurs de type entier, elle donne un résultat entier
(cf. Leçon 2). Ainsi, après
int a = 5;
int b = 2;
l'expression (a/b) a pour valeur 2.
Dans le cas de valeurs de type entier, il arrive que l'on s'intéresse au reste de la division plutôt
qu'à son quotient. Ce reste peut être obtenu à l'aide de l'opérateur modulo, noté %. A titre
d'exemple, voici comment pourrait être définie une fonction convertissant une durée exprimée
en secondes en une durée exprimée en minutes et secondes :
1 void enMnSec(int duree, int * nbMin, int * nbSec)
2 {
3 *nbMin = duree / 60; //le quotient
4 *nbSec = duree % 60; //le reste
5 }

Les opérateurs dyadiques à effet : +=, -=, *=, /= et %=

Les cinq opérateurs arithmétiques que nous connaissons désormais permettent de combiner
deux opérandes pour obtenir un résultat. L'évaluation d'une expression ainsi formée reste, bien
entendu, sans effet sur les variables qui servent éventuellement à spécifier l'un ou l'autre des
opérandes. Lorsqu'une variable doit recevoir le résultat obtenu, il nous faut donc utiliser
l'opérateur d'affectation en plus de celui qui spécifie le calcul à effectuer, et c'est ce que nous
sommes le plus souvent amenés à faire :
index = index + 1;
Lorsque, comme dans l'exemple ci-dessus, la même variable sert tout d'abord à spécifier l'un
des opérandes puis, dans un second temps, à recueillir le résultat obtenu, le nom de cette
variable figure deux fois dans la même instruction, de part et d'autre de l'opérateur
d'affectation. Dans cette situation, il est possible d'utiliser un seul opérateur, qui indique à la
fois la nature du calcul à effectuer et le fait que le résultat obtenu doit être stocké dans la
variable. L'exemple précédent peut donc être réécrit ainsi :
index += 1; //l'évaluation de cette expression a un EFFET
L'intérêt de cette forme d'expression est qu'elle évite d'avoir à désigner deux fois la cible de
l'affectation. L'avantage peut sembler bien mince lorsque cette cible est désignée directement par

1 Au point qu'une première version s'intitulait franchement "Trucs en vrac", titre qui n'a heureusement pas survécu à

la pression académique ambiante.

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 3/11

un nom de variable, mais il devient nettement plus tangible lorsqu'on a affaire à une
expression plus complexe. Une instruction telle que
tab[ligne+k][colonne-3]->data[p] = tab[ligne+k][colonne-3]->data[p] + 1 ;
est encore plus désagréable à lire qu'à écrire, car elle nécessite un examen attentif pour vérifier
que le résultat du calcul est stocké dans la zone de mémoire qui a été utilisée pour déterminer
la valeur du premier opérande de l'addition. L'idée générale, "ajouter 1 quelque part", est bien
plus visible si l'on écrit
tab[ligne+k][colonne-3]->data[p] += 1 ;
Les cinq opérateurs arithmétiques élémentaires se doublent donc d'autant d'opérateurs
combinant calcul et affectation, et on peut écrire des choses comme
1 index += decalage; //équivaut à index = index + decalage ;
2 index -= 3; //équivaut à index = index - 3 ;
3 index *= 2 + k; //équivaut à index = index * (2 + k) ;
4 index /= 2; //équivaut à index = index / 2 ;
5 index %= 7; //équivaut à index = index % 7 ;

Ces opérateurs utilisent une notation faisant intervenir deux caractères, ce qui évoque
clairement l'idée qu'ils permettent à la fois de calculer le résultat et d'obtenir un effet
d'affectation. Il n'en reste pas moins que chacun de ces couples de caractères constitue un
symbole unique, qui désigne l'opérateur en question. Il n'est pas plus envisageable d'écrire =-
à la place de -= qu'il ne serait envisageable d'écrire wen à la place de new pour effectuer une
allocation dynamique. Attention, toutefois : si l'usage du mot wen n'a aucune chance de
passer inaperçu du compilateur (qui ne saura qu'en faire…), une ligne telle que
variable =- 3.5;
ne déclenchera pas le moindre avertissement, puisqu'elle ordonne tout simplement d'affecter
la valeur -3.5 à la variable.

Les opérateurs d'incrémentation et de décrémentation : ++ et --

L'opération consistant à ajouter (ou soustraire) un au contenu d'une variable est tellement
fréquente qu'elle fait l'objet d'une notation particulière. On écrira, par exemple
1 index++; //équivaut à index = index + 1; ou à index += 1;
2 valeur--; //équivaut à valeur = valeur - 1; ou à valeur -= 1;
L'usage habituel de l'opérateur ++ pour "passer à l'étape suivante" est à l'origine du nom du
langage qui, rappelons-le, est dérivé d'un langage plus ancien nommé simplement C…
Les opérateurs ++ et -- peuvent être placés avant la variable sur laquelle ils opèrent (on les dit
alors préfixés) ou après celle-ci (on les dit alors suffixés). Les deux instructions précédentes
pourraient donc être remplacées par
1 ++index; //équivaut à index = index + 1; ou à index += 1;
2 --valeur; //équivaut à valeur = valeur - 1; ou à valeur -= 1;
Si l'effet obtenu est le même dans les deux cas, ce n'est pas le cas de la valeur de l'expression :
lorsque l'opérateur est suffixé, l'expression a pour valeur le contenu qu'avait la variable avant
application de l'opérateur, alors que lorsque l'opérateur est préfixé, l'expression a pour valeur
celle obtenue après application de l'opérateur.
Autrement dit, lorsque l'opérateur est devant son opérande, il est appliqué avant de
déterminer la valeur de l'expression, alors que lorsqu'il n'apparaît qu'après son opérande, il
est appliqué après avoir déterminé la valeur de l'expression.
Pour les puristes : En toute rigueur, une expression utilisant l’opérateur préfixé est
équivalente à l’opérande lui-même, et non simplement à sa nouvelle valeur. Si la variable a
est de type int et contient 5, on peut donc écrire
++a *= 2 ;// a contient maintenant 12, comme si on avait écrit a = (a+1)* 2;

alors qu’il n’est pas possible d’écrire


a++ *= 2 ; //ERREUR : a++ n’est pas une lvalue

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 4/11

Bien entendu, dans les exemples précédents, seul l'effet est important, puisque aucun usage
n'est fait des valeurs obtenues à la suite de l'évaluation des expressions. Il n'en va pas
nécessairement toujours ainsi, et ces opérateurs sont (trop ?) souvent employés dans des
expressions plus complexes, où la valeur qu'ils produisent sert d'opérande à un autre
opérateur. Ainsi, par exemple, après
1 int a = 2;
2 int b = a++; //équivaut à
//int b = a ;
//a = a + 1;
la variable b contient 2, puisque la valeur de l'expression a++ est celle contenue dans a avant
incrémentation. En revanche, après
1 int a = 2;
2 int b = ++a; //équivaut à
//a = a + 1 ;
//int b = a;
la variable b contiendra 3 puisque la valeur de l'expression ++a est celle contenue dans a après
incrémentation. Bien entendu, dans un cas comme dans l'autre, la variable a contient
finalement 3, puisque l'effet de l'opérateur est le même, qu'il soit préfixé ou suffixé.
S'il est vrai que les opérateurs ++ et -- permettent parfois une concision qui favorise
l'intelligibilité du code, il existe des circonstances où il convient de s'abstenir de les utiliser.
C'est en particulier le cas des expressions où la même variable figure à plusieurs reprises :
comme la norme ISO du langage C++ n'impose aux compilateurs aucun ordre particulier
d'évaluation des sous-expressions, appliquer un opérateur d'incrémentation à une telle
variable produit des résultats imprévisibles.
C'est à dire que, même si un compilateur donné à de bonnes chances de toujours donner le
même résultat (et encore, même ceci n'est pas absolument garantit…), il est tout à fait normal
que des compilateurs différents donnent des résultats différents.
Considérons l'exemple suivant :
1 int a = 2;
2 int b = ++a + a; //b = 6, 5 ou autre chose ?
L'expression permettant de calculer la valeur d'initialisation de b est une addition faisant
intervenir deux sous-expressions. Il existe au moins deux façons d'évaluer cette addition. Soit
on commence par la gauche et l'évaluation de ++a se traduit par l'incrémentation de cette
variable et produit la valeur 3. L'évaluation de a donnera donc ensuite 3, et le résultat de
l'addition sera 6. Soit on commence par la droite et l'évaluation de a donne 2. L'évaluation de
++a donne ensuite la valeur 3, et conduit à incrémenter cette variable, mais ceci reste sans
effet sur l'évaluation de a, qui a déjà eu lieu. Le résultat de l'addition est donc 5.

N'utilisez JAMAIS les opérateurs ++ et -- dans des expressions où la variable à laquelle ils
s'appliquent apparaît plusieurs fois.

2 - Constance
Jusqu'à présent, l'information manipulée par les fonctions que nous avons écrites s'est
présentée sous l'une de trois formes : il s'agissait soit de constantes littérales (3.14, 'a' ou
true, par exemple), soit de variables, soit de zones de mémoires allouées par new et
manipulées par l'intermédiaire de pointeurs contenant leur adresse.
Les constantes littérales présentent de graves défauts, qui doivent inciter le programmeur
sérieux à les proscrire totalement du code qu'il produit. Ces défauts proviennent du fait qu'on
ne peut pas baptiser une constante littérale puisque, par définition, sa valeur apparaît
"littéralement" dans le code. Les conséquences de cet état de fait sont toutes néfastes :
La lisibilité du code est médiocre : si un prix se trouve multiplié par 1.196, il est possible qu'il
s'agisse du taux de TVA, mais il est également possible qu'il s'agisse d'une pure coïncidence et
que l'opération effectuée n'ait en fait rien à voir avec la TVA. Il est en outre possible qu'il

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 5/11

s'agisse bel et bien du taux de TVA, mais que sa valeur ne soit pas suffisamment familière au
lecteur pour que celui-ci lui attribue immédiatement sa sémantique.
Les opérations de modification du programme sont fastidieuses : si le taux de TVA change, la
mise à jour du programme implique de modifier toutes les instructions qui utilisent la
constante littérale correspondante.
Les opérations de modification du programme sont dangereuses : étant donnée l'ampleur de la
tâche, le risque est grand que certaines occurrences de 1.196 fassent l'objet d'une mauvaise
décision (c'est à dire qu'elles soient modifiées alors qu'elles n'ont rien à voir avec la TVA, ou
qu'elles soient laissées inchangées alors qu'il s'agissait effectivement du taux de TVA). Qu'une
seule erreur de ce genre soit commise, et la nouvelle version du programme donnera des
résultats faux…
Les variables et les zones de mémoire désignées par des pointeurs, pour leur part, évitent ces
inconvénients grâce au nom utilisé pour y accéder : si celui-ci est bien choisi, il permet au
lecteur de comprendre la logique des opérations effectuées, et deux valeurs qui se trouvent par
hasard identiques seront connues sous des noms différents, ce qui élimine tout risque de
confusion. Cet avantage considérable s'accompagne malheureusement d'une caractéristique
qui est souvent un inconvénient majeur : les valeurs représentées de cette façon sont
susceptibles d'être modifiées par le programme durant son exécution2. Les programmeurs
débutants ont souvent tendance à penser que cette variabilité ne peut être qu'un avantage :
après tout, il ne s'agit là que d'une possibilité, qu'ils sont libres d'utiliser lorsqu'elle présente
un intérêt et de ne pas utiliser lorsque cela ne serait pas judicieux. C'est seulement lorsque les
programmes réalisés prennent de l'ampleur, que plusieurs programmeurs collaborent au même
projet, ou qu'un programme doit être modifié plusieurs mois après qu'une première version en
a été terminée que la vérité apparaît : si une partie du programme a été conçue en supposant
une certaine quantité constante et qu'une autre partie du programme fait varier cette quantité,
les résultats produits seront très probablement faux.
Si l'on repense à l'exemple du taux de TVA, offrir à l'utilisateur la possibilité d'en modifier la
valeur n'est peut-être pas une idée fondamentalement mauvaise. Le problème est que
certains des calculs effectués avant cette modification risquent de devoir être refaits, ce qui
n'est généralement possible que si l'ensemble du programme a été conçu dans cet esprit.

Les quantités supposées constantes pendant l'exécution d'un programme ne doivent pas être
représentées d'une façon qui autorise leur modification3.

Constantes symboliques

Il est très facile d'obtenir simultanément la constance d'une valeur littérale et l'expressivité
d'un nom de variable : il suffit pour cela de créer une constante symbolique. D'un point de vue
syntaxique, cette création ressemble de très près à une définition de variable initialisée : la
seule différence est la présence, en début de ligne, du mot const.
const double TVA = 1.196; //définition d'une constante symbolique nommée TVA
Une constante symbolique possède donc, tout comme une variable, un nom, un type et une
valeur. Il existe cependant certaines différences, dont la plus évidente est bien entendu que la
valeur d'une constante (qui doit impérativement être spécifiée lors de sa définition) ne peut
jamais être modifiée. Une différence plus subtile est que, lorsque c'est possible, le
compilateur n'utilisera pas un fragment de la zone de mémoire consacrée aux données pour
stocker la valeur de la constante, mais insérera celle-ci directement dans le code, exactement
comme si le programmeur avait utilisé une constante littérale4.
La tradition veut que les noms des constantes symboliques soient capitalisés, ce qui permet de
distinguer très facilement les constantes des variables :
uneVariable = MA_CONSTANTE;

2 Un autre inconvénient est que, à la différence des constantes littérales qui sont directement incluses dans le code,

ces valeurs occupent de la place dans la mémoire réservée aux données, ce qui est moins efficace aussi bien en termes
de vitesse d'exécution qu'en termes de gestion de la mémoire.
3 Cette règle n'est qu'un cas particulier d'une loi fondamentale en matière de sécurité : "Tout ce qui ne doit pas être fait

doit être rendu impossible".


4 Ce raffinement (qui complique évidemment la tâche du compilateur) n'a généralement pas de conséquence visible sur

les performances du programme. Il a été inclus dans le langage C++ pour que les programmeurs n'aient aucune
(mauvaise) excuse pour ne pas utiliser les constantes symboliques.

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 6/11

S'il est possible de définir une constante symbolique à l'intérieur d'une fonction, cette façon de
procéder ne permet d'utiliser la constante en question que dans la fonction en question.
Lorsque, comme c'est souvent le cas, plusieurs fonctions ont besoin d'utiliser la valeur ainsi
spécifiée, on choisit souvent de faire figurer la définition de la constante symbolique en début
de fichier, avant le début de la définition de la première fonction. Cette façon de procéder rend
la constante utilisable par toutes les fonctions définies dans le fichier en question, sans qu'il
soit nécessaire d'utiliser le mécanisme de passage de paramètre pour leur en communiquer la
valeur.
1 const double TVA = 1.196; //définition d'une constante symbolique GLOBALE
//définition d'une première fonction
2 double calculeTTC(double prixHT)
3 {
4 return prixHT * TVA; //elle a accès à la constante TVA
5 }
6 //définition d'une seconde fonction
7 double calculeHT(double prixTTC)
8 {
9 return prixTTC / TVA; //elle aussi a accès à la constante TVA
10 }
Il est également possible de faire figurer les définitions des constantes symboliques dans des
fichiers .h qui feront l'objet de directives #include dans les fichiers .cpp où figurent les
fonctions qui utilisent les constantes en question. Cette façon de procéder implique la mise en
place d'une technique garantissant contre les inclusions multiples d'un même fichier .h, car il
n'est pas possible de définir plusieurs fois le même objet.
Avec Visual C++, cette protection est obtenue en insérant la directive
#pragma once

avant les définitions. Cette directive est de toute façon nécessaire lorsque le fichier .h définit
une classe, puisque la redéfinition d'une classe est tout aussi interdite que celle des
variables, des constantes ou des fonctions. Si vous examinez, par exemple, le fichier
monDialogue.h de l'un des programmes réalisés en TD, vous observerez que la directive en
question y a automatiquement été placée par Visual C++, dès la création du fichier.
Le problème des inclusions multiples d'un même fichier .h est plus délicat qu'il ne le paraît,
car la transitivité de l'inclusion conduit rapidement à des situations inextricables : imaginez
simplement deux classes A et B qui comportent chacune comme membre une instance de la
classe C. Les fichiers A.h et B.h, qui définissent respectivement les classes A et B, doivent
tous deux obligatoirement inclure le fichier C.h (qui définit la classe C), faute de quoi l'un de
leurs membres resterait d'une nature indéterminée. Jusque là, tout va bien. Mais que se
passe-t-il si une même fonction doit manipuler une instance de la classe A et une instance de
la classe B ? Le fichier dans lequel cette fonction est définie doit inclure A.h et B.h, sans quoi
le type de l'une des variables reste inconnu et la compilation échouera. Comme A.h et B.h
incluent tous deux C.h, le fichier qui définit la fonction inclut donc fatalement deux fois C.h,
et la compilation est impossible si le fichier C.h n'a pas été spécifiquement conçu pour ne
jamais redéfinir des objets qui existent déjà.
Si l'on choisit souvent de définir les constantes symboliques dans des fichiers .h, c'est parce
qu'une définition "globale" ne vaut, en fait, que dans le fichier où elle figure. Lorsqu'un projet
implique plusieurs fichiers .cpp qui comportent des fonctions utilisant une même constante, la
solution qui consiste à définir la constante dans chacun des fichiers présente un peu les
mêmes défauts que l'utilisation de constantes littérales : les modifications sont fastidieuses et
sources d'erreur. Il est alors préférable de n'avoir qu'un seul exemplaire de la définition, placé
dans un .h qui peut être inclus partout où la constante doit être définie.

A la différence d'une variable, une constante symbolique peut être rendue globale sans que cela
compromette gravement la qualité du programme. Cette globalité étant limitée au fichier où la
constante est définie, il est souvent nécessaire de la prolonger en plaçant la définition de la
constante dans un .h qui sera inclus dans tous les fichiers où la constante est utilisée.

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 7/11

Pointeurs sur objets constants

Lorsqu'un pointeur contient l'adresse d'un objet (variable ou zone de mémoire réservée par
new), ce pointeur peut être utilisé pour modifier l'objet en question. Lorsque ceci n'est pas
souhaitable, il est possible d'utiliser un "pointeur sur objet constant", plutôt qu'un simple
"pointeur sur objet". Ce mécanisme est particulièrement utile dans le cas des fonctions qui se
voient confier l'adresse d'un objet qu'elles ne sont pas supposées modifier. Si le paramètre de la
fonction est de type "pointeur sur objet constant", le programmeur qui utilise la fonction peut
avoir la certitude absolue (même s'il ignore tout du code de la fonction) que l'objet dont il
communique l'adresse ne sera pas modifié par la fonction appelée.
Imaginons par exemple qu'il existe une fonction déclarée ainsi :
double calculeMoyenne(double * tableau, int nbElements);
Le nom de cette fonction suggère bien qu'elle effectue un simple calcul qui devrait laisser
intactes les valeurs contenues dans le tableau sur lequel elle opère. Néanmoins, étant donné
que cette fonction exige qu'on lui révèle l'adresse où sont stockées ces valeurs, l'intégrité du
tableau n'est pas rigoureusement garantie. Il est en effet possible qu'une erreur subtile se soit
glissée dans la fonction et que celle-ci ne laisse pas le tableau dans l'état où elle l'a trouvé.
Cette erreur peut d'ailleurs ne pas empêcher la fonction de renvoyer une moyenne
parfaitement exacte. Nous risquons alors d'obtenir un programme dont le seul résultat
correct est cette moyenne, même si la seule erreur qu'il comporte réside précisément dans la
fonction qui la calcule. La localisation de l'erreur risque de prendre du temps…
La tranquillité d'esprit de l'auteur de la fonction, ainsi que celle de tous ceux qui seront
amenés à utiliser celle-ci, serait bien meilleure si la fonction était déclarée ainsi :
double calculeMoyenne(const double * tableau, int nbElements);
L'adresse du tableau étant maintenant contenue dans un "pointeur sur double constant", la
fonction ne peut plus (même par inadvertance) l'utiliser pour modifier l'un des éléments.
Le mot const peut indifféremment être placé avant ou après la mention du type de l'objet
pointé. Il est donc également possible d'écrire
double calculeMoyenne(double const * tableau, int nbElements);

Il faut toutefois veiller à ne pas placer le mot const après l'étoile signalant que le paramètre
est un pointeur, faute de quoi nous aurions affaire à un "pointeur constant sur double" et
non plus à un "pointeur sur double constant" : il serait alors impossible de modifier la valeur
contenue dans la variable de type pointeur, mais cette adresse permettrait tout à fait de
modifier les valeurs de type double stockées dans la zone de mémoire qu'elle désigne… Les
pointeurs constants correspondent donc à des situations très différentes de (et sans doute
plus rares que) celles auxquelles répondent les "pointeurs sur objets constants".
Faire de son premier paramètre un "pointeur sur objet constant" reste bien entendu sans
conséquence sur l'usage qui peut être fait de la fonction : il n'est pas nécessaire que l'objet
dont on lui passe l'adresse soit lui-même une constante, car la conversion de type entre
"pointeur sur objet" et "pointeur sur objet constant" est assurée automatiquement par le
compilateur5. On pourra donc écrire en toute sérénité :
1 double mesPrecieusesDonnees[] = {1, 2, 3, 4, 5};
2 int nbDonnees = sizeof(mesPrecieusesDonnees)/sizeof(mesPrecieusesDonnees[0]);
3 double laMoyenne = calculeMoyenne(mesPrecieusesDonnees, nbDonnees);
Si l'on veut rassurer le lecteur du code appelant sur l'innocuité de l'appel (et, du même coup,
se protéger contre une modification ultérieure et malencontreuse de la fonction appelée), on
peut transmettre explicitement une adresse de type "pointeur sur objet constant" :
const double * ptrProtecteur = mesPrecieusesDonnees;
double laMoyenne = calculeMoyenne(ptrProtecteur, nbDonnees);

5 La conversion inverse n'est évidemment jamais effectuée automatiquement : elle réduirait à néant la sécurité promise

par ce dispositif !

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 8/11

Fonctions membre constantes

Les fonctions membre d'une classe sont un peu dans la même position que les fonctions qui
reçoivent l'adresse d'un objet : elles ont la possibilité de modifier le contenu des variables
membre de l'instance au titre de laquelle elles sont invoquées. Lorsqu'elles ne sont pas sensées
le faire, il est possible de les déclarer elles-mêmes constantes, ce qui leur interdit de modifier
l'objet sur lequel elles opèrent. Imaginons une classe définie ainsi :
1 class CDemo
2 {
3 public:
4 int a;
5 int b;
6 int c;
7 int sommeDeAetB();
8 };
Typiquement, l'utilisation de cette classe impliquera une séquence d'instructions du genre :
1 CDemo unObjet;
2 unObjet.a = 12;
3 unObjet.b = 29;
4 unObjet.c = 0;
5 int somme = unObjet.sommeDeAetB();
Nous nous trouvons, là encore, face à une incertitude désagréable : du fait de la mission qu'elle
remplit, la fonction sommeDeAetB() ne devrait pas modifier le contenu des variables dont elle
fait la somme, ni, d'ailleurs, celui des autres variables membre de l'objet sur lequel elle opère.
Elle en a cependant le pouvoir, et nous ne pouvons donc pas être absolument certain qu'elle ne
le fait pas.
Bien entendu, si vous avez affaire à une fonction dont le code se réduit à une seule ligne et
que vous avez accès au texte source, vous pouvez arriver à être raisonnablement certain que
les variables membre sortent intactes de l'exécution de la fonction. Mais les fonctions ne sont
malheureusement pas toujours aussi simples, et le principe même des classes est qu'on doit
pouvoir les utiliser sans accéder au code définissant leurs fonctions membre.
La situation peut être rendue plus saine en privant la fonction membre d'une partie des
privilèges dont elle dispose normalement : si elle est déclarée constante, elle conservera le droit
de consulter le contenu des variables membre, mais ne sera plus autorisée à le modifier. Il
suffit pour cela que la déclaration de la classe soit légèrement modifiée :
1 class CDemo
2 {
3 public:
4 int a;
5 int b;
6 int c;
7 int sommeDeAetB() const;
8 };
Le mot const doit également apparaître lors de la définition de la fonction :
1 int CDemo::sommeDeAetB() const
2 {
// c = 4; //cette ligne provoquerait une erreur de compilation !
3 return a + b;
4 }

Mutabilité

Déclarer constantes les fonctions membre qui ne doivent pas modifier l'objet à l'aide duquel
elles sont invoquées améliore grandement la lisibilité et la sécurité d'usage des classes que
nous définissons. Il arrive parfois que certaines des variables membre d'une classe jouent un
rôle particulier et doivent rester modifiables, même par les fonctions qui ne sont pas sensées
modifier l'objet qui sert à les invoquer. Plutôt que d'exiger alors qu'aucune fonction membre ne

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 9/11

soit rendue constante (ce qui priverait totalement la classe d'un dispositif de sécurité
important), le langage permet de désigner les variables membre concernées comme étant
"mutables", ce qui signifie précisément que même les fonctions membre constantes seront
autorisées à en changer le contenu. Si une classe est définie ainsi :
1 class CSpeciale
2 {
3 public :
4 int a;
5 mutable int b;
6 void fonction() const;
7 };
sa fonction membre, bien que constante, garde un accès libre au membre b :
1 void CSpeciale::fonction() const
2 {
3 //a = 2; //interdit : la fonction est constante !
4 b = 12; //OK : cette variable est mutable
5 }
Comme le montre l'exemple ci-dessous, le statut dérogatoire d'un membre mutable ne
concerne pas seulement les fonctions membre constantes. Si une instance est déclarée
constante (4), ou est désignée par son adresse stockée par un "pointeur sur instance
constante" (7), un membre mutable restera modifiable (6 et 9) :
CSpeciale uneVariable;
uneVariable.a = 4;
uneVariable.b = 5;

const CSpeciale uneConstante = uneVariable;


//uneConstante.a = 5;//IMPOSSIBLE : uneConstante est une constante
uneConstante.b = 4; //AUTORISE : le membre b est mutable

const CSpeciale * ptrSpe = & uneVariable;


//ptrSpe->a = 36; //IMPOSSIBLE : ptrSpe est un pointeur sur objet
constant
ptrSpe->b = 36; //AUTORISE : le membre b est mutable

Eloge de la constance

L'utilisation du mot const ne permet pas d'écrire des programmes réalisant des tâches qui
seraient impossibles autrement. On pourrait donc être tenté de penser que seuls les
programmeurs masochistes l'utilisent : son unique effet semble être une augmentation de la
probabilité d'apparition d'une erreur de compilation.
C'est exactement l'effet recherché. Si le programme contient une erreur, une erreur de
compilation est INFINIMENT préférable à des résultats faux que tout le monde croit justes.

3 - Assertions
Il arrive parfois que certaines portions d'un programme ne donnent des résultats corrects (ou
même ne soient exécutables) que dans la mesure où certaines conditions sont remplies. Il est
alors clair dans l'esprit du programmeur que ces conditions sont TOUJOURS remplies lorsque
son code est exécuté. Lorsqu'une fonction est dans ce cas, il est bien entendu nécessaire de la
documenter très clairement, de façon à ce que tous ses utilisateurs soient conscients des
conditions à remplir pour obtenir les résultats souhaités. L'inconvénient de la documentation
est qu'elle n'offre une certaine sécurité que dans la mesure où :
a) elle est exacte (et à jour…)
b) elle est lue par tous les utilisateurs de la fonction
c) elle est parfaitement comprise par tous ses lecteurs.
En situation réelle, cette sécurité s'avère donc le plus souvent insuffisante. Par ailleurs, il
arrive parfois aux programmeurs de se tromper, et des conditions dont il semble évident
qu'elles sont remplies peuvent très bien ne pas l'être. Le langage C++ permet de mettre en place
des mesures de protection un peu plus rigoureuses, qui permettent de détecter pendant la

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 10/11

phase de mise au point du programme certaines situations où les conditions de


fonctionnement attendues ne sont pas remplies. A la base de ce système se trouve la fonction
assert(), qui fait partie de la librairie standard.

Comme assert() fait partie de la librairie standard et non du langage proprement dit, son
utilisation suppose la présence d'une directive d'inclusion en début de fichier :
#include "assert.h"

Le principe de la fonction assert() est très simple : elle reçoit comme paramètre une valeur
logique, et ne fait rien du tout si cette valeur est vraie. Lorsque cette valeur est fausse, en
revanche, la fonction assert() met brutalement fin à l'exécution du programme en émettant
un message qui permet de localiser la ligne de code qui l'a appelée et, par conséquent, de
déterminer rapidement quelle est la condition qui n'est pas remplie. L'exemple suivant est tout
à fait typique d'une situation où l'utilisation de la fonction assert() s'impose:
1 int tableau[] = {1,2,3,4,5};
2 int aModifier = calculeIndex();
3 tableau[aModifier]++;
Dans ce fragment de code, il est fait appel (2) à une fonction nommée calculeIndex() dont le
rôle est de déterminer auquel des éléments du tableau il convient d'ajouter un. Manifestement,
le programmeur responsable des trois lignes de code ci-dessus est absolument convaincu que
la valeur renvoyée par la fonction calculeIndex() n'est jamais ni négative ni égale (ou même
supérieure) au nombre d'éléments du tableau. Reste à savoir si tous programmeurs qui ont
contribué à la version actuelle de la fonction en question partageaient bien ce point de vue… Et
même si vous êtes le seul programmeur impliqué dans le projet, êtes-vous certain de ne jamais
avoir modifié la fonction calculeIndex() en prenant en compte un autre de ses contextes
d'utilisation, et en oubliant un peu celui-là ? Il serait bien préférable d'écrire :
1 int tableau[5] = {1,2,3,4,5};
2 int aModifier = calculeIndex();
3 assert(aModifier >= 0 && aModifier < sizeof(tableau)/sizeof(tableau[0]);
4 tableau[aModifier]++;
Bien entendu, l'utilisation d'un index calculé pour déréférencer un pointeur n'est pas le seul
cas où une assertion se justifie, et nous aurons souvent l'occasion de recourir à cette
technique dans les TDs à venir. Notez bien, toutefois que

L'assertion est une technique de détection des erreurs de programmation et non de gestion des
erreurs d'exécution.

En d'autres termes, la mise à mort de l'exécution d'un programme par la fonction assert() est
un spectacle qui ne doit JAMAIS être offert aux yeux d'un utilisateur innocent, mais doit être
strictement réservé à l'intimité du cercle des programmeurs. Les situations anormales qui
peuvent légitimement survenir pendant l'exécution d'un programme ne comportant pas
d'erreur (une disquette prématurément éjectée, par exemple) doivent faire l'objet d'une prise en
charge civilisée, offrant à l'utilisateur la possibilité de rectifier la situation et de poursuivre
ensuite l'exécution du programme.
Lorsque les tests suffisants ont été conduits, on admet que le programme est correct et on en
compile une version d'où les assertions sont retirées6 (de façon à diminuer la taille du
programme exécutable et à augmenter sa vitesse d'exécution). La version distribuée aux
utilisateurs ne comporte donc normalement aucun appel à la fonction assert(). Cette
suppression finale des assertions doit être prise en compte lorsqu'on rédige le programme : si
l'évaluation des expressions testées a un effet, celui-ci disparaîtra de la version finale du
programme, qui ne fonctionnera donc pas comme la version testée lors de la mise au point.

L'évaluation des expressions testées dans les assertions doit être dépourvue d'effet.

Une ligne telle que

6 Automatiquement, bien entendu. La procédure à suivre pour produire cette version du programme dépend du

compilateur utilisé, et sera donc examinée en TD.

J-L Péris - 18/03/03


C++ - Leçon 11 Opérateurs, constance et assertions 11/11

assert (index++ < VALEUR_MAXI); //HORREUR : l'assertion a un effet !


risque en effet de créer des problèmes lorsqu'elle sera retirée de la version finale du
programme, puisque l'augmentation de l'index qu'elle provoque n'aura alors plus lieu.
Il s'agit là d'un exemple particulièrement triste, où la méconnaissance de l'outil utilisé peut
transformer un dispositif de sécurité en source de catastrophe.

4 - Bon, c'est gentil tout ça, mais ça fait quand même 10 pages.
Qu'est-ce que je dois vraiment en retenir ?
1) On obtient le reste de la division entière grâce à l'opérateur %.
2) Les programmeurs C++ adorent utiliser les opérateurs ++, --, +=, etc, parce que ces
opérateurs n'ont pas d'équivalents dans la plupart des autres langages, et leur procurent
donc un sentiment de supériorité tout à fait délectable.
3) Ces opérateurs ne devraient être utilisés que s'ils améliorent réellement la lisibilité du code.
Lorsqu'on s'en sert pour faire le malin, on finit toujours par s'en mordre les doigts.
4) L'Annexe 5 : Quand un octet est trop grand décrit des opérateurs d'usage peu fréquent, qui
peuvent néanmoins rendre service dans certains cas particuliers.
5) Il ne faut jamais utiliser de constantes littérales dans le code.
6) On définit une constante symbolique presque de la même façon qu'une variable initialisée
(il suffit de faire précéder le type du mot const).
7) Les constantes symboliques globales ne présentent aucun inconvénient majeur dans les
projets de petite taille.
8) On peut empêcher qu'une fonction qui reçoit l'adresse d'un objet modifie cet objet en
recevant l'adresse en question dans un paramètre de type "pointeur sur objet constant".
9) On peut empêcher qu'une fonction membre modifie les variables membre de l'instance au
titre de laquelle elle est invoquée en déclarant cette fonction constante.
10) Il faut rendre constant tout ce qui peut l'être.
11) L'utilisation judicieuse des assertions améliore grandement la probabilité de détection des
erreurs de programmation.

5 - J'ai rien compris, est-ce que quelqu'un d'autre pourrait


m'expliquer ça un peu plus clairement ?
Pour les opérateurs arithmétiques, mes meilleures références restent
S.P. Harbison & G. L. Steele : C, a reference manual. Prentice Hall, 1991 (ISBN 0-13-110933-2)
ainsi que les FAQ du langage C : http://www.eskimo.com/~scs/C-faq/top.html
La façon de définir les constantes était l'un des défauts du langage C, qui a été magistralement
corrigé dans C++. Avant d'adopter les méthodes proposées dans les ouvrages traitant
exclusivement du langage C, consultez l'Annexe 4 : #define et typedef.
Ceci dit, si vous êtes arrivé jusqu'à la Leçon 11, il est sans doute plus rentable pour vous de
me poser directement les questions qui vous inquiètent que de chercher les réponses dans la
littérature…

J-L Péris - 18/03/03

Vous aimerez peut-être aussi