web-dev-qa-db-fra.com

jeter des exceptions sur un destructeur

La plupart des gens disent que jamais ne lève une exception sur un destructeur, ce qui entraîne un comportement indéfini. Stroustrup précise que "le destructeur de vecteur appelle explicitement le destructeur pour chaque élément. Cela implique que si un destructeur d'élément est lancé, la destruction du vecteur échoue ... Il n'y a vraiment pas de bonne façon de protéger contre les exceptions émises par les destructeurs, la bibliothèque ne fait donc aucune garantie si un destructeur d’élément lève "(de l’appendice E3.2) .

Cet article semble dire le contraire - que les destructeurs de jet sont plus ou moins corrects.

Ma question est donc la suivante: si le fait de lancer un destructeur entraîne un comportement indéfini, comment gérez-vous les erreurs qui se produisent lors d'un destructeur?

Si une erreur se produit pendant une opération de nettoyage, est-ce que vous l'ignorez? S'il s'agit d'une erreur qui peut potentiellement être traitée dans la pile, mais pas directement dans le destructeur, est-ce que cela n'a pas de sens de jeter une exception dans le destructeur?

Évidemment, ce genre d’erreurs est rare, mais possible.

240
Greg Rogers

Lancer une exception sur un destructeur est dangereux.
Si une autre exception se propage déjà, l'application se terminera.

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

Cela revient essentiellement à:

Tout ce qui est dangereux (c’est-à-dire qui pourrait déclencher une exception) devrait être fait par le biais de méthodes publiques (pas nécessairement directement). L'utilisateur de votre classe peut alors potentiellement gérer ces situations en utilisant les méthodes publiques et en détectant les exceptions potentielles.

Le destructeur termine ensuite l'objet en appelant ces méthodes (si l'utilisateur ne le fait pas explicitement), mais toutes les exceptions renvoyées sont interceptées et supprimées (après avoir tenté de résoudre le problème).

Donc, en réalité, vous passez la responsabilité à l'utilisateur. Si l'utilisateur est en mesure de corriger les exceptions, il appelle manuellement les fonctions appropriées et traite les erreurs éventuelles. Si l'utilisateur de l'objet n'est pas inquiet (car l'objet sera détruit), le destructeur doit alors s'occuper de tout.

Un exemple:

std :: fstream

La méthode close () peut potentiellement lever une exception. Le destructeur appelle close () si le fichier a été ouvert, mais veille à ce que les exceptions ne se propagent pas hors du destructeur.

Ainsi, si l'utilisateur d'un objet de fichier souhaite effectuer un traitement spécial des problèmes liés à la fermeture du fichier, il appellera manuellement close () et gérera les exceptions. Si, par contre, ils ne s'en soucient pas, le destructeur devra alors gérer la situation.

Scott Myers a un excellent article sur le sujet dans son livre "Effective C++"

Modifier:

Apparemment aussi dans "C++ plus efficace"
Point 11: Empêcher les exceptions de laisser des destructeurs

183
Martin York

Le fait de jeter un destructeur peut entraîner un crash, car ce destructeur peut être appelé dans le cadre du "Stack de déroulement". Le déroulement de pile est une procédure qui se produit lorsqu'une exception est levée. Dans cette procédure, tous les objets qui ont été placés dans la pile depuis le "try" et jusqu'à ce que l'exception soit levée seront terminés -> leurs destructeurs seront appelés. Et au cours de cette procédure, une autre exception n'est pas autorisée, car il est impossible de gérer deux exceptions à la fois. Cela provoquera un appel à abort (), le programme se bloquera et le contrôle reviendra à l'OS.

52
Gal Goldman

Nous devons différencier ici au lieu de suivre aveuglément général conseil pour spécifique cas .

Notez que ce qui suit ignore le problème des conteneurs d’objets et la marche à suivre face à de multiples objets à l’intérieur des conteneurs. (Et il peut être ignoré partiellement, car certains objets ne sont tout simplement pas bien adaptés pour être placés dans un conteneur.)

Il est plus facile de penser au problème dans son ensemble lorsque nous divisons les classes en deux types. Un dtor de classe peut avoir deux responsabilités différentes:

  • (R) release sémantique (aka free that memory)
  • (C) commit sémantique (aka flush fichier sur disque)

Si nous voyons la question de cette façon, alors je pense que l'on peut argumenter que la sémantique (R) ne devrait jamais causer d'exception à un dénomination, car il n'y a rien que nous puissions faire à ce sujet et b) de nombreuses opérations en ressources libres ne le sont pas. même prévoir une vérification d'erreur, par exemple voidfree(void* p);.

Les objets avec la sémantique (C), comme un objet fichier qui doit vider correctement ses données ou une connexion à une base de données ("scope gardée") qui effectue une validation dans le dtor sont d'un type différent: Nous peut faire quelque chose à propos de l'erreur (au niveau de l'application) et nous ne devrions vraiment pas continuer comme si de rien n'était.

Si nous suivons la route RAII et prenons en compte les objets qui ont une sémantique (C), je pense que nous devons également tenir compte du cas étrange où de tels artistes peuvent lancer. Il s'ensuit que vous ne devez pas placer de tels objets dans des conteneurs et que le programme peut toujours terminate() si un commit-dtor est lancé alors qu'une autre exception est active.


En ce qui concerne le traitement des erreurs (sémantique Commit/Rollback) et les exceptions, on parle bien un par un Andrei Alexandresc : Traitement des erreurs dans le flux de contrôle déclaratif/C++ (tenue à NDC 2014 )

Dans les détails, il explique comment la bibliothèque Folly implémente un UncaughtExceptionCounter pour leurs outils ScopeGuard .

(Je devrais noter que autres avaient également des idées similaires.)

Bien que la discussion ne se concentre pas sur le lancer d'un joueur, il montre un outil qui peut être utilisé aujourd'hui pour se débarrasser du problèmes de moment pour lancer d'un d'tor.

Dans le futur, Là mai être une fonctionnalité std pour cela, voir N3614 , et un discussion à ce sujet .

Upd '17: la fonctionnalité standard C++ 17 pour cela est std::uncaught_exceptions afaikt. Je citerai rapidement l'article de cppref:

Remarques

Un exemple où int- retournant uncaught_exceptions _ is is is ... ... crée tout d'abord un objet guard et enregistre le nombre d'exceptions non capturées dans son constructeur. La sortie est effectuée par le destructeur de l'objet guard sauf si foo () lève ( dans ce cas, le nombre d'exceptions non capturées dans le destructeur est supérieur à celui observé par le constructeur)

46
Martin Ba

La vraie question à se poser sur le fait de jeter à partir d'un destructeur est "Qu'est-ce que l'appelant peut faire avec ça?" Y a-t-il réellement quelque chose d'utile que vous puissiez faire à l'exception, qui compenserait les dangers créés par le lancement d'un destructeur?

Si je détruis un objet Foo et que le destructeur Foo jette une exception, que puis-je raisonnablement en faire? Je peux le connecter ou l'ignorer. C'est tout. Je ne peux pas le "réparer", parce que l'objet Foo est déjà parti. Dans le meilleur des cas, je connecte l'exception et continue comme si de rien n'était (ou que je termine le programme). Est-ce que cela vaut vraiment la peine de provoquer un comportement indéfini en lançant un destructeur?

19
Derek Park

Extrait du projet ISO pour C++ (ISO/IEC JTC 1/SC 22 N 4411)

Ainsi, les destructeurs devraient généralement intercepter les exceptions et ne pas les laisser se propager hors du destructeur.

3 Le processus d'appel des destructeurs pour les objets automatiques construits sur le chemin d'un bloc try à une expression-throw est appelé "pile en cours de déroulement". [Remarque: si un destructeur appelé lors du déroulement de la pile avec une exception, std :: terminate est appelé (15.5.1). Ainsi, les destructeurs devraient généralement intercepter les exceptions et ne pas les laisser se propager hors du destructeur. - note de fin]

12
lothar

C'est dangereux, mais cela n'a également aucun sens du point de vue de la lisibilité/de la compréhensibilité du code.

Ce que vous devez demander est dans cette situation

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

Que devrait attraper l'exception? Est-ce que l'appelant de foo? Ou est-ce que foo devrait le gérer? Pourquoi l'appelant de foo devrait-il s'intéresser à un objet interne à foo? Il y a peut-être un moyen par lequel le langage définit cela comme ayant un sens, mais cela va être illisible et difficile à comprendre.

Plus important encore, où va la mémoire pour Object? Où va la mémoire de l'objet possédé? Est-il toujours alloué (apparemment parce que le destructeur a échoué)? Pensez également que l’objet était dans l’espace de pile , il est donc évident qu’il a disparu.

Alors considérons ce cas

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

Lorsque la suppression d'obj3 échoue, comment puis-je réellement supprimer d'une manière qui ne risque pas d'échouer? C'est ma mémoire, bon sang!

Considérons maintenant dans le premier extrait de code que l'objet disparaisse automatiquement car il est sur la pile pendant que Object3 est sur le tas. Puisque le pointeur sur Object3 est parti, vous êtes un peu SOL. Vous avez une fuite de mémoire.

Maintenant, un moyen sûr de faire les choses est le suivant

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

Voir aussi ceci FAQ

12
Doug T.

Votre destructeur est peut-être en train de s’exécuter dans une chaîne d’autres destructeurs. Le lancement d'une exception qui n'est pas interceptée par votre appelant immédiat peut laisser plusieurs objets dans un état incohérent, ce qui causera encore plus de problèmes que l'ignorance de l'erreur lors de l'opération de nettoyage.

7
Franci Penov

Pour compléter les réponses principales, qui sont bonnes, complètes et exactes, j'aimerais commenter l'article que vous avez mentionné - celui qui dit: "jeter des exceptions dans des destructeurs n'est pas si mauvais".

L'article adopte la ligne "quelles sont les alternatives aux exceptions," et énumère quelques problèmes avec chacune d'elles. Cela étant fait, il conclut que, faute de trouver une alternative sans problème, nous devrions continuer à lancer des exceptions.

Le problème est qu’aucun des problèmes qu’il énumère avec les alternatives n’est aussi grave que le comportement d’exception, qui, rappelons-le, est un "comportement indéfini de votre programme". Parmi les objections de l'auteur figurent "esthétiquement laide" et "encourage le mauvais style". Maintenant que préféreriez-vous avoir? Un programme avec un mauvais style ou avec un comportement indéfini?

5
DJClayworth

Tout le monde a expliqué pourquoi jeter des destructeurs est terrible ... que pouvez-vous faire à ce sujet? Si vous effectuez une opération qui peut échouer, créez une méthode publique distincte qui effectue un nettoyage et peut générer des exceptions arbitraires. Dans la plupart des cas, les utilisateurs l'ignoreront. Si les utilisateurs veulent surveiller le succès/l'échec du nettoyage, ils peuvent simplement appeler la routine de nettoyage explicite.

Par exemple:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};
5
Tom

Je fais partie du groupe qui considère que la configuration de la "protection périmée" introduite dans le destructeur est utile dans de nombreuses situations - en particulier pour les tests unitaires. Cependant, sachez qu'en C++ 11, l'inclusion d'un destructeur entraîne un appel à std::terminate Car les destructeurs sont annotés implicitement avec noexcept.

Andrzej Krzemieński a écrit un excellent article sur les destructeurs qui jettent:

Il souligne que C++ 11 dispose d'un mécanisme pour remplacer le paramètre par défaut noexcept des destructeurs:

En C++ 11, un destructeur est implicitement spécifié sous la forme noexcept. Même si vous n’ajoutez aucune spécification et définissez votre destructeur comme suit:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

Le compilateur ajoutera toujours de manière invisible la spécification noexcept à votre destructeur. Et cela signifie que le moment où votre destructeur lève une exception, std::terminate Sera appelé, même s'il n'y a pas eu de situation de double exception. Si vous êtes vraiment déterminé à laisser vos destructeurs lancer, vous devrez le spécifier explicitement; vous avez trois options:

  • Spécifiez explicitement votre destructeur en tant que noexcept(false),
  • Héritez votre classe d'un autre qui spécifie déjà son destructeur sous la forme noexcept(false).
  • Placez un membre de données non statique dans votre classe qui spécifie déjà son destructeur sous la forme noexcept(false).

Enfin, si vous décidez de lancer le destructeur, vous devez toujours être conscient du risque d’exception double (lancer pendant que la pile est en train de se dérouler à cause d’une exception). Cela provoquerait un appel à std::terminate Et c'est rarement ce que vous voulez. Pour éviter ce problème, vous pouvez simplement vérifier s'il existe déjà une exception avant d'en lancer une nouvelle en utilisant std::uncaught_exception().

4
GaspardP

Q: Ma question est donc la suivante: si le fait d’émettre à partir d’un destructeur donne lieu à un comportement indéfini, comment gérez-vous les erreurs qui se produisent pendant un destructeur?

A: Il y a plusieurs options:

  1. Laissez les exceptions sortir de votre destructeur, peu importe ce qui se passe ailleurs. Et ce faisant, soyez conscient (voire craintif) que std :: terminate peut suivre.

  2. Ne laissez jamais une exception sortir de votre destructeur. Peut être écrit dans un journal, un gros gros texte rouge si vous le pouvez.

  3. mon préféré: Si std::uncaught_exception renvoie false, vous permet de dériver les exceptions. Si la valeur est vraie, retournez à l'approche de journalisation.

Mais est-ce bien de jeter dans le dos?

Je suis d’accord avec la plupart des réponses ci-dessus, il est préférable d’éviter le lancer dans destructor, là où il peut être. Mais parfois, il vaut mieux accepter que cela se produise et le gérer correctement. Je choisirais 3 ci-dessus.

Il y a quelques cas étranges où c'est en fait un bonne idée à jeter d'un destructeur. Comme le code d'erreur "must check". C'est un type de valeur qui est renvoyé par une fonction. Si l'appelant lit/vérifie le code d'erreur contenu, la valeur renvoyée est détruite de manière silencieuse. Mais, si le code d'erreur renvoyé n'a pas été lu au moment où les valeurs renvoyées sortent du champ d'application, une exception, de son destructeur, sera émise.

2
MartinP

Actuellement, je suis la politique (que beaucoup disent) que les classes ne devraient pas lancer de manière active des exceptions de leurs destructeurs, mais devraient plutôt fournir une méthode "close" publique pour effectuer l'opération qui pourrait échouer ...

... mais je pense que les destructeurs de classes de type conteneur, comme un vecteur, ne doivent pas masquer les exceptions renvoyées par les classes qu’ils contiennent. Dans ce cas, j'utilise en fait une méthode "free/close" qui s'appelle de manière récursive. Oui, j'ai dit récursivement. Il y a une méthode à cette folie. La propagation des exceptions repose sur l'existence d'une pile: si une seule exception se produit, les deux destructeurs restants continueront de s'exécuter et l'exception en attente se propagera une fois que la routine aura été renvoyée, ce qui est génial. Si plusieurs exceptions se produisent, alors (selon le compilateur), la première exception se propagera ou le programme se terminera, ce qui est correct. Si autant d'exceptions surviennent que la récursivité déborde de la pile, il y a quelque chose qui ne va vraiment pas, et quelqu'un va en apprendre davantage à ce sujet, ce qui est bien aussi. Personnellement, je préfère les erreurs qui explosent au lieu d’être cachées, secrètes et insidieuses.

Le fait est que le conteneur reste neutre et que c'est aux classes contenues de décider si elles se comportent ou non en ce qui concerne le lancement d'exceptions de leurs destructeurs.

1
Matthew

Martin Ba (ci-dessus) est sur la bonne voie - vous architectez différemment pour la logique RELEASE et COMMIT.

Pour la sortie:

Vous devriez manger des erreurs. Vous libérez de la mémoire, fermez des connexions, etc. Personne d'autre dans le système ne devrait jamais voir ces choses encore, et vous restituez des ressources au système d'exploitation. S'il semble que vous ayez besoin d'une véritable gestion des erreurs ici, il s'agit probablement d'une conséquence de défauts de conception dans votre modèle d'objet.

Pour commettre:

C'est là que vous voulez le même type d'objets wrapper RAII que ceux fournis par std :: lock_guard pour les mutex. Avec ceux-ci, vous ne mettez pas la logique de validation dans dtor AT ALL. Vous avez une API dédiée, puis les objets wrapper qui vont RAII la commettront dans LEUR développeurs et y traiteront les erreurs. N'oubliez pas que vous pouvez capturer des exceptions dans un destructeur, ce qui vous permet de mettre en œuvre une politique et une gestion des erreurs différente en construisant un wrapper différent (par exemple, std :: unique_lock contre std :: lock_guard). vous n'oublierez pas d'appeler la logique de commit, qui est la seule justification à mi-chemin décente pour la placer dans un compteur à la 1ère place.

1
user3726672

Ma question est donc la suivante: si le fait de lancer à partir d’un destructeur entraîne un comportement indéfini, comment gérez-vous les erreurs qui se produisent lors d’un destructeur?

Le problème principal est le suivant: vous ne pouvez pas échouer . Qu'est-ce que cela signifie d'échouer, après tout? Si la validation d'une transaction dans une base de données échoue et échoue (échec de l'annulation), qu'advient-il de l'intégrité de nos données?

Étant donné que les destructeurs sont appelés à la fois pour les chemins normal et exceptionnel (échec), ils ne peuvent pas eux-mêmes échouer, sinon nous échouons.

C'est un problème conceptuellement difficile, mais la solution consiste souvent à trouver un moyen de s'assurer que l'échec ne peut pas échouer. Par exemple, une base de données peut écrire des modifications avant de s’engager dans une structure de données externe ou un fichier. Si la transaction échoue, la structure de fichier/données peut être jetée. Il ne lui reste plus qu'à s'assurer que les modifications apportées par cette structure/fichier externe constituent une transaction atomique qui ne peut échouer.

La solution pragmatique consiste peut-être simplement à s'assurer que les chances d'échec en cas d'échec sont astronomiquement invraisemblables, car il peut être presque impossible dans certains cas de ne pas échouer.

La solution la plus appropriée pour moi consiste à écrire votre logique de non-nettoyage de telle sorte que la logique de nettoyage ne puisse pas échouer. Par exemple, si vous êtes tenté de créer une nouvelle structure de données afin de nettoyer une structure de données existante, vous pouvez éventuellement essayer de créer cette structure auxiliaire à l'avance afin que nous n'ayons plus à la créer dans un destructeur.

Tout cela est beaucoup plus facile à dire qu'à faire, certes, mais c'est la seule façon vraiment appropriée de procéder. Parfois, je pense qu'il devrait exister une capacité à écrire une logique de destructeur distincte pour les chemins d'exécution normaux, à l'exception des destructeurs exceptionnels, car certains destructeurs ont parfois le sentiment d'avoir deux fois plus de responsabilités en essayant de gérer les deux (par exemple, les gardes de portée qui nécessitent un renvoi explicite ; ils n’auraient pas besoin de cela s’ils pouvaient différencier les chemins de destruction exceptionnels des chemins non exceptionnels).

Le problème ultime reste que nous ne pouvons pas échouer, et c’est un problème de conception difficile à résoudre parfaitement dans tous les cas. Cela devient plus facile si vous n'êtes pas trop enveloppé dans des structures de contrôle complexes avec des tonnes d'objets minuscules en interaction les uns avec les autres, et modélisez plutôt vos conceptions de manière plus volumineuse (exemple: système de particules avec un destructeur pour détruire toute la particule système, pas un destructeur non trivial séparé par particule). Lorsque vous modélisez vos conceptions à ce type de niveau plus grossier, vous avez moins de destructeurs non triviaux à gérer, et pouvez également vous permettre souvent de surcharger la mémoire/le traitement nécessaire pour vous assurer que vos destructeurs ne peuvent pas échouer.

Et l’une des solutions les plus simples est naturellement d’utiliser moins souvent les destructeurs. Dans l'exemple de particule ci-dessus, par exemple, lors de la destruction/suppression d'une particule, il convient de faire certaines choses qui pourraient échouer pour une raison quelconque. Dans ce cas, au lieu d'invoquer une telle logique via le dtor de la particule qui pourrait être exécuté dans un chemin exceptionnel, vous pourriez tout faire exécuter par le système de particules lorsqu'il supprime une particule. L'élimination d'une particule peut toujours être effectuée pendant un chemin non exceptionnel. Si le système est détruit, il peut peut-être simplement purger toutes les particules et ne pas s'embarrasser de cette logique de suppression de particule individuelle qui peut échouer, alors que la logique pouvant échouer n'est exécutée que pendant l'exécution normale du système de particules lorsqu'il supprime une ou plusieurs particules.

Il y a souvent des solutions comme celle-ci qui surgissent si vous évitez de traiter beaucoup d'objets minuscules avec des destructeurs non triviaux. Là où vous pouvez vous perdre dans un désordre où il semble presque impossible d'être une exception-sécurité, c'est quand vous êtes empêtré dans de nombreux objets minuscules qui ont tous des détracteurs non triviaux.

Cela aiderait beaucoup si nothrow/noexcept était traduit en erreur de compilation si tout ce qui le spécifiait (y compris les fonctions virtuelles qui devraient hériter de la spécification noexcept de sa classe de base) tentait d’invoquer tout ce qui pouvait se produire. De cette façon, nous serions capables de récupérer tout cela au moment de la compilation si nous écrivions un destructeur par inadvertance qui pourrait lancer.

0
Dragon Energy

Contrairement aux constructeurs, où le lancement d'exceptions peut être un moyen utile d'indiquer que la création d'objet a réussi, les exceptions ne doivent pas être lancées dans des destructeurs.

Le problème se produit lorsqu'une exception est émise par un destructeur lors du processus de déroulement de la pile. Si cela se produit, le compilateur se trouve dans une situation où il ne sait pas s'il faut continuer le processus de déroulement de la pile ou gérer la nouvelle exception. Le résultat final est que votre programme sera immédiatement terminé.

Par conséquent, la meilleure solution consiste simplement à s'abstenir d'utiliser des exceptions dans les destructeurs. Écrivez plutôt un message dans un fichier journal.

0
Devesh Agrawal

Définir un événement d'alarme. En règle générale, les événements d'alarme constituent une meilleure forme de notification d'échec lors du nettoyage des objets.

0
MRN