Est-ce que `catch (…) {throw; } `une mauvaise pratique?

74

Bien que je sois d’accord pour dire que capturer ... sans renverser est en effet une erreur, je pense cependant que l’utilisation de constructions comme celle-ci:

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

Est acceptable dans les cas où RAII n'est pas applicable . (S'il vous plaît, ne demandez pas ... tout le monde dans mon entreprise n'aime pas la programmation orientée objet et RAII est souvent considéré comme un "matériel scolaire inutile" ...)

Mes collègues disent que vous devez toujours savoir quelles exceptions doivent être levées et que vous pouvez toujours utiliser des constructions telles que:

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

Existe-t-il une bonne pratique bien admise concernant ces situations?

ereOn
la source
3
@Pubby: Pas sûr que ce soit exactement la même question. La question liée est plus sur "Dois-je attraper ..." alors que ma question se concentre sur "Dois-je mieux attraper ...ou <specific exception>avant de retransmettre"
ereOn
53
Désolé de le dire, mais C ++ sans RAII n'est pas C ++.
fredoverflow
46
Ainsi, vos ouvriers vendant la vache rejettent la technique qui a été inventée pour traiter un certain problème et se demandent ensuite laquelle des alternatives inférieures devrait être utilisée? Désolé de le dire, mais cela semble stupide , peu importe comment je le regarde.
sbi
11
"attraper ... sans le renvoyer est en effet faux" - vous vous trompez. In main, catch(...) { return EXIT_FAILURE; }pourrait bien avoir raison dans un code qui ne s'exécute pas sous un débogueur. Si vous n'attrapez pas, la pile peut ne pas être déroulée. Ce n'est que lorsque votre débogueur détecte des exceptions non interceptées que vous souhaitez les laisser main.
Steve Jessop
3
... donc même s'il s'agit d'une "erreur de programmation", il ne s'ensuit pas nécessairement que vous ne voulez pas le savoir. Quoi qu'il en soit, vos collègues ne sont pas de bons professionnels du logiciel, aussi, comme le dit sbi, il est très difficile de parler de la meilleure façon de gérer une situation chroniquement faible.
Steve Jessop

Réponses:

196

Mes collègues disent que vous devriez toujours savoir quelles exceptions doivent être levées [...]

Je détesterais le dire à votre collègue, de toute évidence, n’a jamais travaillé sur des bibliothèques polyvalentes.

Comment dans le monde une classe semblable peut-elle std::vectormême prétendre savoir ce que les constructeurs de copies vont lancer, tout en garantissant la sécurité des exceptions?

Si vous saviez toujours ce que l'appelant ferait au moment de la compilation, le polymorphisme serait inutile! Parfois, l’ objectif entier est de faire abstraction de ce qui se passe à un niveau inférieur, de sorte que vous ne voulez surtout pas savoir ce qui se passe!

Mehrdad
la source
32
En fait, même s’ils savaient que des exceptions doivent être levées. Quel est le but de cette duplication de code? À moins que le traitement ne diffère, je ne vois aucune raison de énumérer les exceptions pour montrer vos connaissances.
Michael Krelin - pirate informatique
3
@ MichaelKrelin-hacker: ça aussi. Ajoutez également à cela le fait qu'elles ont déconseillé les spécifications des exceptions, car la liste de toutes les exceptions possibles dans le code avait tendance à causer des bogues plus tard ... c'est la pire idée de tous les temps.
Mehrdad
4
Et ce qui me dérange, c’est ce qui pourrait être à l’origine d’une telle attitude quand on l’a associé à la vision d’une technique utile et commode comme «une matière scolaire inutile». Mais bon ...
Michael Krelin - pirate informatique
1
+1, l'énumération de toutes les options possibles est une excellente recette pour un futur échec. Pourquoi diable quelqu'un aurait-il choisi de le faire ...?
Littleadv
2
Bonne réponse. Il pourrait éventuellement être intéressant de mentionner que si un compilateur qu’il faut supporter a un bogue dans la zone X, alors utiliser les fonctionnalités de la zone X n’est pas intelligent, du moins ne pas l’utiliser directement. Par exemple, étant donné les informations sur la société, je ne serais pas surpris qu’ils utilisent Visual C ++ 6.0, qui contient quelques bêtises dans ce domaine (comme les destructeurs d’objets d’exception appelés deux fois) - certains petits descendants de ces premiers bogues ont survécu. ce jour-là, mais nécessitent des dispositions soigneuses pour se manifester.
Alf P. Steinbach
44

Vous semblez être pris au piège de quelqu'un qui essaie de manger son gâteau et de le manger.

La RAII et les exceptions sont conçues pour aller de pair. RAII est le moyen par lequel vous n'avez à écrire beaucoup de catch(...)déclarations à faire le nettoyage. Cela se produira automatiquement, bien sûr. Et les exceptions sont le seul moyen de travailler avec des objets RAII, car les constructeurs ne peuvent que réussir ou lancer (ou placer l'objet dans un état d'erreur, mais qui le souhaite?).

Une catchdéclaration peut faire l’une des deux choses suivantes: gérer une erreur ou des circonstances exceptionnelles, ou effectuer des travaux de nettoyage. Parfois, il fait les deux, mais chaque catchdéclaration existe pour faire au moins une de celles-ci.

catch(...)est incapable de gérer correctement les exceptions. Vous ne savez pas quelle est l'exception; vous ne pouvez pas obtenir d'informations sur l'exception. Vous n'avez absolument aucune information autre que le fait qu'une exception a été levée par quelque chose dans un certain bloc de code. La seule chose légitime que vous puissiez faire dans un tel blocage est de faire le ménage. Et cela signifie relancer l'exception à la fin du nettoyage.

Ce que RAII vous donne en ce qui concerne la gestion des exceptions est un nettoyage gratuit. Si tout est correctement encapsulé dans RAII, tout sera nettoyé correctement. Vous n'avez plus besoin que les catchdéclarations nettoient. Dans ce cas, il n'y a aucune raison d'écrire une catch(...)déclaration.

Donc, je conviens que catch(...)c'est surtout du mal ... provisoirement .

Cette disposition étant le bon usage de RAII. Parce que sans cela, vous devez pouvoir effectuer certains travaux de nettoyage. Il n'y a pas moyen de s'en sortir; vous devez être capable de faire le travail de nettoyage. Vous devez pouvoir vous assurer que le lancement d'une exception laissera le code dans un état raisonnable. Et catch(...)est un outil essentiel pour le faire.

Tu ne peux pas en avoir un sans avoir l'autre. Vous ne pouvez pas dire que les deux RAII et catch(...) sont mauvais. Vous avez besoin d'au moins un d'entre eux. sinon, vous n'êtes pas exceptionnellement en sécurité.

Bien sûr, il y a une utilisation valable, bien que rare, catch(...)que même la RAII ne puisse bannir: la exception_ptrtransmission à quelqu'un d'autre. Généralement via une promise/futureinterface ou similaire.

Mes collègues disent que vous devez toujours savoir quelles exceptions doivent être levées et que vous pouvez toujours utiliser des constructions telles que:

Votre collègue est un idiot (ou tout simplement terriblement ignorant). Cela devrait être immédiatement évident en raison de la quantité de code copier-coller qu'il suggère que vous écriviez. Le nettoyage de chacune de ces déclarations de capture sera exactement le même . C'est un cauchemar d'entretien, sans parler de la lisibilité.

En bref: c’est le problème que RAII a été créé pour résoudre (non pas qu’il ne résout pas d’autres problèmes).

Ce qui me déroute dans cette idée, c'est que c'est généralement à l'envers que la plupart des gens prétendent que RAII est mauvais. En général, l'argument est le suivant: "RAII est mauvais, car vous devez utiliser des exceptions pour signaler une défaillance du constructeur. Mais vous ne pouvez pas lancer d'exceptions, car ce n'est pas sûr et vous devez disposer de nombreuses catchinstructions pour tout nettoyer." Ce qui est un argument cassé parce que RAII résout le problème créé par le manque de RAII.

Plus que probablement, il est contre RAII parce qu'il cache des détails. Les appels de destructeurs ne sont pas immédiatement visibles sur les variables automatiques. Donc, vous obtenez un code qui est appelé implicitement. Certains programmeurs détestent vraiment ça. Apparemment, au point où ils pensent avoir 3 catchdéclarations qui font toutes la même chose avec du code copier-coller est une meilleure idée.

Nicol Bolas
la source
2
Il semble que vous n'écriviez pas de code offrant une garantie de sécurité exceptionnelle. RAII aide à fournir une garantie de base . Mais afin de fournir une garantie solide, vous devez annuler certaines actions pour rétablir le système dans l'état où il se trouvait avant l'appel de la fonction. La garantie de base est le nettoyage , la garantie forte est le retour en arrière . Faire une restauration est spécifique à une fonction. Donc, vous ne pouvez pas le mettre dans "RAII". Et c'est à ce moment que le bloc fourre-tout devient pratique. Si vous écrivez du code avec une forte garantie, vous utilisez fourre-tout beaucoup.
anton_rh
@anton_rh: Peut-être, mais même dans ces cas, les déclarations fourre-tout sont l'outil de dernier recours . L'outil préféré consiste à faire tout ce qui est jeté avant de modifier tout état dans lequel vous auriez à revenir à une exception. Évidemment, vous ne pouvez pas tout mettre en œuvre de cette manière dans tous les cas, mais c'est le moyen idéal d'obtenir la garantie d'exception forte.
Nicol Bolas
14

Deux commentaires, vraiment. La première est que, même dans un monde idéal, vous devriez toujours savoir quelles exceptions peuvent être levées. En pratique, si vous utilisez des bibliothèques tierces ou si vous compilez avec un compilateur Microsoft, vous ne le savez pas. Plus précisément, cependant; même si vous connaissez exactement toutes les exceptions possibles, est-ce pertinent ici? catch (...)exprime l’intention bien mieux que catch ( std::exception const& ), même en supposant que toutes les exceptions possibles découlent de std::exception(ce qui serait le cas dans un monde idéal). En ce qui concerne l’utilisation de plusieurs blocs de capture, s’il n’existe pas de base commune pour toutes les exceptions: il s’agit là d’un obscurcissement total et d’un cauchemar d’entretien. Comment reconnaissez-vous que tous les comportements sont identiques? Et que c'était l'intention? Et que se passe-t-il si vous devez modifier le comportement (résolution de bogue, par exemple)? C'est trop facile d'en manquer un.


la source
3
En fait, mon collègue a conçu sa propre classe d’exception qui ne découle passtd::exception et essaie tous les jours d’imposer son utilisation à notre base de code. J'imagine qu'il essaie de me punir pour avoir utilisé du code et des bibliothèques externes qu'il n'a pas écrites.
ereOn
17
@ereOn Il me semble que votre collègue a un besoin urgent de formation. En tout cas, j'éviterais probablement d'utiliser des bibliothèques écrites par lui.
2
Les modèles et le choix des exceptions possibles vont de pair avec le beurre de cacahuète et le geckos morts. Quelque chose d'aussi simple que std::vector<>peut lancer n'importe quelle exception pour à peu près n'importe quelle raison.
David Thornley
3
Dites-nous, comment exactement savez-vous quelle exception sera levée par le correctif de demain, plus loin dans l’arbre des appels?
mattnz
11

Je pense que votre collègue a mélangé quelques bons conseils - vous ne devriez gérer les exceptions connues que dans un catchbloc lorsque vous ne les relancez pas.

Ça signifie:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

Est mauvais car il cachera silencieusement toute erreur.

Pourtant:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

C'est bon - nous savons ce que nous avons à faire et n'avons pas besoin de l'exposer au code d'appel.

Également:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

C’est bien, même selon les meilleures pratiques, le code permettant de traiter les erreurs générales devrait être lié au code qui les provoque. C'est mieux que de compter sur l'appelé pour savoir qu'une transaction doit être annulée ou peu importe.

Keith
la source
9

Toute réponse par oui ou par non doit être accompagnée d'une justification expliquant pourquoi il en est ainsi.

Dire que c'est faux simplement parce que j'ai appris de cette façon, c'est du fanatisme aveugle.

Écrire la même chose //Some cleanup; throwplusieurs fois, comme dans votre exemple, est une erreur car il s'agit d'une duplication de code, ce qui représente une charge de maintenance. L'écrire juste une fois, c'est mieux.

Écrire une commande catch(...)pour réduire au silence toutes les exceptions est une erreur, car vous ne devez gérer que les exceptions que vous savez manipuler. Grâce à ce caractère générique, vous pouvez effectuer plus de tâches que vous ne le souhaitez et faire taire les erreurs importantes.

Mais si vous redemandez après a catch(...), la dernière raison ne s'applique plus, car vous ne gérez pas l'exception, il n'y a donc aucune raison pour que cela soit découragé.

En fait, je l'ai fait pour la connexion de fonctions sensibles sans aucun problème:

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}
pestophage
la source
2
Espérons Log(...)ne peut pas jeter.
Déduplicateur
2

Je suis généralement d’accord avec l’ambiance des articles. Je n'aime pas vraiment le modèle d’attrapage d’exceptions spécifiques. Je pense que la syntaxe de cette méthode en est encore à ses balbutiements et qu’elle n’est pas encore capable de gérer le code redondant.

Mais puisque tout le monde le dit, je dirai que même si je les utilise avec parcimonie, j’ai souvent regardé l’une de mes déclarations "catch (exception e)" et déclaré: "Bon sang, j’aurais aimé appeler les exceptions spécifiques de cette époque "parce que quand on rentre plus tard, il est souvent agréable de savoir quelle était l'intention et ce que le client est susceptible de jeter en un coup d'oeil.

Je ne justifie pas l'attitude de «Toujours utiliser x», mais simplement dire qu'il est parfois agréable de les voir répertoriées et je suis sûr que certaines personnes pensent que c'est la «bonne» façon de faire.

Bill K
la source