ProgrammationDéveloppeur C++ Backend

Décrivez les différences entre le déroulement de pile (stack unwinding) et la destruction des objets lors du traitement des exceptions en C++. Comment garantir une libération correcte des ressources lors de l'apparition d'une exception ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

En C++, le traitement des exceptions s'accompagne d'un mécanisme de déroulement de pile — destruction automatique (destructuration) des objets locaux dans la pile d'appels, créés avant l'apparition de l'exception. Ce phénomène est important pour la libération correcte des ressources.

Historique de la question

À l'origine, lors de la conception de C++, il manque un collecteur de déchets intégré, et la tâche de libération des ressources (mémoire, fichiers, sockets) incombe au programmeur. Le traitement des exceptions devait garantir une libération correcte lors du retour du code à cause d'une erreur.

Problème

Sans mécanisme de destruction automatique des objets lors d'une exception, il y a des fuites de ressources. Si les ressources ne sont pas libérées manuellement dans chaque bloc catch, le code devient complexe et peu fiable.

Solution

C++ implémente le déroulement de pile, lorsque lors du lancement d'une exception, tous les objets situés dans la pile dans la portée avant le throw sont détruits dans l'ordre inverse de leur création. Leur destructeur est appelé automatiquement.

La méthode classique pour libérer constamment des ressources est d'appliquer le motif RAII (Resource Acquisition Is Initialization) : toutes les ressources sont "enveloppées" dans des objets qui libèrent la ressource qu'ils possèdent lors de leur destruction.

Exemple de code:

#include <iostream> #include <stdexcept> struct FileHandle { FILE* file; FileHandle(const char* path) { file = fopen(path, "r"); if (!file) throw std::runtime_error("Impossible d'ouvrir le fichier"); } ~FileHandle() { if (file) fclose(file); } }; void processFile(const char* path) { FileHandle fh(path); // Revient à l'état précédent s'il y a throw // ... travail avec le fichier throw std::runtime_error("Une erreur s'est produite"); // fclose sera appelé automatiquement }

Caractéristiques clés:

  • Le déroulement de pile appelle automatiquement les destructeurs de tous les objets dans la pile.
  • RAII garantit la libération des ressources indépendamment de l'apparition d'exceptions.
  • La libération manuelle des ressources dans catch est rarement nécessaire : les destructeurs font cela mieux.

Questions pièges.

Pourquoi ne pas simplement utiliser try-catch et appeler manuellement delete ou fclose dans le bloc catch pour éviter les fuites ?

Réponse:

C'est peu pratique et peu fiable : il est facile d'oublier de fermer une ressource, surtout en cas de plusieurs points de sortie ou de ressources imbriquées. Les destructeurs sont appelés même si une exception "traverse" une fonction, catch n'est donc pas nécessaire.

Les objets sur le tas (heap) seront-ils détruits s'ils ne sont pas "enveloppés" dans des objets sur la pile lors du déroulement de pile ?

Réponse:

Non. Le déroulement de pile appelle les destructeurs uniquement pour les objets situés dans la pile. Pour que les objets sur le tas soient détruits correctement, ils doivent être possédés par un objet sur la pile (par exemple, via des pointeurs intelligents).

Que se passe-t-il si un destructeur lance une exception lors du déroulement de pile ?

Réponse:

Si une deuxième exception est levée pendant le déroulement de la pile lors de la destruction d'un objet, le programme se terminera en appelant std::terminate(). Ne levez jamais d'exceptions à partir de destructeurs !

Erreurs typiques et anti-modèles

  • Ne pas libérer les ressources dans les destructeurs, en comptant uniquement sur la gestion manuelle dans catch.
  • Lever des exceptions à partir de destructeurs.
  • Ne pas utiliser de pointeurs intelligents pour gérer les ressources sur le tas.
  • Oublier les ressources imbriquées (par exemple, un fichier à l'intérieur d'une structure).

Exemple de la vie réelle

Cas négatif

Le développeur ferme manuellement les fichiers et libère la mémoire via des blocs catch sans utiliser RAII.

Avantages :

  • Gère explicitement les ressources.

Inconvénients :

  • En cas de sortie via throw avant catch, les fuites sont inévitables.
  • Il est difficile de maintenir et de modifier le code, il est facile d'oublier de libérer des ressources.

Cas positif

Les fichiers et ressources sont enveloppés dans des classes d'encapsulation RAII. Résultat : la libération se produit dans les destructeurs, indépendamment de throw/catch.

Avantages :

  • Fiabilité de la libération des ressources.
  • Moins de code, facile à entretenir.

Inconvénients :

  • Il faut savoir écrire des enveloppes RAII.
  • L'ajout de nouveaux types de ressources nécessite de nouvelles classes.