Mot-clef const ✱

Le mot clef const est un incontournable du langage C++. Dans du code professionnel, on peut même le classer dans le top 10 des mots-clefs utilisés.

Pourtant, ce mot-clef disparaît généralement des études supérieures. Pourquoi un tel tabou ?

  • Ce mot clef a des interprétations multiples, il devient donc déroutant pour le développeur débutant.

  • Il est polluant au sens où l’insertion d’un mot-clef const dans le code va forcer le développeur à insérer ce mot clef à d’autres endroits du programme pour éviter des messages d’erreurs du compilateur.

  • Savoir comment réagir face à un problème relatif aux consts n’a rien d’évident.

Cependant, ce mot-clef étant omniprésent dans les sources C++, nous allons quand même introduire et rappeler ces usages les plus fréquents.

Const variable

const int NB_USERS_MAX = 5;

Cette syntaxe permet de définir une constante : une valeur qui ne change pas. Déclarer une variable const permet au compilateur de la substituer par sa valeur afin de rendre l’exécution plus rapide. Cette technique permet aussi de rendre le code plus lisible, en effet le nom NB_USERS_MAX est bien plus parlant que le chiffre 5.

Fonction membre const

class Z
{
… fnt(…) const { … }
};

En C++, une fonction const correspond à une fonction membre qui ne modifie pas l’état de l’objet sur lequel elle est appelée. Autrement dit, son appel n’entraîne pas de modification des variables membres de l’objet. On peut considérer que l’état de l’objet est resté constant durant l’appel de cette fonction. Ainsi cette syntaxe :

  • Sert de documentation aux programmeurs en indiquant que l’appel à cette fonction ne modifie pas l’objet.

  • Fournit une information au compilateur pour lever des messages d’erreur en cas de non respect des règles.

Voici un exemple :

class MaClasse
{
        private:
                int valeur;

        public:
        int obtenirValeur() const
        {
                return valeur;   // lecture seule => on peut déclarer la fonction const
        }

        void changerValeur(int nouvelleValeur)
        {
                valeur = nouvelleValeur;  // écrire => cette fonction ne peut être const
        }
};

Const référence

void fnt(const int & nom) { … }

Voici un exemple :

void afficherValeur(const int& x)
{
        cout << x << endl;
        // x = 5;  => provoquerait une erreur de compilation
}

int main()
{
        int a = 10;
        afficherValeur(a);
}

Une const référence en C++ est une référence qui ne permet d’appeler que des méthodes const. Ainsi, une fonction exposant une const référence en paramètre garantit qu’elle ne modifiera pas l’objet passé. L’utilisation de const références est courante, cela constitue :

  • Une documentation pour les programmeurs

  • Une sécurité car elle impose au compilateur de vérifier que le code respecte les contraintes associés au mot-clef const.

Conversion

Dans cette logique :

Une conversion implicite :

  • Référence => Const référence : autorisée

  • Const référence => référence : impossible

Voici un exemple testant toutes les configurations possibles :

void ConstRef(const int & x)
{
        std::cout << x << std::endl;
}

void NonConstRef(int & y)
{
        y += 10; // Modification
}

int main()
{
        int a = 5;
        int & ref =  a;
        const int & cref = a;

        NonConstRef(ref);    // OK
        ConstRef(ref);       // OK
        NonConstRef(cref);   // ERREUR
        ConstRef(cref);      // OK
}

Référence vers une L-value

Rappel

Ce cas rentre dans la catégorie : programmation avancée du C++. On aurait pu éviter d’en parler, mais comme vous allez le rencontrer, nous abordons maintenant le sujet à travers divers exemples utilisant la classe Matrix.

Matrix M = M1 + M2;

Cet exemple fonctionne. Le déroulé est le suivant :

  • Appel de l’opérateur + prenant en arguments deux objets matrices.

  • Création et retour d’un objet matrice temporaire anonyme correspondant au résultat de l’addition.

  • Création et initialisation de la matrice M à partir de l’objet temporaire.

  • Destruction de l’objet temporaire

Matrix & M = M1 + M2;
M.print();

Cette situation déclenche une erreur de compilation : en effet, on crée une référence sur un objet temporaire (une R-value) et le C++ l’interdit. Soit une erreur est levée à la compilation, soit un plantage peut avoir lieu à l’exécution, certainement le pire des cas car aucun message d’information n’est affiché. A la deuxième ligne de cet exemple, la référence M est associée à un objet probablement détruit. L’appel de la fonction print() déclenche alors une erreur générale.

Cependant, il existe une autre option :

Le langage C++ garantit qu’une const référence prolonge la durée de vie des objets temporaires. De plus, les const références peuvent se lier (bind) à des R-value (objet temporaire, littéral).

Ainsi le code suivant fonctionnera :

const Matrix & M = M1 + M2;
M.print();   // print() doit être une fonction const !

En pratique

Ce problème des références non autorisées vers les L-values apparaît lors des utilisations de cout. Supposons que nous disposions des opérateurs suivants :

Matrix operator + (const Matrix & M1, const Matrix & M2) {... }
ostream& operator<<(ostream& os, Matrix & mat) {...}  // référence non const sur l opérande de droite

En utilisant ces fonctions, vous allez pouvoir réaliser ceci :

Scénario 1

Matrix M1,M2;
cout << M1 ;   // passage d une L-value vers une référence non const : autorisé

Scénario 2

Matrix M1,M2;
Matrix M = M1+M2;   // initialisation d une L-value à partir d une R-value : autorisé
cout << M;          // passage d une L-value vers une référence non const : autorisé

Scénario 3

Matrix M1,M2;
cout << M1+M2;          // erreur générale

Étrangement, alors que les deux premières options ne posent aucun problème, cette syntaxe conduit à l’accident (message d’erreur ou plantage). En effet, l’opérateur + retourne un objet temporaire (une R-Value) alors que l’opérande de droite de l’opérateur << accepte une référence non const, d’où le problème.

Ainsi, pour que ces trois scénarios fonctionnement, l’opérande de droite de l’opérateur << doit être une const référence :

ostream& operator << (ostream& os, const Matrix & mat) {...}

Le caractère polluant

Cas 1 : propagation en arrière

class T
{
    ...

        public :
                void Aff()   {...}
                int GetMin() {...}
                int GetMax() {...}
};

void Test(const T & obj)
{
   obj.  ??? => AUCUNE OPTION POSSIBLE
}

int main ()
{
   T obj;
   Test(obj);
}

Si une fonction accepte une const référence, le compilateur vous autorise uniquement à appeler les méthodes const depuis cette référence, c’est logique. Si la classe n’expose aucune méthode const, la référence ne pourra appeler aucune méthode et là, c’est le drame ! Il faut donc reprendre la classe en question et déclarer const toutes les fonctions membres à caractère read-only. D’où la propagation en arrière du caractère const :

class T
{
    ...

        public :
                void Aff()   const {...}
                int GetMin() const {...}
                int GetMax() const {...}
};

Cas 2 : dédoublement des opérateurs/fonctions

class BoundCheckArray
{
        int T[10];

        public :

        BoundCheckArray()       { for (int i = 0; i < 10; i++) T[i] = i; }

        int & operator[](int i) { if ((i<10) && (i>=0)) return T[i]; else throw std::out_of_range(""); }
};

void Increase(BoundCheckArray & T)  { for (int i = 0; i < 10; i++) T[i]++; }

int main()
{
   BoundCheckArray T;

   Increase(T);

   return 0;
}

L’opérateur d’indexation reçoit un entier comme index et retourne une référence (lvalue) afin de pouvoir éventuellement modifier la valeur contenue dans ce tableau. Si on avait simplement retourner un int (une r-value), on ne pourrait exécuter l’instruction : T[2] = 4; correctement par exemple.

Le code de l’exemple fonctionne correctement. Supposons que l’on veuille maintenant utiliser une fonction affichant les éléments du tableau. Comme cette fonction ne modifie pas l’élément passé, elle prend donc une const référence en paramètre :

void Aff(const BoundCheckArray & T) { for (int i = 0; i < 10; i++)   std::cout << T[i] << " ";   }

Or, comme T représente une const référence et que l’on accède à travers elle à l’opérateur [] non const, le compilateur lève une erreur. Il faut donc ajouter une deuxième version const de l’opérateur d’indexation. Que va retourner cette version const ? On peut lui faire retourner un int ce qui serait exact. Mais, si l’on avait un objet, cela produirait une copie. Il est alors un peu plus judicieux de retourner une const référence afin d’éviter cette recopie.

Ainsi nous obtenons comme programme final :

#include <iostream>

class BoundCheckArray
{
        int T[10];

        public :

              BoundCheckArray()             { for (int i = 0; i < 10; i++) T[i] = i; }
              int & operator[](int i)       { if ((i<10) && (i>=0)) return T[i]; else throw std::out_of_range(""); }
        const int & operator[](int i) const { if ((i<10) && (i>=0)) return T[i]; else throw std::out_of_range(""); }
};

void Increase(BoundCheckArray & T)
{
   for (int i = 0; i < 10; i++) T[i]++;   // Appel de l opérateur [] non const
}

void Aff(const BoundCheckArray & T)
{
   for (int i = 0; i < 10; i++) std::cout << T[i] << " "; // Appel de l opérateur [] const
}

int main()
{
   BoundCheckArray T;

   Increase(T);

   Aff(T);

   return 0;
}