Const keyword


To translate this course in your native langage:

  • In Chrome: right-click anywhere on the page and select “Translate to…”

  • Or use this language selector:

Les mots colorés en anglais représentent des mots-clés de la norme du langage C++. Ils sont conservés à l’identique lors de l’utilisation d’une traduction automatique avec Google Translate. Exemple : l’héritage (inheritance) est un principe fondamental de C++.


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 va forcer le développeur à insérer ce mot clef à d’autres endroits du programme.

  • Savoir comment réagir face à un message d’erreur relatif au mot-clef const n’a rien d’évident.

Nous présentons 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 (constant variable / const variable). Déclarer une variable const permet au compilateur de la substituer par sa valeur afin de rendre l’exécution plus rapide.

Fonction const

Cette configuration n’existe pas.

Fonction membre const

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

En C++, une fonction membre const (const member function / const method) garantit de ne pas modifier l’objet sur lequel elle est appelée. Autrement dit, son exécution n’altère pas les variables membres de l’objet. 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, on ne peut appeler que les méthodes const de l’objet, car elles garantissent de ne pas le modifier. 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.

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 : interdite

Voici un exemple testant toutes les configurations possibles :

void ConstRef(const int & x)            {   std::cout << x << std::endl; }
void NonConstRef(int & y)               {   y += 10;                     }

int main()
{
        int a = 5;

        int & ref = a;
        const int & cref = a;

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

}

Indiquez si les affirmations suivantes sont vraies ou fausses :

  1. L’interprétation du mot clef const dépend du contexte.

  2. Depuis une const référence, on ne peut appeler que les méthodes membres déclarées const.

  3. On peut appeler appeler une fonction acceptant une référence non const en lui fournissant une const référence.

  4. Une variable const doit être initialisée lors de sa création.

  5. Une fonction membre const est une fonction qui retourne toujours le même résultat.

  6. Depuis une fonction membre const, je ne peux accéder qu’aux données const de l’objet.

  7. Une const référence garantit que l’objet référencé ne pourra jamais être modifié.

  8. Il est possible de convertir implicitement une référence non const en const référence .

  9. Le code ci-dessous comporte une erreur :

    void ConstRef(const int& x);
    int a = 5;
    int& ref = a;
    ConstRef(ref);
    
  10. Le code ci-dessous comporte une erreur :

    void NonConstRef(int& x);
    int a = 5;
    const int& cref = a;
    NonConstRef(cref);
    

Durée de vie des objets

Certains objets ont des durées de vie courte. Nous présentons les deux configurations principales :

Objet local

Ce scénario désigne un objet créé à l’intérieur d’une fonction. Sa durée de vie est limitée au bloc dans lequel il est défini car il est détruit quand la fonction se termine.

void localObject()
{
   Matrix M(2,2);
   M.randomize();
   cout << M;
}

L’objet M naît à sa définition et est détruit à la fin de la fonction. C’est un objet local (local object) dont la durée de vie est limitée au bloc de cette fonction.

Object temporaire

On appelle objet temporaire (temporary object) un objet sans nom, créé comme résultat intermédiaire ou final d’une expression et dont la durée de vie est en général limitée à cette expression. Voici quelques exemples :

  • L’objet Matrix existe juste le temps nécessaire à cette expression, puis il est détruit :

    cout << Matrix(2,2);
    
  • L’objet string est temporaire :

    print( string("bonjour").length() );
    
  • L’expression A + B produit un objet temporaire de type Matrix :

    Matrix C = transpose(A + B);
    

Retour d’une fonction

Objet local/temporaire

Si une fonction crée un objet local ou temporaire, il ne faut pas retourner une référence vers cet objet. En effet, cet objet est détruit à la fin de la fonction, donc la référence pointerait vers un objet qui n’existe plus. En résumé :

REGLE : Depuis une fonction, il est interdit de retourner une référence/const référence sur un objet local/temporaire.

Ainsi, les objets locaux/temporaires doivent être retournés par valeur :

struct V2
{
   float x,y;
   V2(float a,float b) : x(a), y(b) { }
};

V2 add(const V2 &  a, const V2 &  b)   // return by value
{
   return V2(a.x+b.x,a.y+b.y);         // temporary object
}

Données membres

Une classe contient des données membres. Lorsque l’appel à une fonction membre se termine, ces données existent encore. Dans cette logique, il est alors possible de retourner une référence/const référence vers ces données.

struct Matrix2x2
{
   float _mydata[2][2];

   float & get(int i,int j)   {   return _mydata[i][j]; }

};

int main()
{
  Matrix2x2 M;
  M.get(1,1) = 3;
}

En résumé

Les retours

Syntaxe

Nature

Modifiable ?

Copie ?

Usage

T f()

Objet

Oui

retourne une copie de la donnée membre
ou retourne l’objet local-temporaire

Retourne une valeur indépendante

T& f()

Référence

Oui

Non

Retourne une référence vers un objet pérenne

const T& f()

Const référence

Non

Non

Retourne une référence vers un objet pérenne

Note

Lorsque l’on veut retourner une donnée membre en lecture seule, on a le choix entre : un retour par valeur ou un retour par const référence. Comment choisir ? Si l’objet est volumineux, préférez généralement un retour par const référence. Pour un type fondamental, préférez un retour par valeur.

Arguments de fonction

Objet temporaire comme argument

Il est recommandé de passer les objets volumineux par référence afin d’éviter leurs copies. Dans l’exemple suivant, tout fonctionne correctement :

Matrix transpose(Matrix & M)  {...}

M2 = transpose(M1);  // OK

Cependant, en écrivant la ligne ci-dessous, le programme produit une erreur de compilation :

T = transpose(M1 + M2);  # error

Lors de l’appel de la fonction transpose(), le paramètre M est une référence recevant comment argument l’objet temporaire M1+M2, ce qui n’est pas autorisé. Il existe toutefois une exception :

REGLE : une const référence peut être liée à un objet temporaire car elle prolonge sa durée de vie.

Ainsi, le code suivant fonctionne parfaitement :

Matrix transpose(const Matrix & M)  {...}

M2 = transpose(M1);      // OK
T = transpose(M1 + M2);  // OK

Autre exemple

Nous présentons ici la surchrage de l’opérateur << utilisé pour afficher une matrice sur cout :

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

Les tests sont moyennement satisfaisants :

cout << M1;       OK
cout << M1+M2;  ERROR

La solution consiste donc à utiliser une const reference lors de la définition de l’opérateur << :

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

Ce qui résout le problème.

En résumé

Passage des paramètres

Syntaxe

Usage

Peut Modifier l’argument

Accepte un objet temporaire

… fnt(Matrix& M)

Modifier l’objet original

Oui

Non

… fnt(const Matrix& M)

Lire un objet sans le copier

Non

Oui

… fnt(Matrix M)

Crée une copie indépendante

Non

Oui

Note

Lorsque l’on veut accepter un argument en lecture seule, on a le choix entre : un passage par valeur ou un passage par const référence. Comment choisir ? Encore une fois, si l’objet est volumineux, préférez généralement un passage par const référence. Pour un type fondamental, préférez un passage par valeur.

Remarque

En C++, les paramètres d’une fonction permettent souvent de deviner son comportement. Par exemple :

  • Matrix transpose(const Matrix & M) : cette fonction prend en paramètre une référence constante sur un objet matrice. Elle ne peut donc pas modifier la matrice passée en argument. L’objet matrice retourné correspond probablement à la transposée. On comprend donc que cette fonction reçoit une matrice, calcule sa transposée sans modifier l’originale, puis retourne un nouvel objet.

  • void transpose(Matrix & M) : cette fonction ne retourne pas d’objet matrice. Son paramètre est une référence non const. Ainsi, nous pouvons conclure que cette fonction transpose les paramètres de la matrice passée en argument.

Encapsulation

Mise en place

L’encapsulation (encapsulation) consiste à cacher les données internes d’un objet et à contrôler la manière dont elles peuvent être consultées ou modifiées.

Ainsi, pour protéger une donnée membre, il est recommandé :

  • de la rendre privée

  • de créer un getter pour lire cette donnée en lecture seule

  • de créer un setter pour la modifier à condition que la nouvelle valeur soit considérée comme valide

struct Person
{

    const Address& getAddress() const
    {
       return address_;
    }

    void setAddress(const Address& address)
    {
      if ( ! address.city.empty() )
         address_ = address;
    }

  private:

    Address address_{"Paris"};
};

On remarque que le getter getAddress() a deux caractéristiques importantes :

  • Cette fonction membre est déclarée const car elle ne modifie pas l’état de l’objet

  • Elle retourne une const reference sur le paramètre interne, ce qui garantit l’absence de recopie et la lecture seule

Le setter setAddress() accepte une const reference :

  • Ceci garantit que la nouvelle adresse n’est pas recopiée inutilement

  • Que l’argument ne sera pas modifié

On remarque que le setter vérifie que l’adresse n’est pas vide avant d’effectuer l’affectation.

Sucre syntaxique

Si nous voulons créer une sorte de liste indexée de doubles, il peut être lourd d’écrire :

cout << l.getvalue(i);
l.setvalue(i,5.2);

Les utilisateurs sont parfois habitués à une syntaxe plus naturelle comme :

cout << l[i];
l[i] = 5.2;

Pour permettre cette écriture, il faut surcharger l’opérateur [] et lui faire retourner une référence vers l’élément courant :

double & liste::operator [ ] (int i)  { ... }

Cette référence peut être utilisée pour gérer la lecture comme l’affectation. Cependant, petit piège, le code suivant ne va pas fonctionner :

void print(const liste & L)  { for (int i = 0 ; i < L.size() ; i++)  cout << L[i]; }

Pourquoi ? Essayez de trouver la raison avant de consulter la réponse ci-dessous :

Dans cette configuration, il faut donc définir en parallèle une version const de l’opérateur []. Cette version retourne une référence constante vers l’élément indexé ce qui valide le fait que l’opérateur [] ne permet pas de modifications. Voici donc sa définition :

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

Ainsi, dans le code ci-dessous:

print(l);
l[i] = 5.2;
  • La fonction print() utilise l’opérateur [] dans sa version const

  • L’affectation l[i] utilise l’opérateur [] dans sa version non const !

Note

Lorsque l’on dispose d’une référence vers cette liste, l’appel à l’opérateur [] pose problème, car on peut choisir la version const ou non const ! Comment fonctionne le mécanisme de surcharge (function overloading) dans cette situation ? Il s’appuie sur les paramètres de la fonction habituellement mais pas seulement, dans notre cas, il gère aussi le qualificatif const des fonctions. Ainsi, il sélectionne dans cette configuration la version non const. Si l’objet est const, la question ne se pose pas, seule la version const de l’opérateur [] est accessible.

Bilan

Le mot clef const est polluant : si vous écrivez un premier const dans votre programme, cela va rapidement vous obliger à multiplier sa présencer dans le code :

  • Une fonction membre déclarée const ne peut appeler que d’autres fonctions membres elles-mêmes déclarées const !

  • Ainsi, lorsqu’un objet est reçu par const référence, seules ses fonctions membres const peuvent être appelées. Si vous n’en avez pas mises en place, vous ne pourrez pas faire grand chose.

  • Lorsqu’une const référence est transmise à une fonction, celle-ci doit accepter des const references car on ne peut convertir une const référence en référence non const.

En conclusion, concevoir une classe utilisant le mot clef const de manière cohérente peut être délicat et demande une certaine expérience. Cette contrainte peut notamment conduire à dédoubler certaines fonctions afin de proposer une version const et une version non const ! Il est donc important de comprendre le fonctionnement des const, car ils sont très largement utilisés dans le code C++.

Quizzz

Indiquez si les affirmations suivantes sont vraies ou fausses :

  1. Depuis une fonction, on doit retourner une const référence si l’on retourne un objet temporaire.

  2. Depuis une fonction, il ne faut pas retourner une référence sur un objet temporaire créé dans cette fonction.

  3. Depuis une fonction, si je crée un objet local, je peux le retourner par référence (non const).

  4. Depuis une fonction, retourner un objet par valeur est valide.

  5. Un paramètre de type référence (non const) peut accepter un objet temporaire comme argument.

  6. Depuis une fonction, on peut retourner une réference (non const) sur un objet pérenne.

  7. Pour passer un objet temporaire en argument, on peut utiliser un paramètre const reference.

  8. Pour passer un objet temporaire en argument, on peut utiliser un paramètre copie.

  9. Depuis une fonction, pour modifier un objet passé en argument, il faut un paramètre const référence.

  10. Depuis une const référence, on ne peut appeler que des fonctions membres const.

Travail à rendre sur l’espace partagé

Exercice 1

  • Créez un fichier nommé const.cpp

  • Dans le code ci-dessous, ajoutez des const/const ref partout où cela est nécessaire

  • Pour vous aider à vérifier votre réponse : 8 occurrences doivent être trouvées

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) { ... }
};
  • Déposez votre fichier dans l’espace partagé.

Exercice 2

Nous voulons mettre en place une classe vecteur de 10 entiers appelés V10.

  • Créez un fichier nommé const_V10.cpp

  • Pour cette classe, fournissez les fonctionnalités suivantes :

    • Constructeur par défaut : ne réalise aucune initialisation des éléments.

    • Constructeur prenant une valeur : initialise tous les éléments avec la valeur passée en paramètre.

    • Constructeur par recopie : reçoit un objet V10 et recopie l’ensemble de ses valeurs.

    • size() : retourne la taille du tableau sans modifier l’objet.

    • get(i) : retourne la valeur située à l’indice i.

    • set(i, value) : modifie la valeur située à l’indice i.

    • print() : affiche le contenu du tableau sans modifier l’objet.

    • equals(…) : indique si l’objet courant contient les mêmes valeurs que l’objet passé en paramètre.

    • operator[] : fournir une version const et une version non const.

    • find(value) : retourne l’indice de la première case contenant value, ou -1 si la valeur n’est pas présente.

    • copyFrom(…) : copie le contenu d’un autre objet V10 sans modifier l’objet source.

    • operator= : copie le contenu d’un objet V10 dans l’objet courant sans modifier l’objet source.

  • Veillez à utiliser le mot clef const partout où cela est pertinent, aussi bien pour les méthodes que pour les paramètres.

  • Déposez votre fichier dans l’espace partagé.

Pour vérifier votre code, vous pouvez utiliser le code suivant : test.txt

Avertissement

IA or not IA, that is the question. Ces exercices sont au programme de l’examen.