Programmation • IoT

Chapitre 1 – RAII et gestion moderne des ressources

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.).

1 RAII et gestion moderne des ressources 1.1 Pourquoi revenir sur la gestion des ressources ? . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.3 RAII ne signifie pas “tout mettre sur la pile” . . . . . . . . . . . . . . . . . . . . . . 1.4 RAII et les différentes sorties d’une fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.1 Sortie anticipée avec return . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6 Notre première classe RAII est-elle vraiment sûre ? . . . . . . . . . . . . . . . . . . . . . . . . 1.6.2 Deux propriétaires pour une seule ressource . . . . . . . . . . . . . . . . . . . . . . . 1.8 Vers la gestion moderne des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2026 – C++ Partie III RAII et gestion moderne des ressources
Accueil
RAII et gestion moderne des ressources Pourquoi revenir sur la gestion des ressources ? Dans la partie consacrée à l’allocation dynamique, nous avons appris à demander explicitement de la mémoire pendant l’exécution d’un programme : double* p = new double[1000]; Cette mémoire doit ensuite être libérée explicitement : Cette manière de procéder nous a permis de comprendre ce qui se passe réellement lors d’une allocation dynamique : une zone de mémoire est réservée, son adresse est conservée dans un pointeur, puis cette zone doit être rendue au système lorsqu’elle n’est plus nécessaire. Mais, le problème est que cette gestion repose entièrement sur le programmeur. Il faut penser à appeler delete ou delete[] au bon moment et, surtout, il faut s’assurer que cette libération aura lieu quel que soit le chemin suivi par le programme. Considérons par exemple : double* p = new double[1000]; // Quelques calculs… // Autres calculs… Si la condition erreur est vraie, la fonction se termine immédiatement et l’instruction n’est jamais exécutée. 2026 – C++ Partie III RAII et gestion moderne des ressources La mémoire réservée avec new[] n’est donc pas libérée ! Nous avons créé une fuite de mémoire. Plus un programme devient complexe, plus il devient difficile de garantir manuellement que chaque ressource acquise sera correctement libérée c’est pour ceci que le C++ propose une manière beaucoup plus robuste La durée de vie d’une ressource peut être liée à la durée de vie d’un objet. C’est le principe fondamental appelé « RAII 1 . Qu’est-ce qu’une ressource ? Avant de définir précisément RAII, il faut préciser ce que l’on entend par le mot ressource. Dans les exemples précédents, la ressource était une zone de mémoire obtenue par une allocation dynamique, mais en programmation, une ressource peut prendre de nombreuses formes. Il peut s’agir par exemple, • d’une zone de mémoire allouée dynamiquement ; • d’un fichier ouvert ; • d’une connexion réseau ; • d’un verrou (mutex ) ; • d’une connexion à une base de données ; • d’un objet ou d’un périphérique fourni par le système d’exploitation ; • plus généralement, de quelque chose qui doit être acquis, utilisé puis libéré. Une ressource possède donc généralement un cycle de vie que l’on peut schématiser ainsi : acquisition −→ utilisation −→ libération Pour une allocation dynamique : new −→ utilisation de la mémoire −→ delete ouverture −→ lecture ou écriture −→ fermeture Dans les deux cas, le problème est fondamentalement le même : une ressource acquise doit être correctement libérée. RAII est l’acronyme de : Resource Acquisition Is Initialization que l’on peut traduire littéralement par : L’acquisition d’une ressource est l’initialisation. Cette traduction ne permet cependant pas de comprendre immédiatement l’idée essentielle de RAII, on en est même assez loin ! Une formulation beaucoup plus parlante est la suivante : 1. Resource Acquisition Is Initialization » » ou « l’acquisition d’une ressource est l’initialisation ».
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources La durée de vie d’une ressource est liée à la durée de vie d’un objet qui en est responsable. L’objet acquiert la ressource lors de son initialisation, généralement dans son constructeur et conserve ensuite cette ressource pendant sa durée de vie, jusqu’à destruction de l’objet, son destructeur libère alors la Le mécanisme peut donc être représenté ainsi : construction de l’objet acquisition de la ressource utilisation de la ressource destruction de l’objet libération de la ressource L’intérêt fondamental vient du fait que la destruction des objets automatiques est elle-même gérée par le langage et dès qu’un objet local arrive à la fin de sa portée, son destructeur est automatiquement appelé 2 . Il devient donc possible de faire dépendre la libération d’une ressource d’un mécanisme que le langage garantit déjà : la destruction de l’objet qui la possède. Construisons volontairement une classe très simple qui possède un tableau dynamique : Tableau(std::size_t n) donnees = new double[n]; Le constructeur acquiert la ressource : donnees = new double[n]; et le destructeur la libère : 2. RAII ne doit pas être confondu avec un garbage collector (ramasse-miettes). Avec RAII, la libération d’une ressource est déterministe : elle intervient lorsque l’objet qui la possède est détruit, généralement à la fin de sa portée. Un garbage collector, utilisé notamment par Java, recherche périodiquement les objets devenus inaccessibles et récupère leur mémoire ; le moment exact de cette récupération n’est généralement pas déterminé par la portée de l’objet. RAII peut en outre gérer d’autres ressources que la mémoire, comme des fichiers, des sockets ou des verrous.
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources Nous pouvons maintenant écrire : L’objet t est créé lorsque l’exécution entre dans la fonction et son constructeur réserve la mémoire nécessaire au tableau. Lorsque l’exécution quitte la fonction, l’objet t arrive à la fin de sa durée de vie, son destructeur est alors automatiquement appelé et celui-ci exécute : La mémoire est donc libérée sans que la fonction calcul() ait elle-même besoin d’appeler delete[]. RAII ne signifie pas simplement « Mettre delete dans un destructeur ». RAII est un principe plus général. Un objet devient responsable d’une ressource et la durée de vie de cette ressource est liée à celle de l’objet. On peut résumer cette relation ainsi : objet propriétaire −→ ressource durée de vie de l’objet −→ durée de vie de la ressource Lorsque l’objet disparaît, la ressource qu’il possède est automatiquement libérée. RAII ne signifie pas “tout mettre sur la pile” Cette distinction est importante. l’objet t possède une durée de vie automatique, mais la mémoire qu’il contrôle est toujours obtenue donnees = new double[n]; Cette mémoire se trouve donc toujours dans la zone utilisée pour les allocations dynamiques et RAII ne supprime pas nécessairement l’allocation dynamique, mais supprime la nécessité pour le code qui utilise l’objet de gérer la libération de cette allocation.
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources Le principe RAII est antérieur au C++ moderne et existait déjà bien avant C++11. Le C++ moderne a cependant introduit ou généralisé des outils qui permettent d’appliquer ce principe beaucoup plus facilement et de manière beaucoup plus sûre. Parmi eux, nous rencontrerons notamment : • std::unique_ptr ; • std::shared_ptr ; • les conteneurs de la bibliothèque standard tels que std::vector. Ces outils permettent dans de très nombreux cas de ne plus écrire directement : Il est cependant important d’avoir étudié ces mécanismes auparavant car les outils modernes ne font pas disparaître la gestion des ressources mais permettent de la confier à des objets conçus pour l’effectuer • RAII (Resource Acquisition Is Initialization) est un principe de conception fondamental du • Une ressource est confiée à un objet qui en devient responsable. • L’acquisition de la ressource est liée à l’initialisation de l’objet et sa libération à la destruction • Ainsi, lorsque l’objet arrive à la fin de sa durée de vie, son destructeur assure automatiquement la libération de la ressource. • RAII ne concerne pas uniquement la mémoire dynamique, le même principe peut être utilisé pour les fichiers, les connexions réseau, les verrous et de nombreuses autres ressources. RAII et les différentes sorties d’une fonction L’intérêt de RAII devient particulièrement évident lorsqu’une fonction peut se terminer de plusieurs manières. Reprenons notre classe Tableau, qui acquiert une zone de mémoire dans son constructeur et la libère dans son destructeur et considérons d’abord le cas le plus simple :
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources Lorsque l’exécution atteint l’accolade fermante de la fonction, l’objet local t arrive à la fin de sa durée de vie et son destructeur est automatiquement appelé, ce qui libère la mémoire. Mais une fonction ne se termine pas nécessairement en atteignant sa dernière accolade. Sortie anticipée avec return Considérons maintenant le programme // Premiers calculs… // Autres calculs… Si erreur est vraie, l’instruction return provoque une sortie immédiate de la fonction. Cependant, avant de quitter la fonction, le C++ détruit les objets locaux dont la durée de vie doit se terminer et par conséquent, le destructeur de t est appelé et celui-ci lance : La mémoire est ainsi correctement libérée, même si l’exécution n’atteint jamais la dernière accolade de la fonction. Comparons cela avec une gestion entièrement manuelle : double∗ p = new double[1000]; // Premiers calculs… // Autres calculs… Dans ce deuxième programme, le return empêche l’exécution de La mémoire n’est donc pas libérée. Avec RAII, la libération de la ressource ne dépend plus de l’exécution explicite d’une instruction placée à un endroit précis du programme mais de la durée de vie de l’objet qui possède cette ressource.
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources • Avec RAII, une sortie anticipée d’une fonction par return ne provoque pas la perte des ressources appartenant aux objets locaux. • Lorsque ces objets arrivent à la fin de leur durée de vie, leurs destructeurs sont automatiquement appelés et libèrent les ressources qu’ils possèdent. RAII et les exceptions Une fonction peut également être interrompue par une exception, par exemple : // Calcul susceptible de provoquer une exception… Si une exception est déclenchée pendant l’exécution de la fonction, le programme doit quitter la portée dans laquelle l’objet t a été créé et avant de quitter cette portée, le C++ détruit les objets automatiques locaux déjà construits. Le destructeur de t est donc appelé et la mémoire qu’il possède est libérée. Ce mécanisme fait partie de ce que l’on appelle le stack unwinding, que l’on peut traduire par déroulement de la pile donc, lorsqu’une exception remonte à travers les appels de fonctions, les objets automatiques rencontrés sont détruits conformément aux règles du langage. RAII permet ainsi de garantir que les ressources qu’ils possèdent sont libérées pendant ce processus. Nous voyons qu’avec une gestion manuelle, la situation est beaucoup plus délicate, soit : double∗ p = new double[1000]; operationQuiPeutEchouer(); car si operationQuiPeutEchouer() déclenche une exception, l’instruction n’est jamais exécutée. Nous obtenons à nouveau une fuite de mémoire. Avec un objet RAII : operationQuiPeutEchouer();
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources la destruction de t entraîne automatiquement la libération de sa ressource lors de la sortie de la portée. • RAII permet de rendre la libération des ressources indépendante du chemin suivi par le programme. • Sortie normale d’une portée, return anticipé ou propagation d’une exception : lorsque l’objet propriétaire est détruit, sa ressource est libérée. Notre première classe RAII est-elle vraiment sûre ? Notre classe Tableau semble maintenant résoudre le problème de la libération de la mémoire. Tableau(std::size_t n) donnees = new double[n]; Le constructeur acquiert la ressource et le destructeur la libère cependant, un problème important subsiste, que se passe-t-il si nous copions un objet Tableau ? Nous n’avons défini aucun constructeur de copie particulier donc le compilateur utilise le mécanisme de copie disponible par défaut et copie les membres de l’objet. Le membre important de notre classe est : Or copier un pointeur ne copie pas la zone de mémoire vers laquelle il pointe, c’est uniquement l’adresse contenue dans le pointeur qui est copiée. Après nous pouvons donc nous retrouver conceptuellement dans la situation suivante :
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources Les deux objets contiennent alors une adresse désignant la même zone de mémoire. Deux propriétaires pour une seule ressource Lorsque l’un des objets est détruit, son destructeur exécute l’instruction et la zone de mémoire est libérée. Mais l’autre objet contient toujours la même adresse et lorsqu’il est à son tour détruit, son destructeur tente d’exécuter une deuxième fois l’instruction sur une zone de mémoire qui a déjà été libérée. Il s’agit d’une double libération de la même ressource et le comportement du programme est alors indéfini. • Un pointeur ne représente pas, à lui seul, une notion de propriété. • Copier un pointeur copie une adresse, mais ne copie pas nécessairement la ressource vers laquelle cette adresse pointe. • Une classe qui possède directement une ressource doit donc définir très précisément ce que signifient la copie, l’affectation et la destruction de ses objets. La notion de propriété Le problème précédent met en évidence une notion fondamentale de la gestion moderne des ressources : la Lorsqu’un objet est responsable de la libération d’une ressource, il est naturel de dire qu’il en est le propriétaire. Dans notre première classe Tableau, nous voulions établir la relation : Tableau −→ propriétaire du tableau dynamique Mais la copie de l’objet a créé une situation ambiguë : deux objets −→ une seule ressource Qui doit alors libérer cette ressource ? Si les deux objets le font, nous obtenons une double libération ! Si aucun ne le fait, nous obtenons une fuite Il faut donc que la relation de propriété soit définie sans ambiguïté.
Accueil
2026 – C++ Partie III RAII et gestion moderne des ressources Vers la gestion moderne des ressources Il est parfaitement possible d’écrire nous-mêmes une classe qui gère correctement ces situations, il faudrait notamment définir précisément le comportement de l’objet lorsqu’il est copié, affecté ou déplacé. Mais cette gestion devient rapidement délicate et répétitive. Le C++ moderne fournit des classes spécialement conçues pour exprimer et gérer la propriété d’une ressource dont la plus fondamentale est : Son nom donne déjà une indication importante : unique −→ propriétaire unique Un std::unique_ptr représente une ressource possédant un seul propriétaire. Il applique le principe RAII : lorsque le unique_ptr est détruit, la ressource qu’il possède est automatiquement libérée, mais contrairement à notre classe Tableau rudimentaire, std::unique_ptr est conçu pour empêcher précisément le type de copie dangereuse que nous venons d’observer. Ce sera l’objet du chapitre suivant. Le principe RAII consiste à associer la durée de vie d’une ressource à celle d’un objet, qui en devient responsable. L’acquisition de la ressource intervient lors de l’initialisation de l’objet et sa libération lors de sa destruction. Cette approche permet notamment de garantir la libération des ressources, • lors d’une sortie normale d’une portée, • lors d’un return anticipé, • ou lors de la propagation d’une exception. RAII ne concerne pas uniquement la mémoire dynamique. Le même principe s’applique aux fichiers, aux connexions réseau, aux verrous et, plus généralement, à toute ressource qui doit être acquise puis libérée. Cependant, posséder directement une ressource impose également de définir ce qui doit se produire lorsqu’un objet est copié ou déplacé. La gestion moderne des ressources en C++ repose donc non seulement sur RAII, mais également sur une notion explicite de propriété. Dans le chapitre suivant, nous étudierons std::unique_ptr, qui permet de représenter une ressource possédant un propriétaire unique.
Accueil
P4-Chap-01 RAII et gestion moderne des ressources
Accueil
Termes à ajouter au glossaire
Contenu

Inscription

×
Cancel