Module 2 - cours du 1er septembre (quiz 1)

Méthodes constantes, passage de paramètres, vecteurs

Leçons Moodle « Méthodes constantes », « Passage de paramètres », « Vecteurs » | Vidéo « Passage des paramètres en programmation orientée objet » | Mis en pratique au TP1 (paramètres par valeur, référence constante, pointeur, flux ; vecteurs de pointeurs)

constPar valeur, par référence, par pointeurRéférence constanteRésolution de surchargestd::vectorBoucle for sur un intervalle

Trois questions à se poser pour chaque paramètre et chaque méthode : est-ce qu'on copie ? est-ce qu'on modifie ? est-ce qu'on promet de ne pas modifier ? Le mot-clé const répond à la troisième, et le compilateur vérifie la promesse.

1. Méthodes constantes

Une méthode déclarée avec const après sa liste de paramètres promet de ne pas modifier l'objet sur lequel elle est appelée. Dans son corps, l'objet courant est vu comme constant.

Ce qu'une méthode const ne peut pas faire
  • modifier un attribut (charge_ = 0; → erreur « cannot assign to non-static data member within const member function ») ;
  • appeler une méthode non constante de l'objet courant ou de ses attributs objets ;
  • retourner une référence (ou un pointeur) non constante vers l'objet lui-même ou vers un attribut (cela ouvrirait une porte de modification).

Elle peut par contre modifier ses paramètres et ses variables locales, même s'ils sont de la même classe : la constance porte sur *this seulement.

Pourquoi ? Un objet constant (const Moteur m(...)) et une référence constante (const Moteur& m, le mode de passage le plus courant) ne peuvent appeler que des méthodes constantes. Si getNom() n'est pas const, un paramètre const Robot& r ne peut même pas faire r.getNom(). Règle pratique : tout accesseur et toute méthode d'affichage ou de calcul est const.

Pièges
  • Une méthode const peut retourner une copie d'un attribut ou une const T&, jamais une T&. Les deux versions T& get() et const T& get() const peuvent coexister (surcharge sur la constance) : le compilateur choisit selon que l'objet est constant ou non.
  • const fait partie de la signature : si le .h dit const, le .cpp doit le répéter, sinon vous définissez une autre méthode (erreur « out-of-line definition does not match »).
  • Un attribut mutable peut être modifié par une méthode const (rare, pour un cache ou un compteur d'accès).

2. Passage de paramètres

ModeSyntaxeCopie ?L'appelé peut modifier l'original ?Quand l'utiliser
Par valeurvoid f(Robot r)oui (constructeur de copie)non, il travaille sur la copietypes fondamentaux (int, double, bool), petits objets, ou quand on veut justement une copie à modifier
Par référencevoid f(Robot& r)non (alias)ouiparamètre de sortie, objet à modifier (swap, flux ostream&)
Par référence constantevoid f(const Robot& r)nonnon (seules les méthodes const sont utilisables)le mode par défaut pour tout objet qu'on ne veut que lire : string, vector, classes du TP
Par pointeurvoid f(Robot* r)copie de l'adresse seulementoui (via r->)quand « pas d'objet » (nullptr) est une valeur possible ; transfert de propriété (TP1 : installerBatterie(Batterie*)) ; tableaux C
Par pointeur constantvoid f(const Robot* r)adressenonobservation d'un objet optionnel
Référence constante : pourquoi c'est le choix par défaut

Référence : on évite de copier l'objet (temps et mémoire ; une copie d'un vecteur de 10 000 éléments copie les 10 000). Constante : on empêche de modifier l'original, et on peut passer des temporaires et des objets constants. Utilisez const T& partout où vous auriez écrit T sans avoir besoin de modifier le paramètre.

Pas à pas : valeur, référence, référence constante

Même objet, trois façons de le passer. Observez ce qui est copié et ce qui est modifié.

Retourner une valeur

Résolution de surcharge

Plusieurs fonctions peuvent porter le même nom si leurs paramètres diffèrent (nombre ou types). Le compilateur choisit d'après le type des arguments, à la compilation. Le type de retour ne compte pas. Un appel qui correspond aussi bien à deux surcharges est ambigu : erreur de compilation.

Quelle surcharge est appelée ? (intra H18, question 1f)

Quatre fonctions tester sont déclarées. Choisissez un appel et vérifiez votre réponse.

3. Pointeur ou référence ?

Référence T&Pointeur T*
Doit désigner un objettoujours, dès sa déclaration (Robot& r = a;)non : peut valoir nullptr
Peut changer de ciblenonoui (p = &b;)
Accès aux membresr.getNom()p->getNom()
Arithmétiquenonoui (p + 1, tableaux)
Dans le coursparamètres et retours de fonctionsallocation dynamique, agrégation, tableaux, polymorphisme

Attention aux deux sens du symbole & : dans une déclaration (Robot& r, void f(const Robot&)) il signifie « référence » ; dans une expression (&q) il signifie « adresse de ». De même * : « pointeur » dans une déclaration, « objet pointé » dans une expression.

4. Les vecteurs de la bibliothèque standard

std::vector<T> (en-tête <vector>) est un tableau dynamique qui gère lui-même sa mémoire : il grandit au besoin, se copie en profondeur et libère ses éléments dans son destructeur. Dans les TP, il remplace les tableaux new T[n].

Pièges de vector
  • v[i] ne vérifie rien : lire ou écrire hors de [0, size()[ est un comportement indéfini, même si la capacité est plus grande. vector<int> v(9); v[9] = 7; est une erreur.
  • push_back peut réallouer : tous les pointeurs, références et itérateurs vers les éléments deviennent invalides. Ne gardez pas &v[i] pendant qu'on ajoute.
  • Passer un vecteur par valeur le copie en entier. Paramètres : const vector<T>& (lecture) ou vector<T>& (modification).
  • size() retourne un size_t (non signé) : v.size() - 1 sur un vecteur vide donne un nombre gigantesque, et comparer avec un int provoque -Wsign-compare.
  • Un vector<Moteur*> détruit ses pointeurs, pas les objets pointés : avec des pointeurs bruts, c'est à vous de faire les delete (ou d'utiliser des pointeurs intelligents, module 3).

Simulateur de std::vector<int>

Exécutez des opérations et observez la taille, la capacité (croissance typique par doublement) et les erreurs d'indice. Les cases en pointillé sont allouées mais inutilisées.

Retirer un élément en conservant l'ordre (TP1 : retirerMoteur)

Avec les algorithmes de la STL (module 9), la recherche s'écrit auto it = find(moteurs_.begin(), moteurs_.end(), moteur); puis if (it != moteurs_.end()) moteurs_.erase(it);.

5. À retenir

Quiz rapide - Module 2

Que signifie le const dans double getMasse() const; ?

  • La valeur retournée ne peut pas être modifiée par l'appelant
  • La méthode ne peut être appelée qu'une fois
  • La méthode ne modifie pas l'objet sur lequel elle est appelée
  • Les paramètres de la méthode sont constants
Le const après la signature porte sur *this. Un const devant le type de retour (const string& getNom() const) porterait, lui, sur la valeur retournée.

Dans une fonction void f(const Robot& r), lequel de ces appels compile ?

  • r.setNom("x"); (setNom n'est pas const)
  • r.getNom(); (getNom est const)
  • r = Robot("y");
  • Tous les trois
Sur une référence constante, seules les méthodes constantes sont autorisées, et l'affectation est interdite.

Une méthode const peut-elle modifier une variable locale de type Robot déclarée dans son corps ?

  • Oui : la constance ne porte que sur l'objet courant
  • Non : tout Robot est constant dans une méthode const
  • Seulement si la variable est déclarée mutable
  • Seulement si Robot n'a pas d'attributs
Les paramètres et variables locales ne sont pas concernés, même s'ils sont de la même classe que l'objet courant.

Quel mode de passage choisir pour un paramètre std::string qu'on ne fait que lire ?

  • string s
  • string& s
  • string* s
  • const string& s
Pas de copie (référence) et pas de modification possible (const). C'est la convention des TP : « le nom est transmis par référence constante ».

Combien de fois le constructeur de copie de Robot est-il appelé ?

  • 2
  • 3
  • 4
  • 5
Robot b = a (1), f(a) par valeur (2), Robot c(b) (3). g(a) passe par référence constante : aucune copie.

Pourquoi ce code est-il dangereux ?

  • Il ne compile pas
  • La concaténation copie les chaînes
  • Il retourne une référence vers une variable locale détruite à la sortie
  • Les paramètres devraient être passés par valeur
Il faut retourner par valeur (string nomComplet(...)). Une référence vers un objet détruit est un pointeur pendant déguisé.

Avec void tester(Jouet& p) et void tester(Jouet p) toutes deux déclarées, l'appel tester(q) avec Jouet q; :

  • Ne compile pas : appel ambigu
  • Appelle la version par référence
  • Appelle la version par valeur
  • Appelle les deux
Référence et valeur conviennent aussi bien : le compilateur ne peut pas trancher.

Qu'affiche ce programme ?

  • 9
  • 11
  • 12
  • 10
9, +1 = 10, +1 = 11, -3 = 8, +1 = 9, +1 = 10. Les affectations v[i] = ... ne changent pas la taille (et v[9] = 7 est valide seulement parce que la taille est redevenue 10). Question de l'intra H15.

Quelle différence entre v[i] et v.at(i) ?

  • Aucune
  • at vérifie l'indice et lance std::out_of_range ; [] ne vérifie rien
  • [] vérifie l'indice, at non
  • at retourne une copie, [] une référence
Les deux retournent une référence. Seul at est vérifié (plus lent, mais sûr).

Pour parcourir un vector<Robot> robots sans copier chaque robot ni le modifier, on écrit :

  • for (Robot r : robots)
  • for (Robot& r : robots)
  • for (const Robot& r : robots)
  • for (Robot* r : robots)
La première copie chaque élément, la deuxième permet de modifier, la quatrième ne compile pas (les éléments sont des objets, pas des pointeurs).

Que fait v.erase(v.begin() + 2); ?

  • Retire l'élément d'indice 2 et décale les suivants vers la gauche
  • Retire les 2 premiers éléments
  • Retire l'élément de valeur 2
  • Met l'élément d'indice 2 à zéro
erase prend un itérateur ; begin() + i désigne l'indice i. La taille diminue de 1 et l'ordre des autres est conservé.

Que se passe-t-il quand push_back dépasse la capacité du vecteur ?

  • Une exception est lancée
  • Un bloc plus grand est alloué, les éléments y sont copiés, l'ancien bloc est libéré ; les anciens pointeurs vers les éléments deviennent invalides
  • L'élément est refusé
  • Le vecteur est vidé
C'est l'algorithme « agrandir un tableau » du module 1, automatisé. D'où l'invalidation des pointeurs et itérateurs.

Un vector<Capteur*> est détruit. Que deviennent les capteurs pointés ?

  • Ils sont détruits automatiquement
  • Ils sont copiés dans un autre vecteur
  • Rien : seuls les pointeurs disparaissent, les objets restent alloués
  • Le programme plante
Le vecteur possède des pointeurs, pas des objets. Si personne ne fait delete, c'est une fuite. C'est la situation du TP1 (agrégation).

Que vaut v.size() - 1 si v est vide ?

  • -1
  • 0
  • Une exception
  • Un très grand nombre positif (débordement non signé)
size() retourne un size_t, non signé : 0 - 1 déborde vers la valeur maximale. Écrire i + 1 < v.size() plutôt que i < v.size() - 1.

Quelle signature permet à f d'accepter nullptr comme argument ?

  • void f(Batterie* b)
  • void f(Batterie& b)
  • void f(const Batterie& b)
  • void f(Batterie b)
Une référence doit toujours désigner un objet. Seul un pointeur peut représenter « aucun objet » (c'est l'usage dans installerBatterie(nullptr) du TP1 pour retirer la batterie).

Exercices à développement

Intra H15Copies

Compter les appels au constructeur de copie

La classe Dragon est fonctionnelle et possède tous les constructeurs nécessaires. Combien de fois son constructeur de copie est-il appelé ?

Indice

Le constructeur de copie sert à créer un objet à partir d'un autre : déclaration avec initialisation, paramètre par valeur, retour par valeur. Une affectation entre deux objets existants appelle operator=, pas le constructeur de copie.

Solution

5 appels.

  1. Dragon dragon2(dragon1); : 1.
  2. Dragon dragon3 = dragon1; : 2 (c'est une initialisation, pas une affectation).
  3. dragon3 = dragon2; : opérateur d'affectation, pas de copie.
  4. fonction1(dragon1); : paramètre par valeur, 3. (Dragon dragon2; à l'intérieur : constructeur par défaut.)
  5. fonction2(dragon1) : le paramètre est une référence constante (0) ; le return *new Dragon() construit la valeur de retour par copie de l'objet dynamique : 4. Puis dragon4 = ... est une affectation. Au passage, l'objet créé par new n'est jamais libéré : fuite.
  6. fonction1(dragon4); : 5.
ConstCorrection d'erreurs

Rendre la classe const-correcte

Ce code ne compile pas. Trouvez les erreurs liées à const et corrigez la déclaration de la classe pour que main fonctionne sans retirer aucun const de main.

Solution
  • afficher reçoit une const Pince& : getNom et getConsommationW doivent être const, sinon « member function not viable: 'this' argument has type const Pince ».
  • setFermee déclaré const ne peut pas écrire fermee_ = f. On retire le const. Il reste appelable sur mobile (non constant).
Intra H16Vecteurs

Achats::Ajout

Soit la fonction membre Ajout de la classe Achats, qui reçoit le nom de l'item (string) et la quantité (int) et l'insère dans le vecteur mesAchats_. Donnez uniquement l'implémentation de Ajout.

Solution

On construit un Item temporaire et push_back le copie dans le vecteur. (mesAchats_.emplace_back(nom, quantite); construit l'élément directement dans le vecteur.) Remarque : getItems() retourne le vecteur par valeur, donc une copie complète à chaque appel ; const vector<Item>& serait préférable.

VecteursTP1

Masse et consommation d'un robot

Le robot du TP1 possède Batterie* batterie_, Chassis* chassis_ (peuvent être nullptr) et des vecteurs vector<Moteur*> moteurs_, vector<Capteur*> capteurs_, vector<Pince*> pinces_. Écrivez double Robot::getMasse() const (somme des masses de toutes les pièces présentes) et double Robot::getConsommationW() const (somme des consommations des moteurs, capteurs et pinces ; la batterie et le châssis ne consomment pas). Chaque pièce a getMasseKg() et, sauf la batterie et le châssis, getConsommationW().

Solution

Les deux méthodes sont const (elles ne modifient pas le robot) et n'appellent que des méthodes const des pièces. Les boucles sur intervalle avec const Moteur* copient seulement le pointeur.

ParamètresConception

Choisir le bon mode de passage

Pour chaque fonction, proposez la signature la plus appropriée (valeur, référence, référence constante, pointeur) et justifiez en une phrase.

  1. echanger : échange le contenu de deux robots.
  2. estPlusLourd : compare la masse de deux robots et retourne un booléen.
  3. installerBatterie (TP1) : donne au robot une batterie allouée avec new, ou aucune.
  4. afficher : écrit la description d'un robot dans un flux de sortie.
  5. setCharge : règle la charge d'un moteur avec un double.
  6. nomsDesMoteurs : retourne la liste des noms des moteurs d'un robot.
Solution
  1. void echanger(Robot& a, Robot& b) : on doit modifier les deux originaux.
  2. bool estPlusLourd(const Robot& a, const Robot& b) : lecture seule, sans copier les robots.
  3. void Robot::installerBatterie(Batterie* batterie) : « aucune » se représente par nullptr, et le pointeur matérialise le transfert de propriété (le robot fera le delete).
  4. void Robot::afficher(std::ostream& out) const : un flux se passe toujours par référence (il n'est pas copiable, et écrire dedans le modifie) ; la méthode est const.
  5. bool Moteur::setCharge(double charge) : un double se passe par valeur.
  6. vector<string> Robot::nomsDesMoteurs() const : un nouveau vecteur est construit dans la fonction, on le retourne par valeur (jamais par référence : il serait détruit).
VecteursAlgorithme

Insertion triée et suppression

Écrivez void insererTrie(vector<int>& v, int val) qui insère val dans un vecteur déjà trié en ordre croissant, en gardant le tri, et void supprimerValeur(vector<int>& v, int val) qui retire toutes les occurrences de val.

Indice

Pour la suppression, attention à l'indice après un erase : l'élément suivant a pris la place de celui qu'on vient de retirer, il ne faut pas l'enjamber.

Solution

Version STL (module 9) : v.insert(lower_bound(v.begin(), v.end(), val), val); et v.erase(remove(v.begin(), v.end(), val), v.end());.