Question

La plupart des gens disent que jamais ne lance jamais 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éments lance, la destruction de vecteurs échoue ... Il n’existe vraiment aucun moyen efficace de se protéger contre les exceptions émises par des destructeurs. La bibliothèque ne donne aucune garantie si un destructeur d’élément lance des " (voir annexe E3.2) .

Cet article semble indiquer le contraire - le fait de jeter des destructeurs sont plus ou moins d'accord.

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 lors d’un destructeur?

Si une erreur se produit pendant une opération de nettoyage, ne l'ignorez-vous pas? Si c’est une erreur qui peut potentiellement être traitée dans la pile mais pas dans le destructeur, n’a-t-elle pas un sens de jeter une exception dans le destructeur?

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

Était-ce utile?

La solution

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 fait, 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 est alors chargé de s'occuper de ses affaires.

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 sera alors responsable de la situation.

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

Modifier:

Apparemment aussi dans "C ++ plus efficace"

Point 11: Empêchez les exceptions de quitter les destructeurs

Autres conseils

Le fait de quitter un destructeur peut provoquer 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 la commande "essayer" et jusqu'à ce que l'exception soit levée, sera terminé - > 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 au système d'exploitation.

Nous devons différencier ici au lieu de suivre aveuglément les conseils généraux pour des cas spécifiques .

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

L’ensemble du problème devient plus facile à résoudre lorsque nous divisons les classes en deux types. Un dtor de classe peut avoir deux responsabilités différentes:

  • (s) sémantique de la version (c’est-à-dire libérer cette mémoire)
  • (C) Sémantique commit (autrement dit, vider le fichier sur le disque)

Si nous voyons la question de cette façon, alors je pense que l'on peut affirmer que la (S) sémantique ne devrait jamais causer d'exception à un auteur car il y a a) rien à faire contre et b) de nombreuses ressources libres. les opérations ne permettent même pas de vérifier les erreurs, par exemple void gratuit (void * p); .

Les objets avec la sémantique (C), comme un objet fichier qui doit vider correctement ses données ou une connexion à la base de données ("gardée par la portée") qui effectue une validation dans le répertoire sont d'un autre type: nous pouvons faites 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 objets 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 la gestion des erreurs (sémantique Commit / Rollback) et les exceptions, il y a une bonne conversation de la part de Andrei Alexandrescu . : Traitement des erreurs dans le flux de contrôle C ++ / déclaratif (tenu à NDC 2014 )

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

(Je dois noter que d'autres avait également des idées similaires.)

Bien que la discussion ne se concentre pas sur le fait de lancer d'un joueur, elle montre un outil qui peut être utilisé aujourd'hui pour se débarrasser du des problèmes liés au moment de lancer d'un utilisateur.

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

Upd '17: la fonctionnalité standard C ++ 17 utilisée à cette fin est std :: uncaught_exceptions Je citerai rapidement l'article de Cppref:

  

Notes

     

Un exemple d'utilisation de int -retournant uncaught_exceptions est ... ... d'abord   crée un objet de garde et enregistre le nombre d'exceptions non capturées   dans son constructeur. La sortie est effectuée par l'objet de garde   destructeur à moins que foo () jette (), auquel cas le nombre de   les exceptions dans le destructeur sont supérieures à ce que le constructeur   observé )

La vraie question à se poser sur le fait de jeter depuis un destructeur est "Que peut faire l'appelant avec cela?" Y a-t-il réellement quelque chose d'utile que vous puissiez faire avec 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 lève une exception, que puis-je raisonnablement en faire? Je peux le connecter ou l'ignorer. C'est tout. Je ne peux pas "réparer" car l’objet Foo est déjà parti. Dans le meilleur des cas, je consigne 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?

C’est dangereux, mais cela n’a pas non plus de sens du point de vue lisibilité / compréhensibilité du code.

Ce que vous devez demander, c'est dans cette situation

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

Que devrait capturer l'exception? Est-ce que l'appelant de foo? Ou faut-il que foo s'en occupe? Pourquoi l'appelant de foo devrait-il s'intéresser à un objet interne à foo? Le langage pourrait peut-être donner un sens à cela, mais ce serait 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 se trouvait dans l’espace de pile . Il est donc évident qu’il a disparu malgré tout.

Considérons ensuite 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 supprimer d'une manière qui ne risque pas d'échouer? C'est ma mémoire sacrément!

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 également cette FAQ

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

Les destructeurs doivent donc généralement intercepter les exceptions et ne pas les laisser se propager hors du destructeur.

  

3 Processus d’appel des destructeurs pour les objets automatiques construits sur le chemin d’un bloc d’essai à un     expression est appelée & # 8220; décompression de pile. & # 8221; [Remarque: si un destructeur appelé pendant le déroulement de la pile se termine avec un     exception, std :: terminate est appelé (15.5.1). Donc, les destructeurs devraient généralement attraper des exceptions et ne pas laisser     les propager hors du destructeur. & # 8212; note de fin]

Votre destructeur est peut-être en train de s'exécuter à l'intérieur d'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 d'ignorer l'erreur dans l'opération de nettoyage.

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...
};

En tant que complément aux réponses principales, qui sont bonnes, complètes et précises, j'aimerais commenter l'article que vous avez mentionné - celui qui dit "jeter des exceptions dans des destructeurs n'est pas si mal".

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 solution de rechange sans problème, nous devrions continuer à lancer des exceptions.

Le problème, c’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 non défini de votre programme". Certaines des objections de l'auteur incluent "esthétiquement laide". et "encourager le mauvais style". Maintenant que préféreriez-vous avoir? Un programme avec un mauvais style ou avec un comportement indéfini?

Je suis dans le groupe qui considère que la "garde périmée" Le motif jeté dans le destructeur est utile dans de nombreuses situations - en particulier pour les tests unitaires. Cependant, sachez qu'en C ++ 11, l'insertion d'un destructeur entraîne un appel à std :: terminate car les destructeurs sont annotés implicitement avec noexcept .

Andrzej Krzemienski a publié un excellent article sur le thème des destructeurs qui jettent:

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

  

Dans 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 lorsque 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 comme noexcept (false) ,
  •   
  • Héritez votre classe d'un autre qui spécifie déjà son destructeur sous la forme noexcept (false) .
  •   
  • Placez dans votre classe un membre de données non statique qui spécifie déjà son destructeur sous la forme noexcept (false) .
  •   

Enfin, si vous décidez d’inclure le destructeur, vous devez toujours être conscient du risque d’une double exception (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 à l'aide de std :: uncaught_exception () .

  

Q: Ma question est donc la suivante: si   jeter d'un destructeur a pour résultat   comportement indéfini, comment gérez-vous   Les erreurs qui se produisent lors d’un destructeur?

R: Il y a plusieurs options:

  1. Laissez les exceptions sortir de votre destructeur, peu importe ce qui se passe ailleurs. Et ce faisant, sachez (voire craignez) 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 fave : Si std :: uncaught_exception renvoie false, laissez les exceptions s'écouler. Si le résultat est vrai, retournez à l'approche de journalisation.

Mais est-ce bien de jeter dans le dos d'ors?

Je suis d’accord avec la plupart des réponses ci-dessus pour dire qu’il est préférable d’éviter de jeter 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 existe quelques cas étranges où il s’agit d’une bonne idée à partir d’un destructeur. Comme le "doit vérifier" code d'erreur. 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 deviennent hors de portée, il générera une exception, de son destructeur .

Je suis actuellement conforme à la règle (ce que beaucoup disent) selon laquelle les classes ne doivent pas activement lancer des exceptions de leurs destructeurs, mais doivent plutôt fournir un paramètre public "close". méthode 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 réellement un " free / close " méthode qui s’appelle récursivement. 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 sera retourné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 tant 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 le découvrir, ce qui est bien aussi. Personnellement, je préfère les erreurs qui sautent plutôt que 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.

Martin Ba (ci-dessus) est sur la bonne voie. Votre architecte diffère de la logique RELEASE et COMMIT.

Pour la version:

Vous devriez manger toutes les 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. Si vous avez besoin d’une véritable gestion des erreurs, c’est probablement une conséquence des défauts de conception de votre modèle objet.

Pour engager:

C’est là que vous voulez le même type d’objets d’affichage RAII que ceux fournis par std :: lock_guard pour les mutex. Avec ceux-ci, vous ne mettez pas du tout la logique de validation dans le gestionnaire. Vous avez une API dédiée pour cela, puis des objets wrapper qui seront RAII le commettront dans LEUR rôle et traiteront les erreurs. Rappelez-vous que vous pouvez capturer des exceptions dans un destructeur parfaitement; c'est leur donner c'est mortel. Cela vous permet également d’implémenter la politique et la gestion des erreurs différentes simplement en construisant un wrapper différent (par exemple std :: unique_lock vs std :: lock_guard), et vous assure que vous n’oublierez pas d’appeler la logique de validation, qui est la seule à mi-chemin. justification décente de le placer dans un premier à la 1ère place.

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

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 générées dans des destructeurs.

Le problème se produit lorsqu'une exception est générée 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 doit 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 un message dans un fichier journal à la place.

  

Donc, ma question est la suivante: si le lancement d'un destructeur entraîne   comportement indéfini, comment gérez-vous les erreurs qui se produisent pendant une   destructeur?

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

Etant donné que les destructeurs sont appelés à la fois pour les chemins normaux et exceptionnels (échec), ils ne peuvent pas échouer eux-mêmes, 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 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 de   échouer sur un échec sont astronomiquement improbables, car faire des choses   impossible d'échouer peut être presque impossible dans certains cas.

La solution la plus appropriée pour moi consiste à écrire votre logique de non-nettoyage de manière à ce 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 d'éviter de la créer dans un destructeur.

Tout cela est bien 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 être possible d'écrire une logique de destructeur distincte pour les chemins d'exécution normaux, en dehors du cas exceptionnel, car les 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 réside toujours dans le fait que nous ne pouvons pas échouer et que 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, mais modélisez plutôt vos conceptions de manière plus volumineuse (exemple: système de particules avec 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 le temps système / mémoire nécessaire au traitement pour vous assurer que vos destructeurs ne peuvent pas échouer.

Et l’une des solutions les plus faciles 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 nom 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 qui peut échouer n'est exécutée que pendant l'exécution normale du système de particules lorsqu'il supprime une ou plusieurs particules. / p>

Il existe souvent des solutions comme celle-ci qui apparaissent si vous évitez de manipuler de nombreux objets minuscules avec des destructeurs non triviaux. Vous pouvez vous perdre dans un désordre où il semble presque impossible d'être une exception. La 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 se traduisait par une 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 pourrait le lancer. De cette façon, nous serions en mesure d’attraper tout cela au moment de la compilation si nous écrivions un destructeur par inadvertance qui pourrait le jeter.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top