Const keyword✱

  • Your native language: with Chrome, right-click on the page and select « Translate into … »

  • English version:

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, l’introduction de ce mot-clef dans les cursus de formation est souvent évitée. Pourquoi un tel tabou ?

  • Ce mot-clé prend des sens très différents selon le contexte, ce qui peut facilement dérouter un 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 message d’erreur relatif au mot-clef const n’a rien d’évident.

Cependant, ce mot-clef étant omniprésent dans les sources C++, nous allons l’introduire et présenter ces usages les plus fréquents.

Les différentes significations

Variable const

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.

Note

Cette technique permet de rendre le code plus lisible, en effet le nom NB_USERS_MAX est bien plus parlant que le chiffre 5.

Fonction const

Cette configuration n’existe pas.

Fonction membre const

struct T
{
… fnt(…) const { … }
};

En C++, une fonction membre marquée const garantit de ne pas modifier l’état de l’objet sur lequel elle est appelée. Autrement dit, son exécution n’altère pas les variables membres de l’objet. On peut donc considérer que l’état de l’objet demeure constant pendant toute la durée de l’appel. 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 MyClass
{
        private:
                int value;

        public:
        int getValue() const
        {
                return value;   // read-only => function can be declared const
        }

        void setValue(int newValue)
        {
                value = newValue;  // write => this function cannot be const
        }
};

Const référence

void fnt(const Matrix & nom) { … }

Une const référence en C++ est une référence garantissant que l’objet référencé ne sera jamais modifié à travers l’utilisation de cette référence. Mais alors, que peut-on faire avec une const référence ? Et bien, il est possible d’appeler depuis cette référence les méthodes const de l’objet référencé puisqu’elles sont garanties comme ne modifiant pas l’objet. Tout appel d’une méthode non const depuis une const référence lévera une erreur lors de la compilation.

L’utilisation de const références en C++ est fréquente. Cela sert aussi comme :

  • Documentation pour les programmeurs

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

Il est aussi possible d’utilise le mécanisme des const références avec des types fondamentaux. Mais il est plus naturel de le présenter dans le cadre des classes.

Différents mécanismes

Conversion const/non const

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;

        ConstRef(ref);       // OK
        ConstRef(cref);      // OK
        NonConstRef(ref);    // OK
        NonConstRef(cref);   // ERROR

}

Référence et R-value

L’association d’une réfèrence à une prvalue déclenche une erreur de compilation :

Matrix & M = M1 + M2;  # error

Une prvalue est associé à temporaire alors que la référence est pérenne, donc ce n’est pas possible. Il existe cependant une exception dans le langage donnée par cette règle :

REGLE : Un prvalue peut être liée à une référence const. Dans ce cas, la durée de vie de l’objet temporaire est prolongée pour correspondre à la durée de vie de la référence.

On peut donc écrire :

const Matrix & M = M1 + M2;

En pratique

Passage d’arguments par const référence

Un paramètre de fonction utilisant une référence permet d”EVITER la recopie mémoire de l’objet passé en argument. Il est donc IMPORTANT de les utiliser le plus souvent possible. Cependant, cela ne suffit pas. Prenons un exemple pour illustrer notre propos :

void print(Matrix & M) {...}
...
print(M1)

Un jour, un autre développeur va écrire ceci :

print(M1+M2);

Ce qui va conduire à l’initialisation d’une référence à partir d’une L-value ! Ce qui est interdit. Il n’est pas concevable d’interdire l’écriture print(M1+M2), il faut donc utiliser les règles du C++ pour permettre cette utilisation. Pour cela, nous devons utiliser des const références qui prolongent la durée de vie de l’objet temporaire M1+M2.

Ainsi le code suivant fonctionne :

void print(const Matrix & M) {...}
print(M1+M2);

REGLE : Si l’argument par référence d’une fonction n’a pas pour objectif d’être modifié, alors utilisez une const reference.

Afficher un objet avec cout

Le scénario présenté ici est similaire au précédent. Admettons que vous ayez développé une classe Matrix et que vous ayez surchargé l’opérateur << pour utiliser cout :

ostream& operator<< (ostream& os, Matrix & mat)
{
    ...
        return os;
}
Matrix M1;
cout << M1;

L’utilisation de la référence permet d’éviter la recopie de l’objet matrice passé en paramètre. Cependant, si un utilisateur écrit ce code :

cout << M1+M2;

C’est l’accident, plus rien ne compile. La raison est que l’opérateur + retourne un objet temporaire, donc une prvalue, objet transféré à une référence non const Matrix & mat et le C++ interdit cela. La solution consiste à utiliser une const reference lors de l’écriture de l’opérateur << :

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

Ce qui résout le problème.

Les opérateur arithmétiques

Supposons que nous voulions créer un opérateur + effectuant une addition entre deux matrices. Comme nous l’avons vu, les deux matrices, doivent être passées en référence pour éviter une copie. De plus, comme l’addition entre les deux matrices n’a pas vocation à les modifier, nous devons donc utiliser des const référence comme ceci :

xxx operator + (const Matrix & M1, const Matrix & M2)
{
        Matrix result;
        ...
        return result;
}

Dans le corps de cet opérateur, nous allons créer une nouvelle matrice stockant le résultat. Nous retournons évidemment cet objet. Cependant, quel type doit retourner cet opérateur ? Nous avons vu que retourner une référence sur un object temporaire est interdit. Il ne reste donc qu’un choix possible : retourner une valeur. L’unique option retenue est celle-ci :

Matrix operator + (const Matrix & M1, const Matrix & M2);

Si l’on regarde les diverses contraintes, il n’y a pas tant de choix que cela. Ainsi, vous allez trouver la forme ci-dessus pour la définition de la plupart des opérateurs.

Surcharge const/non const des getters

Imaginons que vous ayez dévelopé une classe Data qui stocke 100 entiers dans un container nommé d. Si vous voulez écrire un getter qui ne permet pas seulement de lire ces éléments mais aussi les modifier, vous devez mettre en place une version qui retourne une référence sur l’élément en question :

int & operator[](int i) { return d[i]; }
Data D;
cout << D[3];   // OK
D[4] = 7;               // OK

Mais un jour, un collègue va écrire ce code :

void Aff(const Data &  info)
{
        cout << info[i];
}
Aff(M1);

Cette fonction Aff a été écrite en utilisant les bonnes pratiques :

  • Passage par référence pour éviter la recopie

  • Utilisation d’une const référence car cette fonction ne modifie pas l’objet passé en argument

Le code de la fonction Aff() déclenche une erreur de compilation. Essayez de trouver pourquoi par vous-même. La réponse est donnée ci-dessous.

Que faut-il faire ? Il faut fournir deux versions de l’opérateur [] : une version const et une non const

      int & operator[](int i)       { ... }
const int & operator[](int i) const { ... }

La première est nécessaire lorsque l’on écrit D[4]=7 et la deuxième est nécessaire lorsque l’on passe l’objet D à une fonction prenant une const référence.

A noter, que pour garantir le caractère const de cet opérateur, il faut qu’il retourne une const référence.

Une classe développée de manière professionnelle doit donc fournir deux versions d’un getter, une version const et non const !

Le caractère polluant des const

L’utilisation d’une seule const référence va vous obliger à deux choses :

  • Rechercher et qualifier comme const toutes les fonctions membres read-only de votre classe. En effet, si vous ne faites pas ce travail, aucune méthode ne sera disponible à travers votre const référence.

  • Si une seule fonction dans votre programme accepte une const reference sur un type T, cela oblige à dédoubler tout getter retournant des références sur T pour fournir une version const et non const.

Travail à rendre sur Github Classroom

Exercice 1

  • Créez un fichier nommé 4_const.cpp

  • Dans le code ci-dessous, ajoutez le qualitificatif const là où il est utile/nécessaire

  • Uploadez le fichier résultat sur votre github

  • Pour information, il y a 8 const à ajouter

struct Rect
{
        V2 A; // lower left corner
        V2 B; // upper right corner

        Rect(V2& a, V2& b) : A(a), B(b) {}

        int width()  { return abs(A.x-B.x); }
        int height() { return abs(A.y-B.y); }

        void translate(V2 v) { A +=v; B+= v; }

        int area()   { return width() * height(); }

        bool isInside(V2 P) { ... }
};

Exercice 2

  • Créez un fichier nommé 4_const_Data.cpp

  • Mettez en place une structure Data stockant 100 entiers en respectant les consignes ci-dessous

  • Intégrez les test donnés ci-dessous dans votre code

  • Validez vos fonctions

  • Uploadez le fichier sur votre github

Objectif

Développez une classe Data qui stocke 100 entiers et expose :

  1. operator[] pour accéder à la i-ème valeur (pas de gestion des bords)

  2. mean() pour la moyenne

  3. fill(int value) pour initialiser tous les éléments à la valeur value

  4. scale(int factor) pour multiplifer tous les éléments par une même valeur

  • Pour le stockage, vous pouvez utiliser un tableau de 100 entiers.

  • Vous devez utiliser le mot clef const à chaque fois que cela est nécessaire

Scénario de tests

#include <iostream>
    using namespace std;


int main()
    {
    Data A;
    const Data& C = A;  // const-correctness

    // fill
    A.fill(2);
    for (std::size_t i = 0; i < 100; ++i) assert(A[i] == 2);

    // scale
    A.scale(3);
            for (std::size_t i = 0; i < 100; ++i) assert(A[i] == 6);

    // mean
    cout << "mean(A) = " << A.mean() << "\n";
    assert(A.mean() == 6.0);
            double m = C.mean();  // const-correctness

    // Write using non-const
    A[3] = 10;
    assert(A[3] == 10);

            // Read using const
            cout << C[3];
}