Pour un texte plus riche sur l'importance des constantes dans un langage OO, voir ceci.
Ce texte sera bientôt enrichi pour tenir compte du très important concept de constexpr que nous offre C++ 11. C'est une simple question de temps...
Les constantes sont méconnues et incomprises de plusieurs informaticien(ne)s. Pourtant, il s'agit d'un élément important et précieux de la boîte à outils des programmeuses et des programmeurs.
Décrit simplement, une constante est un objet dont l'état ne peut plus changer une fois son caractère constante spécifié. La portée de cet énoncé varie d'un langage à l'autre :
J'ai entendu, ces dernières années, des étudiant(e)s dire « je vais ajouter mes constantes quand j'aurai fini d'écrire mon programme »... Quelle mauvaise idée!
On ne met pas des constantes dans un programme pour faire plaisir au professeur mais bien pour simplifier notre propre tâche de programmation. Les constantes sont, avec les commentaires et les types, parmi les premières choses à déterminer dans un programme!
Je propose dans l'immédiat une maxime : si un objet peut être qualifié constant, alors il devrait l'être. Au pire, ça ne coûtera rien. Au mieux, ça documentera le rôle de l'objet dans le code et ça donnera au compilateur une opportunité d'optimisation supplémentaire.
Vous noterez que la constance se gagne sans problème (on peut utiliser un X là où un X constant est requis puisque gagner en constance ne comporte aucun risque) alors qu'elle ne se perd qu'avec peine (en C++, il faut avoir recours à un const_cast pour enlever temporairement une qualification de constance sur un objet, opération qu'il ne faut pas prendre à la légère).
Cet article se veut un bref résumé du rôle des constantes dans un programme. J'utiliserai C++ comme référence parce qu'il permet de couvrir (beaucoup) plus de cas que la plupart des autres langages (C, Java, les langages .NET, etc.).
Une première catégorie, classique et supportée par la plupart des langages de programmation (du moins dans le cas des types primitifs), est celle des constantes dont la valeur est connue à la compilation.
Un bon compilateur reconnaîtra habituellement le fait que l'entité marquée constante et dont la valeur est connue dès la compilation ne pourra pas changer d'état et brûlera souvent la valeur de la constante dans le code compilé. Utiliser des constantes permet alors à la programmeuse ou au programmeur de travailler de manière symbolique, par exemple en parlant NELEVES plutôt que du littéral 30 pour une classe de 30 élèves. Les constantes clarifier aussi le texte du programme et simplifient l'entretien de code : si le nombre d'élèves passe de 30 à 35, il suffit de changer la valeur de la constante et de recompiler. Si, au contraitre, le littéral 30 avait été apposé manuellement ici et là dans le code, il faudrait se demander pour chaque occurrence du littéral 30 s'il s'agit bel et bien du nombre d'élèves ou s'il ne s'agit pas d'une autre information ayant la valeur 30. |
|
Les constantes connues à la compilation :
Évidemment, la conséquence de marquer un objet comme étant constant est que le programme s'engage à ne pas en modifier l'état par la suite.
Il faut être pragmatique : certains objets voudront permettre, souvent à des fins de performance ou de statistiques, à certains de leurs attributs d'échapper à la contrainte de constance. Ces attributs pourront alors être qualifiés du mot clé mutable. Ce détail n'est pas essentiel à une discussion simple comme celle-ci.
Notez que, dans les exemples de déclarations de constantes plus haut, celles qui sont de types non primitifs (SEUIL_PASSAGE et NOM_DEFAUT) sont des objets constants. En tant que tels, ils doivent être construits et ils doivent être détruits.
Un objet constant est considéré constant suite à sa construction et jusqu'au début de sa destruction. Pendant la construction de l'objet (donc pendant son initialisation), il est évidemment possible de le modifier, sinon il n'y aurait pas d'objet. De même, pendant sa destruction, il est possible de le modifier sinon l'objet ne pourrait pas libérer les ressources qu'il s'est attribué pendant son existence.
La règle de constance sur un objet constant est qu'on ne peut ni modifier ses attributs, ni invoquer sur lui une méthode qui n'en garantisse pas la constance. Nous y reviendrons plus bas.
Imaginons le cas proposé à droite. Trois options de menu sont identifiées dans un programme par des constantes dont les valeurs se succèdent. Imaginons que ce soit un choix de design, au sens où l'ajout de nouvelles options devrait se faire à la suite des valeurs existantes (selon cette stratégie, ajouter l'option MENU_GAUCHE devrait impliquer de lui associer la valeur 3 qui succède directement à la valeur de MENU_RECULER). |
|
Maintenant, imaginons que l'on veuille plus de souplesse, par exemple parce qu'on estime important de pouvoir commencer les options de menu à 1 plutôt qu'à 0.
Dans les circonstances, c'est très simple à réaliser : il suffit de faire en sorte que les valeurs associées aux options de menu soient calculées à partir des valeurs d'autres constantes elles aussi connues à la compilation. Ce faisant, on gagne de la souplesse (changer la valeur de MENU_BASE change aussi les valeurs de toutes les constantes qui en dépendent) sans que cela ne réduise la clarté du code (on utilise les mêmes symboles, après tout) et sans que cela ne coûte quoi que ce soit en temps ou en espace dans le programme (MENU_BASE + 2, par exemple, est un calcul fait sur deux constantes dont les valeurs sont connues à la compilation, ce qui implique que ce calcul sera fait à la compilation). Évidemment, dans un tel cas, utiliser une énumération devient un choix logique :
|
|
Le programme proposé à droite, et composé des fonctions lire() et afficher_inverse(), utilise une constante NELEMS dont la valeur est connue à la compilation et fixée à l'intérieur du programme principal; de même, les deux fonctions appelées profitent du fait que cette taille (appelée N dans ces fonctions) soit connue dès la compilation pour générer des algorithmes sécuritaires, sans risque de débordement. Le type de NELEMS est primitif, et la valeur par laquelle NELEMS est initialisé ne dépend d'aucun événement dû à l'exécution du programme (il s'agit d'un simple littéral). Dans ce cas, un compilateur est en droit de brûler la valeur du littéral dans le code là où apparaît le symbole NELEMS. Encore une fois, le recours à une constante n'a vraiment que des avantages. Remarquez que même dans un petit programme comme celui-ci, les symboles modélisant des constantes apparaissent à plusieurs endroits. Cela signifie que ne pas utiliser de constantes symbolique et nous limiter à utiliser des littéraux nous aurait forcé à faire de nombreux changements dans le code si nous avions décidé de ne plus lire et afficher le même nombre de valeurs. Les risques d'oublier l'une des occurrences de ce littéral sont grands, surtout à moyen terme. Il n'y a pas de bonne raison de ne pas utiliser de constantes lorsque cela s'y prête. |
|
Avec les objets, une petite nuance s'impose.
Puisque les objets voient leur construction et leur destruction gérée par programmation (les programmeuses et les programmeurs déterminent ce que signifie construire et ce que signifie détruire un objet, et ce pour chaque classe), le compilateur ne peut pas brûler la valeur de l'objet dans le code dans un cas comme celui de NOM_GENERIQUE proposé à droite. En effet, il est possible que le code comptabilise le nombre d'appels à identifier_generique() en incrémentant une variable dans le constructeur de NOM_GENERIQUE (c'est peu probable avec la classe std::string puisque celle-ci appartient à la bibliothèque standard du langage mais c'est très possible avec des instances de classes prises au sens large). |
|
Si le compilateur décidait de ne pas construire puis détruire NOM_GENERIQUE à chaque appel de identifier_generique(), cette décision pourrait nuire au bon fonctionnement du programme.
Il est toutefois possible d'indiquer au compilateur notre souhait de ne voir la constante NOM_GENERIQUE construite qu'une seule fois, soit lors de la première invocation du sous-programme identifier_generique(). Cette spécification se fait en apposant la mention static avant (ou après, peu importe) le mot const. Notez que cela n'a de sens que si l'état initial de la constante NOM_GENERIQUE dans identifier_generique() est la même peu importe l'appel du sous-programme. Nous verrons plus bas des cas où cela ne s'appliquerait pas. |
|
On obtient ainsi le meilleur des mondes :
Un tableau construit de manière automatique (tableau local à un sous-programme) ou statique (tableau global) doit avoir une taille entière, strictement positive (donc plus grande que zéro) et connue à la compilation. Un exemple proposé à droite montre au moins un cas où cette dernière règle intervient. La constante SURPRISE est entière, constante dès sa déclaration et peut-être même positive, mais sa valeur dépend d'une intervention humaine et n'est donc pas connue à la compilation. Pour cette raison, la déclaration du tableau tabC de taille SURPRISE est une déclaraiton illégale. Le compilateur de sait pas quoi faire avec tabC parce qu'il n'en connaît pas la taille au moment où il génère le code. |
|
Nous reviendrons un peu plus loin sur le recours à un nom en majuscules dans un tel cas qui est un peu abusif.
En POO, il est possible de définir des constantes de classes. Ceci permet de regrouper ensembles une idée, par exemple celle de cours, et les idées associées, par exemple le nombre d'élèves et un tableau d'élèves.
Dans presque tous les cas, une constante de classe est déclarée dans la déclaration de sa classe puis définie dans un fichier source adjoint. L'exemple proposé à droite est celui d'une classe Personne dans laquelle apparaît une constante de classe privée nommée NOM_DEFAUT. Le fichier d'en-tête Personne.h déclare la classe Personne et indique l'existence de la constante (son type et son nom), alors que le fichier source Personne.cpp définit (construit) cette constante en sollicitant l'un des ses constructeurs (remarquez que le mot clé static n'apparaît que lors de la déclaration). Il est important de définir un attribut de classe comme la constante Personne::NOM_DEFAUT dans un seul fichier source. C++ procède par compilation séparée des fichiers sources, et l'édition des liens lie les divers fichiers résultant de cette compilation dans un tout qui se veut cohérent. Si la définition d'un attribut de classe apparaissait dans le fichier d'en-tête, alors comment déterminerait-on lequel des fichiers source est responsable de définir effectivement cet attribut? Ceci change avec C++ 17 et l'avènement des variables inline, qui permettent désormais d'écrire simplement ceci :
|
|
|
Pensez-y :
Lequel d'entre A.obj et B.obj devrait contenir la définition de la constante Personne::NOM_DEFAUT? La réponse est qu'aucun n'est plus qualifié que l'autre en ce sens, or si cette constante était définie dans Personne.h, alors A.cpp et B.cpp seraient tous deux aussi conscients des règles s'appliquant à sa contruction.
En pratique, on déclare donc les attributs de classe (ceux qualifiés static) dans la déclaration d'une classe (le .h dans la plupart des cas) et on les définit dans un fichier source (.cpp) associé à la classe. Le cas ci-dessus devient donc :
Ceci nous mène au cas des attributs d'instances qui sont des tableaux. Comme toujours, certaines contraintes s'appliquent :
Il faut comprendre ici que C++, comme C, a recours à la compilation séparée de chaque fichier source. Les fichiers qui incluront Cours.h sauront qu'il y existe une constante entière nommée NELEVES et sauront que chaque instance de Cours contiendra un tableau de NELEVES instances de Eleve, mais ne sauront pas combien d'espace réserver pour une instance de Cours puisque l'espace requis dépendra de la taille d'un Cours, qui sera au moins NELEVES*sizeof(Eleve), or la valeur de NELEVES leur est inconnue! |
|
|
Pour cette raison, plusieurs compilateurs acceptent une dispense particulière pour les constantes de classe entières (et seulement elles; cette dispense ne s'applique pas aux autres types, même primitifs) qui peuvent être définies à même la déclaration de la classe.
L'exemple à droite est donc légal, du moins sur un nombre important de compilateurs. Une solution plus ancienne (et plus portable) pour contourner cet irritant technique est de remplacer une constante entière par une constante énumérée. |
|
Ainsi, dans la classe Cours proposée à droite, on aurait pu remplacer la définition static const int NELEVES = 30; par la définition enum { NELEVES = 30 }; pour obtenir exactement le même résultat. On pourrait d'ailleurs toujours procéder ainsi dans le cas des constantes entières, ce que certains (dont moi) font régulièrement parce que la formule résultante est plus succincte et plus compacte.
Il n'y a aucun avantage à l'une des formules en comparaison avec l'autre dans les cas des entiers. Le résultat est strictement équivalent en taille ou en vitesse ou en lisibilité (les symboles sont les mêmes dans le code et se nomment dans chaque cas de la même manière, soit Cours::NELEVES dans notre exemple). La préférence pour l'une ou l'autre de ces formes est d'ordre esthétique.
Vous aurez remarqué que les constantes utilisées jusqu'ici dans cet article ont toutes des noms écrits en majuscules.
Ce choix est volontaire et respecte la tradition. La plupart des constantes système de C et de C++ sont écrites en minuscules, et utiliser dans nos progammes des noms en majuscules réduit les risques de conflits. De même, les constantes sont des indicateurs importants dans un programme et les rendre visibles est une bonne chose.
Vous remarquerez aussi que je n'appliquerai plus cette politique de manière systématique plus bas. La raison est que nous allons opérer un glissement sémantique en passant de constantes dont la valeur est connue à la compilation à des constantes dont la valeur est fixée à l'exécution.
Les deux catégories de constantes partagent une caractéristique fonamentale : elles ne peuvent changer d'état une fois qualifiées constantes. Elles ne partagent toutefois pas la caractéristique d'être connues lorsque le programme est compilé, et ne peuvent pas jouer le même rôle dans un programme.
Par exemple, pour déclarer un tableau de manière automatique dans un sous-programme (en tant que variable locale), le compilateur doit connaître la taille du tableau à la compilation pour lui réserver un espace suffisant. Une constante entière dont la valeur est connue à la compilation permet cela; une constante entière dont la valeur n'est pas connue à la compilation ne le permet pas (par définition).
J'utiliserai donc les majuscules pour les constantes statiques, dont la valeur est fixée dès la compilation d'un programme, mais pas pour les constantes dynamiques, dont la valeur initiale dépend de l'exécution du programme mais demeure fixée à partir de sa déclaration.
Moins connues que les constantes de classes mais parfois fort sympathiques : les constantes d'instances. La différence entre les deux va comme suit :
La valeur d'une constante d'instance est fixée à la construction. Ces constantes n'ont donc pas des valeurs connues à la compilation des programmes et ne peuvent pas servir pour déterminer la taille d'un tableau sans avoir recours à de l'allocation dynamique de mémoire. De même, sachant que les attributs d'une instance doivent être construits avant que l'instance elle-même ne le soit, il importe que les constantes d'instances soient initialisés par préconstruction, donc avant l'accolade ouvrante du constructeur de l'instance à laquelle elles appartiennent. En effet, une fois l'accolade ouvrante du constructeur atteinte, les attributs sont construits et, si un attribut est construit et est qualifié constant, alors on ne peut plus en changer l'état. À droite, j'ai écrit le constructeur de copie à titre illustratif, mais pour une classe comme celle-ci, il serait préférable de laisser le compilateur générer implicitement ce constructeur. |
|
En Java, une constante d'instance doit avoir une valeur fixée (une seule fois!) à l'intérieur du constructeur de l'instance à laquelle elle appartient, mais il n'existe pas de mécanique spécifique pour la préconstruire.
Remarquez qu'utiliser une constante d'instance restreint certaines opérations sur l'objet qui la possède. Entre autres, il est imposible de modifier cet état de l'objet, ce qui implique que l'opérateur d'affectation par défaut, généré par le compilateur si la programmeuse ou le programmeur n'en a pas suppléé(e) une lui-même ou elle-même, ne sera pas opérationnel puisque, par défaut, il cherche à réaliser une affectation membre à membre des attributs, chose illégale par définition sur un membre constant.
Il peut arriver qu'un sous-programme utilise une constante locale qui soit fixée pour la duré de l'invocation du sous-programme mais qui ne soit pas connue à la compilation, par exemple dans le cas où elle dépendrait de la valeur d'un paramètre.
Dans un tel cas, clairement, il ne faut pas que la constante locale soit qualifiée static puisqu'on veut que sa valeur initiale soit fixée lors de l'invocation, pas avant, et ce à chaque invocation. Considérant que la valeur devra être évaluée à chaque appel et ne pourra donc pas être brûlée dans le code, pourquoi ne pas se limiter à une variable ici? Pourquoi se préoccuper d'une constante? |
|
Pour plusieurs raisons :
Il est aussi possible d'utiliser des paamètres constants. Un trouve trois catégories de tels paramètres, soit les paramètres constants par valeur, les paramètres constants par référence et les paramètres constants par adresse.
Vous remarquerez que les noms des paramètres constants ne sont habituellement pas écrits strictement en majuscules, respectant plutôt les usages habituels pour les noms de variables. L'idée derrière cette façon de faire est que les paramètres constants sont surtout des objets sur lesquels le progamme, pour une courte période (la durée du sous-programme), s'engage à ne pas modifier quelque état que ce soit.
Le compilateur garantit le respect de cet engagement en refusant de compiler le sous-programme si le contrat n'est pas respecté. De même, le compilateur peut profiter de cet engagement pour procéder à un certain nombre d'optimisations agressives; en toute honnêteté, cela dit, la plupart n'en profitent pas.
Les paramètres par valeur constants jouent le même rôle que les constantes locales connues à l'exécution : ce sont des indications au compilateur que la valeur d'un paramètre ne sera pas modifiée lors de l'exécution d'un sous-programme.
Un cas simple est proposé en exemple à droite. La borne minimale (paramètre min) du sous-programme afficher_valeurs() est une variable, utilisée par la répétitive à l'intérieur du sous-programme à titre de compteur, alors que la borne maximale (paramètre max) est constante, indiquant au compilateur qu'il est en droit de procéder à toute forme d'optimisation reposant sur le caractère fixe de sa valeur. Remarquez que le paramètre max porte un nom combinant majuscules et minuscules ici, respectant les usages pour les noms de variables. Il s'agit en effet d'une variable dont la valeur est fixée pour la durée de l'invocation du sous-programme (la programmeuse ou le programmeur spécifie le paramètre const pour s'engager à ne pas en modifier la valeur et pour permettre certaines optimisations) plutôt que d'une constante dont la valeur serait connue à la compilation. |
|
Plusieurs compilateurs négligent d'exploiter ce potentiel d'optimisation, ce qui mène certains experts à négliger de spécifier constants les paramètres par valeur qui s'y prêteraient, mais souvenez-vous que la rigueur, ici, ne peut pas nuire : au pire, vous ne gagnez rien mais vous ne perdez rien non plus alors qu'au mieux, vous gagnez.
Notez aussi que le compilateur ne peut faire la différence entre un sous-programme dont les paramètres sont passés par valeur et un autre dont les paramètres sont passés par valeur et sont constants (donc int f(int); et int f(const int); ne poeuvent être distingués l'un de l'autre par un compilateur... Pensez-y!). Évitez l'ambiguïté et ne cherchez pas à distinguer deux sous-programmes sur cette base.
Une donnée d'un type primitif est habituellement de petite taille et peut être copiée sans coût réel lorsqu'on la passe en paramètre à un sous-programme. En retour, copier un objet est une opération de complexité arbitrairement grande puisque l'action de copier un objet implique un appel de sous-programme (son contructeur de copie) pour créer la temporaire résultante et un autre appel de sous-programme (son destructeur) pour l'éliminer.
Il y a un coût caché dans cette paire d'opérations qui peut être très élevé si copie un objet (et détruire la variable temporaire résultante) s'avère être une opération coûteuse. Ainsi, chaque appel au sous-programme f0() dans l'exemple à droite impliquera appeler le constructeur par copie de X (par X::X(const X&)) pour créer le X local à f0() et un appel à son destructeur (X::~X()) lorsqu'il faudra détruire cette variable. Passer cet objet par référence éliminerait le coût de la copie (une référence, vue de près, est simplement une adresse) et accélérerait ainsi les invocations au sous-programme en question mais la conséquence de ce geste est une réduction de la sécurité puisque l'objet passé en paramètre peut alors être modifié par le sous-programme appelé à l'insu du sous-programme appelant. C'est la situation qu'on rencontre par défaut avec Java et les langages .NET et qui rend délicate la tâche d'écrire des programmes sécuritaires avec ces langages. |
|
En retour, f2() combine sécurité et rapidité : son paramètre est une référence sur un X const, ce qui signifie que le X original ne sera pas copié à l'invocation de f2() (le paramètre étant une référence) et que le X original ne risquera pas d'être modifié par l'exécution de f2() (la référence étant constante).
C'est pourquoi, si un sous-programme ne compte pas modifier une donnée dont le type n'est pas primitif, il est pratiquement toujours préférable de la passer par référence constante plutôt que de la passer par valeur. Évidemment, si un sous-programme compte modifier la donnée, la passer par référence tout court reste la chose à faire.
Petite parenthèse : pour les types primitifs, le passage par valeur (constante ou non) reste à privilégier. Utiliser une référence implique accéder indirectement au référé, et pour un type primitif cela ralentira inutilement l'exécution du code. Uune référence est (au pire!) une adresse, donc quelque chose d'à peu près aussi gros qu'un int, donc une référence n'est pas passée plus rapidement en paramètre que ne l'est un simple entier ou un nombre à virgule flottante. Dans les exemples à droite, les deux meilleurs candidats sont g1() et g2() (selon les besoins). |
|
Autre petite parenthèse : bien que passer un objet en paramètre par référence-vers-const soit habituellement plus efficace que de le passer par valeur, les deux options restent possibles. Le système de types de C++ demeure très homogène.
Les paramètres par adresse constants existent à la fois en C et en C++. L'idée d'un paramètre par adresse constant est de permettre de manipuler une indirection tout en offrant une garantie de non-modification.
L'exemple type est la fonction strcpy() de la bibliothèque <string.h> du langage C, placée dans l'espace nommé std et dans la bibliothèque <cstring> en C++. Cette fonction prend deux paramètres :
La fonction copie chaque byte (chaque char) de src vers dest jusqu'à un octet de valeur 0 (ou '\0', ce qui est identique), la copie du dernier octet incluse. La valeur 0 sur un byte sert de délimiteur dans la tradition des chaînes ASCIIZ du langage C. La fonction strcpy() laisse au code client la responsabilité d'allouer au préalable pour dest un espace suffisant pour recevoir les données de src. C'est rapide et brutal. |
|
En examinant l'une ou l'autre des formes de cette fonction, on remarquera effectivement que *dest (là où pointe dest) se fait affecter des valeurs alors que *src (là où pointe src) ne sert qu'en lecture (à droite des affectations). En fait, on utilise un pointeur temporaire (non constant) nommé p pour itérer à partir de dest parce que la convention est que strcpy() retourne l'adresse de destination suite à la copie des données.
Remarquez que la protection offerte par une adresse constante est limitée : ici, si src et dest se chevauchent, écrire dans *dest risque de modifier la zone où mène src. Cela fait partie des risques de code reposant sur des pointeurs, jouet puissant mais dangereux s'il en est un.
En fait, la forme de pointeur constant vers une donnée que je connais le plus sert à implémenter... des références, qui ne sont essentiellement que ça – une référence est un pointeur sur lequel l'artihmétique de pointeurs est illégale.
Remarquez aussi qu'incrémenter un pointeur vers une donnée constante n'est pas illégal puisque cela déplace le pointeur sans changer la valeur du pointé. Il est toutefois possible, en déplaçant le mot const, de spécifier p est un pointeur constant vers une donnée (p. ex. : char * const p) ou de spécifier p est un pointeur constant vers une donnée constante (p. ex. : const char * const p), mais la forme p est un pointeur vers une donnée constante (p. ex. : const char *p) est la plus fréquente et la plus utile en général.
Élément très important pour un système de types OO homogène : les méthodes const, qui sont toujours des méthodes d'instance. Sur le plan conceptuel, une méthode const garantit qu'elle laissera son instance intacte de par ses actions. Sur le plan syntaxique, une méthode constante est qualifiée const suite à la parenthèse fermante de sa liste de paramètres (voir valeur(), à droite). Techniquement, qualifier une méthode avec const implique rendre *this constant pour la durée de l'invocation de cette méthode. Une méthode const garantit que son action ne changera rien à l'état de l'instance à laquelle elle appartient. Par définition, donc :
|
|
En retour, plusieurs méthodes sont typiquement const :
Certains cas, comme l'opérateur [] par exemple, se déclinent souvent de manière à la fois const et non-const pour une même classe :
#include <iostream>
template <int N>
class Tableau {
int vals[N]{};
public:
Tableau() = default;
int& operator[](int n) { // pas const
return vals[n];
}
int operator[](int n) const {
return vals[n];
}
};
template &int N>
void remplir(Tableau<N> tab, int val) {
for(int i = 0; i ! N; ++i)
tab[i] = val; // version non-const (tab est non-const)
}
template &int N>
void afficher(const Tableau<N> tab, int val) {
for(int i = 0; i ! N; ++i)
std::cout << tab[i] << ' '; // version const (tab est const)
}
Les méthodes const sont nécessaires à un système de types homogène si celui-ci admet des objets. Sur un objet const, seules les méthodes const peuvent être utilisées. Ainsi, si la programmeuse ou le programmeur d'une classe donnée néglige de qualifier const les méthodes qui devraient l'être, alors il devient impossible de manipuler une instance constante de cette classe. Le code devient inefficace ou tout simplement inutilisable.
La position du mot const dans une signature de méthode ou de fonction est importante. Dans l'exemple ci-dessous (où le type interne et public str_type, correspondant à std::string, est défini pour alléger l'écriture) :
|
|