Programmation • IoT

Chapitre 10 – Rule of Zero

Document réservé

Vous consultez actuellement la présentation publique de ce chapitre.

Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.

Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).

10.2 Une classe qui gère directement de la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . 10.4 Il faut programmer la copie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.6 L’arrivée de la sémantique de déplacement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.7 Un constructeur de déplacement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.9 De la Rule of Three à la Rule of Five . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.10 Changeons maintenant la représentation interne . . . . . . . . . . . . . . . . . . . . . . . . . 10.12 Le constructeur de copie a disparu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.13 L’affectation par copie a disparu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.14 Le déplacement a également disparu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.16 Zero ne signifie pas zéro constructeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.17 Déléguer la gestion des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.18 Un exemple avec une matrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.21 Rule of Zero et encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.22 Moins de code, moins de possibilités d’erreur . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.23 Faut-il alors encore apprendre la gestion manuelle ? . . . . . . . . . . . . . . . . . . . . . . . 10.24 De la gestion manuelle à la Rule of Zero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.25 Une règle de conception, pas une obligation . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.26 Conclusion de la Partie IV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2026 – C++ Partie III
Accueil
Dans les chapitres précédents, nous avons étudié plusieurs mécanismes liés à la gestion des ressources : • allocation et libération de mémoire ; • constructeurs et destructeurs ; • copie d’objets ; • sémantique de déplacement ; • pointeurs intelligents ; • conteneurs de la bibliothèque standard. Nous allons maintenant réunir ces notions autour d’un principe de conception particulièrement important Son idée peut sembler surprenante : Une classe devrait, lorsque cela est possible, ne définir elle-même aucune fonction membre spéciale destinée à gérer ses ressources. Pour comprendre pourquoi cette règle est intéressante, revenons d’abord à la gestion manuelle d’une Une classe qui gère directement de la mémoire Considérons une classe représentant un tableau dynamique : explicit Tableau(std::size_t n) donnees(new double[n]) 2026 – C++ Partie III Cette classe possède directement une zone de mémoire obtenue avec : et elle doit donc la libérer dans son destructeur avec l’instruction À première vue, tout semble correct, mais la copie d’un objet va immédiatement poser un problème. Le problème de la copie Si nous ne définissons pas nous-mêmes le constructeur de copie, le compilateur effectue une copie membre par membre. Si le membre est copié sans difficulté, par contre le membre : est lui aussi simplement copié. C’est donc l’adresse contenue dans le pointeur qui est copiée et pas le tableau. Nous obtenons conceptuellement : a.donnees −−−−+ +−−−−> zone de memoire b.donnees −−−−+ Les deux objets possèdent alors un pointeur vers la même zone de mémoire et lorsque les objets sont détruits, chacun exécute : La même zone de mémoire risque donc d’être libérée deux foi et le comportement du programme devient Il faut programmer la copie Notre classe doit donc définir un constructeur de copie réalisant une copie profonde :
Accueil
2026 – C++ Partie III Tableau(const Tableau& autre) : taille(autre.taille), donnees(new double[autre.taille]) for (std::size_t i = 0; i < taille; ++i) donnees[i] = autre.donnees[i]; Cette fois, chaque objet possède sa propre zone mémoire : a.donnees −−−−−−−−> zone A b.donnees −−−−−−−−> zone B Mais ce n’est pas encore suffisant, il faut également traiter correctement l’affectation Il faut donc réfléchir à l’opérateur d’affectation par copie : Tableau& operator=(const Tableau& autre); Historiquement, cette situation conduit à la Rule of Three c.-à-d. lorsqu’une classe doit définir explicitement l’une des trois fonctions suivantes : • constructeur de copie ; • opérateur d’affectation par copie ; il faut généralement se demander si elle ne doit pas définir également les deux autres. Tableau(const Tableau& autre); Tableau& operator=(const Tableau& autre); Ces trois fonctions sont liées au fait que la classe gère directement une ressource. • La Rule of Three n’est pas une règle syntaxique imposée par le compilateur. • C’est une règle de conception. • Si une classe doit gérer explicitement la destruction d’une ressource, sa copie demande généralement elle aussi une attention particulière.
Accueil
2026 – C++ Partie III L’arrivée de la sémantique de déplacement Depuis C++11, nous disposons également de la sémantique de déplacement et notre classe doit donc également considérer : Tableau(Tableau&& autre); Tableau& operator=(Tableau&& autre); Le constructeur de déplacement peut transférer la ressource au lieu de copier tous les éléments. autre.donnees −−−−> zone memoire autre.donnees −−−−> nullptr nouveau.donnees −−> zone memoire La zone mémoire n’a pas été copiée mais sa propriété a été transférée. Un constructeur de déplacement Une implémentation possible serait Tableau(Tableau&& autre) noexcept : taille(autre.taille), donnees(autre.donnees) autre.donnees = nullptr; Le nouvel objet récupère : puis l’ancien objet est placé dans un état valide ne possédant plus la ressource : autre.donnees = nullptr; Lorsque son destructeur sera appelé : delete[] autre.donnees; la valeur nullptr ne provoquera aucune libération de la ressource transférée. Tableau(Tableau&& autre) noexcept
Accueil
2026 – C++ Partie III indique que cette opération ne doit pas laisser s’échapper d’exception. Un déplacement consistant simplement à transférer un pointeur et quelques valeurs peut effectivement être réalisé sans allocation supplémentaire. Cette propriété est également importante pour certains conteneurs de la bibliothèque standard, qui peuvent préférer déplacer les objets lors d’une réallocation lorsqu’ils savent que leur déplacement ne déclenchera pas d’exception. Nous reviendrons sur les exceptions dans le chapitre qui leur est consacré. De la Rule of Three à la Rule of Five Avec la sémantique de déplacement, les trois fonctions deviennent cinq : Tableau(const Tableau& autre); Tableau& operator=(Tableau&& autre) noexcept; Tableau& operator=(const Tableau& autre); Tableau(Tableau&& autre) noexcept; La classe qui gère directement une ressource doit donc considérer : • sa destruction ; • son affectation par copie ; • son déplacement ; • son affectation par déplacement. Cela représente beaucoup de code et chaque ligne supplémentaire de gestion manuelle constitue une possibilité supplémentaire d’introduire une erreur. Changeons maintenant la représentation interne Notre classe contient actuellement : Mais la bibliothèque standard possède déjà un objet conçu pour gérer un tableau dynamique : Nous pouvons donc réécrire notre classe :
Accueil
2026 – C++ Partie III std::vector<double> donnees; explicit Tableau(std::size_t n) Et quelque chose de remarquable vient de se produire. Le destructeur a disparu Nous n’avons plus besoin d’écrire : Pourquoi ? Parce que lorsque l’objet Tableau est détruit, son membre : est automatiquement détruit. Or std::vector sait déjà libérer la mémoire qu’il possède donc nous déléguons donc la gestion de cette ressource au vector. Le constructeur de copie a disparu Le compilateur peut générer automatiquement le constructeur de copie de Tableau qui copie les membres Le membre principal est : std::vector<double> donnees; Or std::vector sait déjà se copier correctement donc chaque Tableau obtient donc son propre ensemble L’affectation par copie a disparu peut utiliser l’opérateur d’affectation généré automatiquement pour Tableau car celui-ci utilise l’opérateur d’affectation de ses membres et le vector sait déjà effectuer correctement cette opération.
Accueil
2026 – C++ Partie III Le déplacement a également disparu Nous n’avons plus besoin d’écrire manuellement : Tableau(Tableau&& autre); Tableau& operator=(Tableau&& autre); Le compilateur peut générer les opérations appropriées à partir de celles des membres de la classe donc le vector sait déjà gérer efficacement le déplacement de ses ressources et de ce fait, nous bénéficions de sa sémantique de déplacement sans devoir manipuler nous-mêmes son allocation dynamique. Nous arrivons maintenant à la : L’idée consiste à concevoir autant que possible les classes de manière à ne pas devoir écrire explicitement les fonctions membres spéciales de gestion des ressources. std::vector<double> donnees; explicit Tableau(std::size_t n) ne définit explicitement : • aucun destructeur ; • aucun constructeur de copie ; • aucun opérateur d’affectation par copie ; • aucun constructeur de déplacement ; • aucun opérateur d’affectation par déplacement. D’où le nom Rule of Zero. Zero ne signifie pas zéro constructeur Il faut éviter une confusion importante. La Rule of Zero ne signifie absolument pas qu’une classe ne doit posséder aucun constructeur car notre classe possède bien :
Accueil
2026 – C++ Partie III explicit Tableau(std::size_t n) Ce constructeur exprime la manière dont un objet Tableau doit être créé et nous pouvons naturellement avoir d’autres constructeurs correspondant aux besoins de la classe. La Rule of Zero concerne les fonctions membres spéciales liées à la gestion de la durée de vie, de la copie et du déplacement des ressources. Déléguer la gestion des ressources La Rule of Zero repose sur une idée fondamentale : Une classe ne devrait pas gérer directement une ressource si elle peut confier cette responsabilité à un objet conçu pour cela. Pour de la mémoire dynamique, nous pouvons par exemple utiliser : Ces objets connaissent leurs propres règles de destruction, de copie ou de déplacement et notre classe peut alors se concentrer sur ce qu’elle représente réellement. Un exemple avec une matrice Considérons une classe représentant une matrice. Une implémentation utilisant directement la mémoire dynamique pourrait contenir : std::size_t colonnes; double* coefficients; Cette classe doit alors gérer explicitement la mémoire associée à : et donc réfléchir à sa destruction, sa copie et son déplacement. Une conception moderne peut utiliser : std::size_t colonnes; std::vector<double> coefficients;
Accueil
2026 – C++ Partie III Matrice(std::size_t l, std::size_t c) La classe Matrice ne gère plus directement l’allocation de mémoire car cette responsabilité appartient au : La classe peut maintenant se concentrer sur les opérations propres aux matrices : • accès à un coefficient ; • multiplication ; • calculs particuliers. La Rule of Zero est une conséquence naturelle de RAII. Avec une gestion manuelle : +−−−−> double∗ +−−−−> memoire dynamique la classe Matrice est directement responsable de la ressource +−−−−> vector<double> +−−−−> memoire dynamique la classe Matrice possède un objet RAII et le vector acquiert, possède et libère sa propre ressource, de plus la durée de vie de la mémoire dynamique est attachée à la durée de vie du vector, elle-même attachée à celle de la Matrice. Et avec un unique_ptr ? Toutes les ressources ne sont pas nécessairement représentées par un conteneur et une classe peut par std::unique_ptr<Ressource> ressource; Elle n’a pas besoin d’écrire un destructeur uniquement pour effectuer :
Accueil
2026 – C++ Partie III Le unique_ptr s’en charge automatiquement mais il faut cependant comprendre une conséquence importante, un unique_ptr n’est pas copiable. Une classe contenant un unique_ptr ne sera donc pas automatiquement copiable simplement parce que nous appliquons la Rule of Zero mais en revanche, elle pourra naturellement bénéficier de la sémantique de déplacement du unique_ptr. • La Rule of Zero ne signifie pas que toutes les classes deviennent automatiquement copiables • Les opérations possibles pour une classe dépendent naturellement des opérations possibles • C’est précisément ce que nous voulons : les propriétés de la classe découlent de la nature des objets qui la composent. Rule of Zero et encapsulation La Rule of Zero renforce également l’encapsulation. Une classe représentant une matrice ne devrait idéalement pas avoir pour préoccupation principale : Comment vais-je libérer ce tableau de double ? Mais devrait plutôt s’occuper de questions comme : Comment accéder à un coefficient ? Comment multiplier deux matrices ? Quelles dimensions rendent une opération possible ? La gestion technique de la mémoire est confiée à un composant spécialisé et la classe peut alors se concentrer sur son propre domaine. Moins de code, moins de possibilités d’erreur Comparons les deux approches. Avec une gestion manuelle, il faut correctement programmer : Matrice(const Matrice&); Matrice& operator=(const Matrice&); Matrice(Matrice&&) noexcept;
Accueil
2026 – C++ Partie III Matrice& operator=(Matrice&&) noexcept; Il faut notamment éviter : • les fuites de mémoire ; • les doubles libérations ; • les copies superficielles accidentelles ; • les problèmes d’auto-affectation ; • les états invalides après déplacement ; • les erreurs lors d’une exception. std::vector<double> coefficients; la bibliothèque standard prend en charge une grande partie de ces problèmes et le code devient non seulement plus court, mais surtout plus facile à rendre correct. Faut-il alors encore apprendre la gestion manuelle ? On pourrait se demander pourquoi nous avons étudié ainsi que les destructeurs, la copie profonde et la sémantique de déplacement, si le but final consiste souvent à ne plus les manipuler directement mais la réponse est fondamentale : Sans comprendre la gestion manuelle des ressources, il est difficile de comprendre : • ce que RAII résout ; • pourquoi un destructeur est nécessaire ; • pourquoi une copie superficielle peut être dangereuse ; • pourquoi unique_ptr interdit la copie ; • ce que signifie réellement déplacer une ressource ; • pourquoi vector simplifie autant la conception d’une classe. Le C++ moderne ne fait donc pas disparaître ces mécanismes mais permet de les encapsuler dans des composants fiables afin de ne pas devoir les reprogrammer continuellement. De la gestion manuelle à la Rule of Zero Nous pouvons maintenant résumer l’évolution : Gestion directe d’une ressource
Accueil
2026 – C++ Partie III destruction + copie + deplacement objets gestionnaires vector, string, unique_ptr, … La Rule of Zero ne supprime donc pas les principes précédents mais en est l’aboutissement. Une règle de conception, pas une obligation Comme la Rule of Three et la Rule of Five, la Rule of Zero n’est pas une règle imposée par le langage. Certaines classes doivent réellement gérer des ressources de bas niveau, c’est notamment le cas lorsqu’on écrit soi-même un composant RAII chargé d’encapsuler une ressource particulière. Une telle classe peut avoir besoin d’un destructeur et de fonctions spéciales explicitement définies. Mais une fois cette ressource correctement encapsulée, les classes de plus haut niveau devraient autant que possible utiliser cet objet plutôt que reprendre elles-mêmes la gestion de la ressource. On peut donc distinguer deux rôles : 1. les classes chargées de gérer une ressource ; 2. les classes qui utilisent ces gestionnaires de ressources. La Rule of Zero concerne particulièrement le second cas. Conclusion de la Partie IV Nous avons commencé cette partie en étudiant RAII : La durée de vie d’une ressource est associée à la durée de vie d’un objet. Nous avons ensuite étudié la sémantique de déplacement, les pointeurs intelligents, les conteneurs, les itérateurs, les algorithmes et les expressions lambda. La Rule of Zero permet maintenant de réunir plusieurs de ces idées. Le but du C++ moderne n’est pas de faire disparaître les ressources, les pointeurs, les allocations, les copies ou les déplacements, mais consiste notamment à construire des abstractions qui permettent de gérer ces mécanismes une fois correctement, puis de les utiliser sans avoir à les reproduire partout dans le programme.
Accueil
2026 – C++ Partie III • Une bonne connaissance de la gestion des ressources permet de comprendre comment l’effectuer correctement. • Une bonne conception permet ensuite, très souvent, de ne plus avoir à l’effectuer directement. • C’est l’idée fondamentale de la Rule of Zero.
Accueil
P4-Chap-10 Rule of Zero
Accueil
Termes à ajouter au glossaire
Contenu

Inscription

×
Cancel