Const keyword ************* .. include:: ../BoutonGoogleTrad.rst 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 -------------- .. panels:: :column: col-lg-5 p-2 **const** :cpp:`int NB_USERS_MAX = 5;` Cette syntaxe permet de définir une constante : une valeur qui ne change pas (:cppterm:`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 --------------------- .. panels:: :column: col-lg-5 p-2 | *struct T* | { | ... *fnt(...)* **const** | { | ... | } | }; En C++, une fonction membre const (:cppterm:`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 : .. code-block:: 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 --------------- .. panels:: :column: col-lg-5 p-2 :cpp:`void fnt(` **const** :cpp:`Matrix & nom) { ... }` Une :cppterm:`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 : .. panels:: :column: col-lg-5 p-2 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 : .. code-block:: 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 } .. quiz:: WhatsCpp :title: Const keywords Indiquez si les affirmations suivantes sont vraies ou fausses : #) :quiz:`{"type":"TF","answer":"T"}` L'interprétation du mot clef const dépend du contexte. #) :quiz:`{"type":"TF","answer":"T"}` Depuis une const référence, on ne peut appeler que les méthodes membres déclarées const. #) :quiz:`{"type":"TF","answer":"F"}` On peut appeler appeler une fonction acceptant une référence non const en lui fournissant une const référence. #) :quiz:`{"type":"TF","answer":"T"}` Une variable const doit être initialisée lors de sa création. #) :quiz:`{"type":"TF","answer":"F"}` Une fonction membre const est une fonction qui retourne toujours le même résultat. #) :quiz:`{"type":"TF","answer":"F"}` Depuis une fonction membre const, je ne peux accéder qu'aux données const de l'objet. #) :quiz:`{"type":"TF","answer":"T"}` Une const référence garantit que l’objet référencé ne pourra jamais être modifié. #) :quiz:`{"type":"TF","answer":"T"}` Il est possible de convertir implicitement une référence non const en const référence . #) :quiz:`{"type":"TF","answer":"F"}` Le code ci-dessous comporte une erreur : .. code-block:: void ConstRef(const int& x); int a = 5; int& ref = a; ConstRef(ref); #) :quiz:`{"type":"TF","answer":"T"}` Le code ci-dessous comporte une erreur : .. code-block:: 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. .. code-block:: 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** (:cppterm:`local object`) dont la durée de vie est limitée au bloc de cette fonction. Object temporaire ----------------- On appelle **objet temporaire** (:cppterm:`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 : .. code-block:: cout << Matrix(2,2); - L’objet string est temporaire : .. code-block:: print( string("bonjour").length() ); - L’expression *A + B* produit un objet temporaire de type *Matrix* : .. code-block:: 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é : .. panels:: :column: col-lg-10 p-2 **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 : .. code-block:: 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. .. code-block:: 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é --------- .. list-table:: Les retours :header-rows: 1 :widths: 15 10 20 10 45 * - Syntaxe - Nature - Modifiable ? - Copie ? - Usage * - :cpp:`T f()` - Objet - Oui - | retourne une copie de la donnée membre | ou retourne l'objet local-temporaire - Retourne une valeur indépendante * - :cpp:`T& f()` - Référence - Oui - Non - Retourne une référence vers un objet pérenne * - :cpp:`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 : .. code-block:: Matrix transpose(Matrix & M) {...} M2 = transpose(M1); // OK Cependant, en écrivant la ligne ci-dessous, le programme produit une erreur de compilation : .. code-block:: T = transpose(M1 + M2); # error Lors de l'appel de la fonction :cpp:`transpose()`, le paramètre :cpp:`M` est une référence recevant comment argument l'objet temporaire :cpp:`M1+M2`, **ce qui n’est pas autorisé**. Il existe toutefois une exception : .. panels:: :column: col-lg-10 p-2 **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 : .. code-block:: 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 :cpp:`<<` utilisé pour afficher une matrice sur :cpp:`cout` : .. code-block:: ostream& operator<< (ostream& os, Matrix & mat) { ... return os; } Matrix M1; cout << M1; Les tests sont moyennement satisfaisants : .. code-block:: cout << M1; OK cout << M1+M2; ERROR La solution consiste donc à utiliser une **const reference** lors de la définition de l'opérateur :cpp:`<<` : .. code-block:: ostream& operator << (ostream& os, const Matrix & mat) ... Ce qui résout le problème. En résumé --------- .. list-table:: Passage des paramètres :header-rows: 1 :widths: 20 30 25 25 * - Syntaxe - Usage - Peut Modifier l'argument - Accepte un objet temporaire * - :cpp:`... fnt(Matrix& M)` - Modifier l'objet original - Oui - Non * - :cpp:`... fnt(const Matrix& M)` - Lire un objet sans le copier - Non - Oui * - :cpp:`... 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 : - :cppterm:`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. - :cppterm:`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 (:cppterm:`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 :cppterm:`getter` pour lire cette donnée en lecture seule * de créer un :cppterm:`setter` pour la modifier à condition que la nouvelle valeur soit considérée comme valide .. code-block:: 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 : .. code-block:: cout << l.getvalue(i); l.setvalue(i,5.2); Les utilisateurs sont parfois habitués à une syntaxe plus naturelle comme : .. code-block:: 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 : .. code-block:: 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 : .. code-block:: 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 : .. dropdown:: Réponse Lors de l'appel à la fonction :cpp:`print()`, comme cette fonction n'a pas vocation à modifier les valeurs dans la liste, elle accepte donc une const référence. Seulement, l'opérateur :cpp:`[]` est une :cpp:`non const function member`, donc non accessible depuis une const référence, ainsi l'écriture :cpp:`cout << L[i]` pose problème. Dans cette configuration, il faut donc définir en parallèle une version const de l'opérateur :cpp:`[]`. Cette version retourne une référence constante vers l'élément indexé ce qui valide le fait que l'opérateur :cpp:`[]` ne permet pas de modifications. Voici donc sa définition : .. code-block:: const double & liste::operator [ ] (int i) const { ... } Ainsi, dans le code ci-dessous: .. code-block:: print(l); l[i] = 5.2; * La fonction :cpp:`print()` utilise l'opérateur :cpp:`[]` dans sa version const * L'affectation :cpp:`l[i]` utilise l'opérateur :cpp:`[]` dans sa version non const ! .. note:: Lorsque l'on dispose d'une référence vers cette liste, l'appel à l'opérateur :cpp:`[]` pose problème, car on peut choisir la version const ou non const ! Comment fonctionne le mécanisme de surcharge (:cppterm:`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 :cpp:`[]` 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 ====== .. quiz:: WhatsCpp2 :title: Parameters Indiquez si les affirmations suivantes sont vraies ou fausses : #) :quiz:`{"type":"TF","answer":"F"}` Depuis une fonction, on doit retourner une const référence si l'on retourne un objet temporaire. #) :quiz:`{"type":"TF","answer":"T"}` Depuis une fonction, il ne faut pas retourner une référence sur un objet temporaire créé dans cette fonction. #) :quiz:`{"type":"TF","answer":"F"}` Depuis une fonction, si je crée un objet local, je peux le retourner par référence (non const). #) :quiz:`{"type":"TF","answer":"T"}` Depuis une fonction, retourner un objet par valeur est valide. #) :quiz:`{"type":"TF","answer":"F"}` Un paramètre de type référence (non const) peut accepter un objet temporaire comme argument. #) :quiz:`{"type":"TF","answer":"T"}` Depuis une fonction, on peut retourner une réference (non const) sur un objet pérenne. #) :quiz:`{"type":"TF","answer":"T"}` Pour passer un objet temporaire en argument, on peut utiliser un paramètre const reference. #) :quiz:`{"type":"TF","answer":"T"}` Pour passer un objet temporaire en argument, on peut utiliser un paramètre copie. #) :quiz:`{"type":"TF","answer":"T"}` Depuis une fonction, pour modifier un objet passé en argument, il faut un paramètre const référence. #) :quiz:`{"type":"TF","answer":"T"}` 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 .. code-block:: 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 :cpp:`V10` et recopie l’ensemble de ses valeurs. * :cpp:`size()` : retourne la taille du tableau sans modifier l’objet. * :cpp:`get(i)` : retourne la valeur située à l’indice **i**. * :cpp:`set(i, value)` : modifie la valeur située à l’indice :cpp:`i`. * :cpp:`print()` : affiche le contenu du tableau sans modifier l’objet. * :cpp:`equals(...)` : indique si l’objet courant contient les mêmes valeurs que l’objet passé en paramètre. * :cpp:`operator[]` : fournir une version **const** et une version :cpp:`non const`. * :cpp:`find(value)` : retourne l’indice de la première case contenant :cpp:`value`, ou :cpp:`-1` si la valeur n’est pas présente. * :cpp:`copyFrom(...)` : copie le contenu d’un autre objet :cpp:`V10` sans modifier l’objet source. * :cpp:`operator=` : copie le contenu d’un objet :cpp:`V10` dans l’objet courant sans modifier l’objet source. * Veillez à utiliser le mot clef :cpp:`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 : :download:`test.txt ` .. warning:: :cppterm:`IA or not IA, that is the question.` Ces exercices sont au programme de l'examen.