jeter des exceptions d'un destructeur

257

La plupart des gens disent de ne jamais jeter d'exception d'un destructeur - cela entraîne un comportement indéfini. Stroustrup fait valoir que "le destructeur de vecteurs invoque explicitement le destructeur pour chaque élément. Cela implique que si un destructeur d'élément lance, la destruction de vecteur échoue ... Il n'y a vraiment aucun bon moyen de se protéger contre les exceptions levées par les destructeurs, donc la bibliothèque ne donne aucune garantie en cas de lancement d'un destructeur d'élément "(de l'annexe E3.2) .

Cet article semble dire le contraire - que lancer des destructeurs est plus ou moins correct.

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

Si une erreur se produit lors d'une opération de nettoyage, l'ignorez-vous simplement? S'il s'agit d'une erreur qui peut potentiellement être gérée dans la pile mais pas directement dans le destructeur, cela n'a-t-il pas de sens de lever une exception du destructeur?

Évidemment, ces types d'erreurs sont rares, mais possibles.

Greg Rogers
la source
36
"Deux exceptions à la fois" est une réponse courante mais ce n'est pas la VRAIE raison. La vraie raison est qu'une exception doit être levée si et seulement si les post-conditions d'une fonction ne peuvent pas être remplies. La postcondition d'un destructeur est que l'objet n'existe plus. Cela ne peut pas arriver. Toute opération de fin de vie sujette à des pannes doit donc être appelée comme une méthode distincte avant que l'objet ne soit hors de portée (les fonctions sensibles n'ont normalement qu'un seul chemin de réussite de toute façon).
spraff
29
@spraff: Savez-vous que ce que vous avez dit implique "jeter RAII"?
Kos
16
@spraff: avoir à appeler "une méthode distincte avant que l'objet ne soit hors de portée" (comme vous l'avez écrit) jette en fait RAII! Le code utilisant de tels objets devra s'assurer qu'une telle méthode sera appelée avant l'appel du destructeur. Enfin, cette idée n'aide pas du tout.
Frunsi
8
@Frunsi non, car ce problème vient du fait que le destructeur essaie de faire quelque chose au-delà de la simple libération de ressources. Il est tentant de dire "je veux toujours finir par faire du XYZ" et de penser que c'est un argument pour mettre une telle logique dans le destructeur. Non, ne soyez pas paresseux, écrivez xyz()et gardez le destructeur propre de la logique non RAII.
spraff
6
@Frunsi Par exemple, commettre quelque chose dans un fichier n'est pas nécessairement OK à faire dans le destructeur d'une classe représentant une transaction. Si la validation a échoué, il est trop tard pour le gérer lorsque tout le code impliqué dans la transaction est hors de portée. Le destructeur doit annuler la transaction à moins qu'une commit()méthode ne soit appelée.
Nicholas Wilson

Réponses:

198

Jeter une exception d'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 se résume essentiellement à:

Tout ce qui est dangereux (c'est-à-dire qui pourrait lever une exception) doit être fait via des 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 interceptant toutes les exceptions potentielles.

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

Donc, en fait, vous transférez la responsabilité à l'utilisateur. Si l'utilisateur est en mesure de corriger les exceptions, il appellera manuellement les fonctions appropriées et traitera toutes les erreurs. Si l'utilisateur de l'objet n'est pas inquiet (car l'objet sera détruit), le destructeur est laissé aux 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 s'assure qu'aucune exception ne se propage hors du destructeur.

Ainsi, si l'utilisateur d'un objet fichier souhaite effectuer une gestion spéciale des problèmes associés à la fermeture du fichier, il appellera manuellement close () et gérera toutes les exceptions. Si d'un autre côté ils s'en moquent alors le destructeur sera laissé pour gérer la situation.

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

Éditer:

Apparemment également dans le point 11 «Plus efficace C ++»
: Empêcher les exceptions de quitter les destructeurs

Martin York
la source
5
"Sauf si cela ne vous dérange pas de potentiellement terminer l'application, vous devriez probablement avaler l'erreur." - cela devrait probablement être l'exception (pardonnez le jeu de mots) plutôt que la règle - c'est-à-dire, échouer rapidement.
Erik Forbes,
15
Je ne suis pas d'accord. L'arrêt du programme arrête le déroulement de la pile. Plus aucun destructeur ne sera appelé. Toutes les ressources ouvertes seront laissées ouvertes. Je pense qu'avaler l'exception serait l'option préférée.
Martin York, le
20
Le système d'exploitation peut nettoyer les ressources dont il est le propriétaire. Mémoire, FileHandles etc. Qu'en est-il des ressources complexes: connexions DB. Cette liaison montante vers l'ISS que vous avez ouverte (va-t-elle automatiquement envoyer les connexions étroites)? Je suis sûr que la NASA voudrait que vous fermiez la connexion proprement!
Martin York
7
Si une application va "échouer rapidement" en abandonnant, elle ne devrait pas lever des exceptions en premier lieu. S'il échoue en passant le contrôle de la pile, il ne doit pas le faire d'une manière qui pourrait entraîner l'abandon du programme. L'un ou l'autre, ne choisissez pas les deux.
Tom
2
@LokiAstari Le protocole de transport que vous utilisez pour communiquer avec un vaisseau spatial ne peut pas gérer une connexion interrompue? Ok ...
doug65536
54

Le fait de jeter un destructeur peut entraîner un plantage, car ce destructeur peut être appelé dans le cadre du "déroulement de la pile". Le déroulement de la pile est une procédure qui a lieu lorsqu'une exception est levée. Dans cette procédure, tous les objets qui ont été poussé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 pendant cette procédure, un autre lancement d'exception n'est pas autorisé, car il n'est pas possible de gérer deux exceptions à la fois, ainsi, cela provoquera un appel à abort (), le programme se bloquera et le contrôle reviendra au système d'exploitation.

Gal Goldman
la source
1
pouvez-vous expliquer comment abort () a été appelé dans la situation ci-dessus. Signifie que le contrôle de l'exécution était toujours avec le compilateur C ++
Krishna Oza
1
@Krishna_Oza: Assez simple: chaque fois qu'une erreur est levée, le code qui déclenche une erreur vérifie un bit qui indique que le système d'exécution est en train de dérouler la pile (c'est-à-dire en gérer un autre throwmais sans avoir trouvé de catchbloc pour le moment) auquel cas std::terminate(not abort) est appelé au lieu de déclencher une (nouvelle) exception (ou de continuer le déroulement de la pile).
Marc van Leeuwen
53

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 à plusieurs teurs d'objets à l'intérieur des conteneurs. (Et cela peut être ignoré partiellement, car certains objets ne conviennent tout simplement pas à mettre dans un conteneur.)

Tout le problème devient plus facile à penser lorsque nous séparons les classes en deux types. Un dtor de classe peut avoir deux responsabilités différentes:

  • (R) libérer la sémantique (alias libérer cette mémoire)
  • (C) validation de la sémantique (aka flush file to disk)

Si nous considérons la question de cette façon, je pense que l'on peut faire valoir que la sémantique (R) ne devrait jamais provoquer d'exception d'un dtor car il n'y a a) rien que nous pouvons faire à ce sujet et b) de nombreuses opérations de ressources libres ne le font pas même prévoir une vérification des erreurs, par exemple .void free(void* p);

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

Si nous suivons la route RAII et permettons aux objets qui ont la sémantique (C) dans leurs d'tors, je pense que nous devons également tenir compte du cas étrange où de tels d'tors peuvent lancer. Il s'ensuit que vous ne devez pas placer de tels objets dans des conteneurs et il s'ensuit également que le programme peut toujours terminate()si un commit-dtor lève 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 un bon discours par un Andrei Alexandrescu : Gestion des erreurs dans C ++ / Declarative Control Flow (tenue au NDC 2014 )

Dans les détails, il explique comment la bibliothèque Folly implémente un UncaughtExceptionCounterpour leur ScopeGuardoutillage.

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

Bien que le discours ne se concentre pas sur le lancer d'un tor, il montre un outil qui peut être utilisé aujourd'hui pour se débarrasser des problèmes liés au moment de lancer à partir d'un tor.

À l' avenir , il peut y avoir une fonctionnalité standard pour cela, voir N3614 , et une discussion à ce sujet .

Upd '17: La fonctionnalité std C ++ 17 pour cela est std::uncaught_exceptionsafaikt. Je citerai rapidement l'article cppref:

Remarques

Un exemple où int-returning uncaught_exceptionsest utilisé est ... ... crée d'abord un objet de garde et enregistre le nombre d'exceptions non interceptées dans son constructeur. La sortie est effectuée par le destructeur de l'objet de garde à moins que foo () ne lance ( auquel cas le nombre d'exceptions non capturées dans le destructeur est supérieur à ce que le constructeur a observé )

Martin Ba
la source
6
Tout à fait d'accord. Et ajouter une sémantique de restauration sémantique (Ro) supplémentaire. Utilisé couramment dans la protection de portée. Comme dans mon projet où j'ai défini une macro ON_SCOPE_EXIT. Le cas de la sémantique de restauration est que quelque chose de significatif pourrait se produire ici. Nous ne devons donc vraiment pas ignorer l'échec.
Weipeng L
Je pense que la seule raison pour laquelle nous avons validé la sémantique dans les destructeurs est que C ++ ne prend pas en charge finally.
user541686
@Mehrdad: finally est un dtor. C'est toujours appelé, quoi qu'il arrive. Pour une approximation syntaxique de finalement, voir les différentes implémentations de scope_guard. De nos jours, avec la machinerie en place (même dans la norme, est-ce C ++ 14?) Pour détecter si le dtor est autorisé à lancer, il peut même être rendu totalement sûr.
Martin Ba
1
@MartinBa: Je pense que vous avez manqué le point de mon commentaire, ce qui est surprenant car j'étais d' accord avec votre idée que (R) et (C) sont différents. J'essayais de dire qu'un dtor est intrinsèquement un outil pour (R) et finallyest intrinsèquement un outil pour (C). Si vous ne voyez pas pourquoi: considérez pourquoi il est légitime de lancer des exceptions les unes sur les autres dans des finallyblocs, et pourquoi il n'en va pas de même pour les destructeurs. (Dans un certain sens, c'est une chose entre données et contrôle . Les destructeurs sont pour libérer des données, finallyc'est pour libérer le contrôle. Ils sont différents; il est malheureux que C ++ les lie ensemble.)
user541686
1
@Mehrdad: Trop long ici. Si vous le souhaitez, vous pouvez développer vos arguments ici: programmers.stackexchange.com/questions/304067/… . Merci.
Martin Ba
21

La vraie question à vous poser à propos du lancement d'un destructeur est "Que peut faire l'appelant avec cela?" Y a-t-il réellement quelque chose d'utile que vous pouvez faire à l'exception, qui compenserait les dangers créés par le lancement d'un destructeur?

Si je détruis un Fooobjet et que le Foodestructeur lance une exception, que puis-je raisonnablement en faire? Je peux l'enregistrer ou l'ignorer. C'est tout. Je ne peux pas le "réparer", car l' Fooobjet est déjà parti. Dans le meilleur des cas, j'enregistre l'exception et continue comme si de rien n'était (ou je termine le programme). Cela vaut-il vraiment la peine de provoquer un comportement indéfini en lançant un destructeur?

Parc Derek
la source
11
Je viens de remarquer ... lancer à partir d'un dtor n'est jamais un comportement indéfini . Bien sûr, il pourrait appeler terminate (), mais c'est un comportement très bien spécifié.
Martin Ba
4
std::ofstreamLe destructeur vide puis ferme le fichier. Une erreur de disque saturé peut se produire lors du vidage, avec laquelle vous pouvez absolument faire quelque chose d'utile: afficher à l'utilisateur une boîte de dialogue d'erreur indiquant que le disque est à court d'espace libre.
Andy
13

C'est dangereux, mais cela n'a pas non plus de sens du point de vue de la lisibilité / 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
}

Qu'est-ce qui devrait attraper l'exception? L'appelant de foo devrait-il? Ou devrait-il s'en occuper? Pourquoi l'appelant de foo devrait-il se soucier d'un objet interne à foo? Il pourrait y avoir un moyen pour le langage de définir cela, mais cela va être illisible et difficile à comprendre.

Plus important encore, où va la mémoire d'Object? Où va la mémoire de l'objet détenu? Est-il toujours alloué (apparemment parce que le destructeur est tombé en panne)? Considérez également que l'objet était dans l' espace de la pile , donc il est évident qu'il a disparu malgré tout.

Considérez alors 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 est garantie de ne pas échouer? C'est ma mémoire, bon sang!

Considérons maintenant dans le premier extrait de code que Object disparaît automatiquement car il se trouve sur la pile alors que Object3 est sur le tas. Depuis que le pointeur vers Object3 a disparu, vous êtes une sorte de 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 cette FAQ

Doug T.
la source
En ressuscitant cette réponse, re: le premier exemple, à propos int foo(), vous pouvez utiliser un bloc de fonction pour encapsuler la fonction entière foo dans un bloc try-catch, y compris les destructeurs de capture, si vous le souhaitez. Ce n'est toujours pas l'approche préférée, mais c'est une chose.
tyree731
13

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 Le processus d'appel de destructeurs pour des objets automatiques construits sur le chemin d'un bloc try à une expression throw est appelé «déroulement de pile». [Remarque: Si un destructeur appelé pendant le déroulement de la pile se termine avec une exception, std :: terminate est appelé (15.5.1). Les destructeurs doivent donc généralement intercepter les exceptions et ne pas les laisser se propager hors du destructeur. - note de fin]

lothar
la source
1
N'a pas répondu à la question - le PO en est déjà conscient.
Arafangion
2
@Arafangion Je doute qu'il était au courant de cela (std :: terminate étant appelé) car la réponse acceptée faisait exactement le même point.
lothar
@Arafangion comme dans certaines réponses ici, certaines personnes ont mentionné que abort () était appelé; Ou est-ce que std :: terminate appelle à son tour la fonction abort ().
Krishna Oza,
7

Votre destructeur s'exécute peut-être à l'intérieur d'une chaîne d'autres destructeurs. Le fait de lever une exception qui n'est pas interceptée par votre appelant immédiat peut laisser plusieurs objets dans un état incohérent, provoquant ainsi encore plus de problèmes, puis ignorant l'erreur dans l'opération de nettoyage.

Franci Penov
la source
7

Je fais partie du groupe qui considère que le modèle de «garde de portée» qui lance le destructeur est utile dans de nombreuses situations - en particulier pour les tests unitaires. Cependant, sachez qu'en C ++ 11, le lancement d'un destructeur entraîne un appel à std::terminatepuisque les destructeurs sont implicitement annotés avec noexcept.

Andrzej Krzemieński a un excellent article sur le sujet des destructeurs qui lancent:

Il souligne que C ++ 11 a un mécanisme pour remplacer la valeur noexceptpar défaut pour les destructeurs:

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

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

Le compilateur ajoutera toujours de manière invisible des spécifications noexceptà votre destructeur. Et cela signifie que le moment où votre destructeur lèvera une exception std::terminatesera appelé, même s'il n'y avait pas de situation de double exception. Si vous êtes vraiment déterminé à autoriser 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'une autre qui spécifie déjà son destructeur comme noexcept(false).
  • Placez un membre de données non statique dans votre classe qui spécifie déjà son destructeur comme noexcept(false).

Enfin, si vous décidez de lancer 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::terminateet c'est rarement ce que vous voulez. Pour éviter ce comportement, vous pouvez simplement vérifier s'il existe déjà une exception avant d'en lancer une nouvelle à l'aide de std::uncaught_exception().

GaspardP
la source
6

Tout le monde a expliqué pourquoi lancer des destructeurs est terrible ... que pouvez-vous faire? Si vous effectuez une opération qui peut échouer, créez une méthode publique distincte qui effectue le nettoyage et peut lever des exceptions arbitraires. Dans la plupart des cas, les utilisateurs l'ignoreront. Si les utilisateurs souhaitent 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...
};
À M
la source
Je cherche une solution mais ils essaient d'expliquer ce qui s'est passé et pourquoi. Je veux juste préciser que la fonction close est appelée à l'intérieur du destructeur?
Jason Liu
5

En plus des principales réponses, qui sont bonnes, complètes et précises, je voudrais commenter l'article que vous référencez - celui qui dit "lever des exceptions dans les destructeurs n'est pas si mal".

L'article prend la ligne «quelles sont les alternatives au lancement d'exceptions» et répertorie certains problèmes avec chacune des alternatives. Cela fait, il conclut que parce que nous ne pouvons pas trouver d'alternative sans problème, nous devons continuer à lever des exceptions.

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

DJClayworth
la source
1
Pas un comportement indéfini, mais une résiliation immédiate.
Marc van Leeuwen
La norme dit «comportement indéfini». Ce comportement est souvent une résiliation mais ce n'est pas toujours le cas.
DJClayworth
Non, lisez [except.terminate] dans Gestion des exceptions-> Fonctions spéciales (qui est 15.5.1 dans ma copie de la norme, mais sa numérotation est probablement obsolète).
Marc van Leeuwen
2

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

R: Il existe plusieurs options:

  1. Laissez les exceptions sortir de votre destructeur, indépendamment de ce qui se passe ailleurs. Et ce faisant, sachez (ou même craignez) que std :: terminate puisse 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_exceptionrenvoie faux, laissez-vous dériver. S'il revient vrai, revenez à l'approche de journalisation.

Mais est-ce bon de jeter des tors?

Je suis d'accord avec la plupart de ce qui précède que le lancer est mieux évité dans le destructeur, où il peut être. Mais parfois, il vaut mieux accepter que cela se produise et bien le gérer. Je choisirais 3 ci-dessus.

Il y a quelques cas étranges où c'est en fait une excellente idée de jeter d'un destructeur. Comme le code d'erreur "à vérifier". Il s'agit d'un type de valeur renvoyé par une fonction. Si l'appelant lit / vérifie le code d'erreur contenu, la valeur renvoyée se détruit silencieusement. Mais , si le code d'erreur renvoyé n'a pas été lu au moment où les valeurs de retour sortent de la portée, il lèvera une exception, de son destructeur .

MartinP
la source
4
Votre préféré est quelque chose que j'ai essayé récemment, et il s'avère que vous ne devriez pas le faire. gotw.ca/gotw/047.htm
GManNickG
1

Je suis actuellement en train de suivre la politique (que tant de gens disent) que les classes ne devraient pas lever activement les exceptions de leurs destructeurs mais devraient plutôt fournir une méthode publique "close" pour effectuer l'opération qui pourrait échouer ...

... mais je crois que les destructeurs pour les classes de type conteneur, comme un vecteur, ne devraient pas masquer les exceptions levées des classes qu'ils contiennent. Dans ce cas, j'utilise en fait une méthode "free / close" qui s'appelle récursivement. Oui, dis-je 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 s'exécuteront toujours et l'exception en attente se propagera une fois la routine de retour, ce qui est formidable. Si plusieurs exceptions se produisent, alors (selon le compilateur) soit cette première exception se propage, soit le programme se termine, ce qui est correct. Si tant d'exceptions se produisent que la récursion déborde la pile, alors quelque chose ne va vraiment pas, et quelqu'un va le découvrir, ce qui est également correct. Personnellement,

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

Matthieu
la source
1

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 les destructeurs.

Le problème se produit lorsqu'une exception est levée à partir d'un destructeur pendant le processus de déroulement de la pile. Si cela se produit, le compilateur est placé 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 se terminera immédiatement.

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

Devesh Agrawal
la source
1
L'écriture d'un message dans le fichier journal peut provoquer une exception.
Konard
1

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

Pour publication:

Vous devez manger toutes les erreurs. Vous libérez de la mémoire, fermez les connexions, etc. Personne d'autre dans le système ne devrait plus jamais VOIR ces choses, et vous remettez des ressources au système d'exploitation. S'il semble que vous ayez besoin d'une véritable gestion des erreurs ici, c'est probablement une conséquence de défauts de conception dans votre modèle d'objet.

Pour Commit:

C'est là que vous voulez le même type d'objets wrapper RAII que des choses comme std :: lock_guard fournissent des mutex. Avec ceux-là, vous ne mettez pas du tout la logique de validation dans le dtor. Vous avez une API dédiée pour cela, puis des objets wrapper qui le RAII le valideront dans LEURS dtors et y gèreront les erreurs. N'oubliez pas que vous pouvez très bien capturer des exceptions dans un destructeur; son émission qui est mortelle. Cela vous permet également d'implémenter une politique et une gestion des erreurs différente simplement en créant 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 pour le mettre dans un détecteur à la 1ère place.

user3726672
la source
1

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

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

Étant donné que les destructeurs sont invoqués pour des chemins normaux et exceptionnels (é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 valider une structure de données ou un fichier externe. Si la transaction échoue, la structure de fichiers / données peut être jetée. Il suffit ensuite de s'assurer que la validation des modifications à partir de cette structure / fichier externe constitue une transaction atomique qui ne peut pas échouer.

La solution pragmatique consiste peut-être simplement à s'assurer que les chances d'échec en cas d'échec sont astronomiquement improbables, car rendre les choses impossibles à échouer peut être presque impossible dans certains cas.

La solution la plus appropriée pour moi est d'écrire votre logique de non-nettoyage d'une manière telle 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, alors vous pourriez peut-être chercher à créer cette structure auxiliaire à l'avance afin que nous n'ayons plus à la créer à l'intérieur d'un destructeur.

Certes, c'est beaucoup plus facile à dire qu'à faire, 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 loin des chemins exceptionnels, car parfois les destructeurs se sentent un peu comme s'ils ont le double des responsabilités en essayant de gérer les deux (un exemple est les gardes de portée qui nécessitent un rejet explicite ; ils ne l'exigeraient pas s'ils pouvaient différencier les voies de destruction exceptionnelles des voies non exceptionnelles).

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 interagissant les uns avec les autres, et modélisez plutôt vos conceptions de manière légèrement plus volumineuse (exemple: système de particules avec un destructeur pour détruire la particule entière 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 vous pouvez souvent vous permettre la surcharge de mémoire / traitement requise pour vous assurer que vos destructeurs ne peuvent pas échouer.

Et c'est naturellement l'une des solutions les plus simples qui consiste à utiliser moins souvent des destructeurs. Dans l'exemple de particules ci-dessus, peut-être lors de la destruction / suppression d'une particule, certaines choses devraient être faites qui pourraient échouer pour une raison quelconque. Dans ce cas, au lieu d'invoquer une telle logique par le biais du dtor de la particule qui pourrait être exécuté dans un chemin exceptionnel, vous pourriez plutôt tout faire par le système de particules lorsqu'il supprime une particule. La suppression d'une particule peut toujours être effectuée lors d'un trajet non exceptionnel. Si le système est détruit, il peut peut-être simplement purger toutes les particules et ne pas s'embêter avec cette logique d'élimination de particules individuelle qui peut échouer, tandis 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.

Il existe souvent des solutions comme celle-ci qui surgissent si vous évitez de traiter de nombreux objets minuscules avec des destructeurs non triviaux. Où vous pouvez vous emmêler dans un gâchis où il semble presque impossible de faire exception à la sécurité, c'est quand vous vous enchevêtrez dans de nombreux objets minuscules qui ont tous des détecteurs non triviaux.

Cela aiderait beaucoup si nothrow / noexcept se traduisait réellement en une erreur de compilation si quelque chose 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 lancer. De cette façon, nous serions en mesure d'attraper tout cela au moment de la compilation si nous écrivons un destructeur qui pourrait se lancer par inadvertance.

Dragon Energy
la source
1
La destruction est un échec maintenant?
curiousguy
Je pense qu'il veut dire que des destructeurs sont appelés lors d'un échec, pour nettoyer cet échec. Donc, si un destructeur est appelé lors d'une exception active, il ne parvient pas à nettoyer une panne précédente.
user2445507
0

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

MRN
la source