Améliorez votre code avec const
Le mot-clé const est malheureusement très peu utilisé par les programmeurs, soit par oubli, soit parce qu'ils jugent ce mot-clé inutile ou encore parce qu'ils ne connaissent pas les bienfaits qu'il procure. Pourtant, il devrait être considéré comme un réel atout du C++ parce qu'il rend le style de programmation meilleur : plus rapide, plus robuste, plus sécurisé, etc.
Ce mot-clé n'est pas là par hasard, l'utiliser va vous apporter de nombreux avantages, allant de la phase de conception au débogage. Le débogage sera simplifié, il devient alors inutile de suivre l'évolution des variables marquées const puisqu'elles ne changent pas, donc on peut se concentrer sur les autres. Utiliser const est une méthode extrêmement sûre : le compilateur garantit que cette propriété restera toujours vraie en vous indiquant lors de la compilation si vous tentez de faire une modification accidentelle d'une variable notée const. Sa signification dépend du contexte dans lequel il est utilisé : variable ou classe mais vous vous doutez bien qu'il indique que l'objet auquel il se réfère ne peut pas être modifié.
Variable constante
Cette partie s'applique aux variables ainsi qu'aux paramètres de fonction. Le mot-clé const s'utilise dans la déclaration de la variable. Contrairement aux autres variables, les constantes doivent être initialisées au moment de leur déclaration, ce qui est logique puisqu'elles ne peuvent être modifiées, on doit leur donner leurs valeurs initiales au moment de la déclaration.
Syntaxe
Voici un exemple illustrant la notion de constante. Ici, il s'agit du type primitif int, mais la notation est la même avec un objet. Les erreurs ont volontairement été mises en commentaire, et notées erreur.
#include <cstdlib> #include <iostream> void func1(const int &n) { // seules des méthodes constantes peuvent être appelées sur n puisqu'il est constant. std::cout << n << std::endl; // affiche n } void func2(int &n) { n++; // incrémente n, qui est passé par référence et non constant. } int main(int argc, char **argv) { int x = 3; // une variable locale qui peut être modifiée. x = 5; const int y = 9; // une variable const doit être initialisée au moment de sa définition. //y = 10; // erreur : elle ne peut pas être modifiée. int const z = 10; // autre notation pour la définition d'une variable const. func1(z); //func2(z); // erreur : elle ne peut pas être modifiée. return EXIT_SUCCESS; }
L'exemple suivant montre comment gérer les constantes membres ou statiques à l'intérieur d'une classe. Les constantes membres doivent être initialisées dans la liste d'initialisation du constructeur. En revanche, les constantes statiques de type autre que int doivent être initialisées dans le fichier d'implémentation. En plus de garantir la constance d'une variable, un autre avantage du mot-clé const est de permettre un accès public en lecture seule à la variable sans nécessiter la création d'un accesseur.
// fichier MaClasse.h #ifndef MA_CLASSE_H #define MA_CLASSE_H #include <string> class MaClasse { public: // cette constante est accessible partout via MaClasse::staticInt. // seul, static const int peut être initialisé lors de sa définition. static const int staticInt = 10; // idem à staticInt mais doivent être initialisés dans le fichier // d'implémentation MaClasse.cpp. static const double staticDouble; static const std::string staticString; MaClasse(); // constructeur ~MaClasse(); // destructeur private: // constantes propres à chaque instance. doivent être initialisées dans la // liste d'initialisation du constructeur, et dans le même ordre que celui // de leurs définitions. const int memberInt; const std::string memberString; }; #endif // fichier MaClasse.cpp #include "MaClasse.h" // initialisation des membres staticDouble et staticString. const double MaClasse::staticDouble(2.0); const std::string MaClasse::staticString("titi"); // initialisation des constantes membres dans la liste d'initialisation du constructeur. MaClasse::MaClasse() : memberInt(4), memberString("toto") { } MaClasse::~MaClasse() { }
Exemples de bonne pratique
On veillera à toujours utiliser les variables constantes à la place des #define MA_VAR 10 car le compilateur ne peut pas faire la vérification de type avec les macros. Concernant les variables, on prendra pour habitude d'utiliser const si l'on sait qu'une variable ne sera pas modifiée, ou si l'on ne souhaite pas qu'elle soit modifiée accidentellement, le compilateur vous avertira alors si vous n'avez pas respecté ce const en générant une erreur au moment de la compilation.
void printVector(const std::vector<int> &v) // nous n'avons pas besoin de modifier le vecteur, donc référence constante. { // cette variable contient la taille du vecteur, elle n'a pas besoin d'être changée. const int size = v.size(); std::cout << "Le vecteur contient " << size << "élément" << ((size > 1) ? "s" : "") << std::endl; // ... quelques lignes intermédiaires int i = 0; while (i < size) { std::cout << "[" << i << "] : " << v[i] << std::endl; ++i; } }
Attention aux pointeurs
Il est possible d'utiliser const avec les pointeurs. Mais la position du const dans la déclaration peut radicalement changer sa signification. L'initialisation des pointeurs membres doit être faite dans la liste d'initialisation, donc ces initialisations s'accompagnent de l'instruction new pour allouer la mémoire nécessaire.
// fichier MaClasse.h #ifndef MA_CLASSE_H #define MA_CLASSE_H class MaClasse { public: MaClasse(); // constructeur ~MaClasse(); // destructeur void func(const BigObject *obj); void func2(const AnotherBigObject &obj); private: // pointeur vers un entier. int *pInt; // pointeur vers un entier constant. l'entier ne peut être changé, // en revanche, l'adresse du pointeur peut l'être pour pointer sur un autre // entier. const int *pConstInt; // pointeur constant sur un entier. l'entier peut être changé, mais // le pointeur (son adresse) ne peut pas être changé. int * const constPInt; // pointeur constant sur un entier constant. on ne peut ni modifier // l'entier pointé, ni le pointeur (son adresse). const int * const constPConstInt; }; #endif // fichier MaClasse.cpp #include "MaClasse.h" MaClasse::MaClasse() : pInt(0), // toujours initialiser les pointeurs à 0 // c'est ici que l'on doit donner leurs valeurs aux constantes pConstInt(new int(8)), // celui-ci peut être initialisé dans le constructeur car ce n'est pas un pointeur constant. constPInt(new int(4)), constPConstInt(new int(45)) { pInt = new int(5); // allocation. *pInt = 10; // réaffectation via le déréférencement. //*pConstInt = 6; // interdit car l'entier est constant. // on peut en revanche changer son adresse : delete pConstInt; // autorisé car on change l'adresse du pointeur en lui en donnant // une nouvelle. ne pas oublier le delete pour éviter les fuites mémoires. pConstInt = new int(6); *constPInt = 34; // autorisé car c'est le pointeur qui est constant. //constPInt = new int(5); // interdit car pointeur constant. //*constPConstInt = 5; // interdit //constPConstInt = new int(6); // interdit aussi } MaClasse::~MaClasse() { delete pInt; delete pConstInt; delete constPInt; delete constPConstInt; } // obj est un pointeur sur un objet constant, on est donc certain que le BigObject // passé en paramètre ne peut être modifié dans la fonction. void MaClasse::func(const BigObject *obj) { // toujours tester les pointeurs if (obj) { int val = obj->getValue(); //... } } // autre moyen efficace de passer un gros objet, par référence. void MaClasse::func2(const AnotherBigObject &obj) { // la référence ne peut être nulle... int val = obj.getValue(); }
Enfin, notez que const s'applique toujours à l'élément situé à sa gauche. Par exemple, int const n; indique qu'il s'agit d'un entier constant ou que int * const x; est un pointeur constant sur un int. Si rien n'est à gauche de const, alors const se réfère à l'élément qui est à sa droite. Ainsi, const int x; est équivalent à int const x;.
Fonctions constantes
Utiliser const à la fin de la signature d'une fonction membre permet d'indiquer qu'elle ne modifie pas le contenu de la classe dans laquelle est elle déclarée. La constance fournie par const est contagieuse. Cela signifie que l'on ne peut pas passer une variable constante à une fonction qui n'accepterait pas un paramètre constant, que l'on ne peut pas appeler une méthode non constante d'un objet constant, etc. Bref, c'est le comportement attendu de la constance.
// fichier main.cpp #include <cstdlib> #include "MaClasse.h" int main(int argc, char **argv) { const MaClasse mc(8); // initialisée lors de la déclaration. // mc est déclaré const donc getMemberInt doit être const sinon // il y aura une erreur à la compilation. mc.getMemberInt(); return EXIT_SUCCESS; } // fichier MaClasse.h #ifndef MA_CLASSE_H #define MA_CLASSE_H class MaClasse { public: MaClasse(int value); // constructeur ~MaClasse(); // destructeur int getMemberInt() const; private: int memberInt; mutable int mutableInt; }; #endif // fichier MaClasse.cpp #include "MaClasse.h" MaClasse::MaClasse(int value) : memberInt(value), mutableInt(0) { } MaClasse::~MaClasse() { } int MaClasse::getMemberInt() const { // on ne peut pas modifier la classe ici sinon il y a une erreur // à moins que la variable modifiée soit mutable : peut être utilisé // comme un cache. mutableInt++; return memberInt; }
Retour de fonction
Le principe est le même pour le retour des fonctions. On pourra retourner une copie, un pointeur ou encore une référence sur un objet. La première méthode n'est pas optimisée, surtout si vous faites appel à la fonction suivante dans une boucle par exemple.
// ici, une copie de obj sera retournée. Object getObject() { Object obj; obj.initialize(); return obj; }
Il est beaucoup plus intéressant de retourner un pointeur ou une référence car seule l'adresse mémoire est copiée. De ce fait, il ne s'agit plus d'une copie... Attention toutefois à ne pas commettre l'erreur classique de retourner la référence d'une variable locale. En effet, retourner une référence sur un objet créé dans une fonction est une erreur, car la variable référencée est détruite à la sortie de la fonction donc le référence n'est plus valide.
int & func() { int i = 3; // i est détruit à la sortie de la fonction donc la référence n'est plus valide. return i; }
Si l'on désire retourner une référence, il faut qu'elle porte sur un objet existant en dehors du contexte de la fonction, comme une variable membre par exemple. Une fonction const ne doit normalement pas être en mesure de retourner une référence non constante ou un pointeur non constant car cela violerait la propriété de constance de la fonction. Par exemple :
class MaClasse { public: MaClasse() : x(0) {} ~MaClasse() {} // on retourne une référence sur la variable membre x, on peut donc modifier x. // donc impossible de déclarer cette fonction const. int & getX() { return x; } // on retourne une référence constante sur la variable membre x. // on souhaite autoriser la lecture mais pas la modification. const int & getConstX() const { return x; } private: int x; }; int main(int argc, char **argv) { MaClasse mc; mc.getX() = 3; // cette syntaxe permet de modifier x via la référence. //mc.getConstX() = 5; // interdit car il s'agit d'une référence constante. // on conserve la référence via le symbole &. const int &constIntRef= mc.getConstX(); // ici une copie est faite via l'opérateur =. int intCopy = mc.getConstX(); }
Via le même principe, on peut retourner un pointeur sur un membre d'une classe. Mais il est également possible de retourner un pointeur sur une variable créée au sein d'une fonction. Il suffit pour cela d'utiliser l'opérateur new. Il ne faudra pas oublier de libérer la mémoire avec delete lorsqu'on n'en aura plus besoin.
// ici, un pointeur sur obj sera retourné. Object * getObject() { Object *obj = new Object(); obj->initialize(); return obj; } int main(int argc, char **argv) { Object o = getObject(); int v = o.getValue(); delete o; }
N'utilisez pas const_cast
N'employez pas - pour ne pas dire jamais - l'opérateur de conversion const_cast. En effet, vous mentez à vos utilisateurs si vous leur annoncez que l'objet qu'ils vont vous passer en paramètre est constant et que vous le modifiez... Utiliser const_cast est souvent synonyme d'une mauvaise conception et peut être à l'origine d'erreurs étranges.
Conclusion
Dans tous les cas, vous devriez, par défaut, déclarer constant les paramètres ainsi que les fonctions au nom de la robustesse mais aussi de certaines opportunités d'optimisations qu'apportent ce qualificatif pour le compilateur comme un exécutable plus petit, plus rapide. Si, bien sûr, un paramètre doit être modifié, ou si vous devez modifier un membre non mutable de la classe, supprimez les const gênants. Le mot-clé const n'indique pas seulement au compilateur ce qu'il se passe avec votre code, il indique aussi comment il se comporte aux personnes qui l'utilisent. On utilisera les accesseurs pour garantir le principe d'abstraction, mais si l'on doit laisser un accès direct à une variable membre privée, on retournera de préférence une référence constante ou un pointeur constant sur cette variable afin d'éviter tout effet de bord et donc de renforcer la qualité du code. On retiendra le principe de contagion du mot-clé const qui interdit d'utiliser des fonctions non constantes sur des variables constantes, l'inverse est fort heureusement autorisé.
Vous pourrez approfondir le sujet avec la sélection des livres suivants : Standards de programmation en C++, Pour mieux développer avec C++ et Le langage C++.

Commentaires
Je souhaite juste apporter une précision sur les variables statique de classe.
Tel que tu le présentes, il a un gros désavantage. L'ensemble des unités de code qui incluront l'en-tête de définition de la classe auront leur propre variable statique. Pour des const, ce n'est pas très gênant (encore que, si on modifie l'en-tête et que toutes les unités qui en dépendent ne sont pas recompilées, ça peut poser problème).
Un article concernant externe, lié à cet article, serait le bienvenu