Guru Of the Week n° 47 : exceptions non capturées

Difficulté : 6 / 10
Qu'est-ce que la fonction standard uncaught_exception(), et quand doit-on l'utiliser ? La réponse donnée ici n'est pas ce à quoi la plupart des gens s'attendent.

Retrouver l'ensemble des Guru of the Week sur la page d'index.

N'hésitez pas à commenter cet article ! Commentez Donner une note à l'article (0)

Article lu   fois.

Les deux auteurs

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Problème

I-A. Question JG

1. Que fait std::uncaught_exception() ?

I-B. Questions Guru

2. Considérez le code suivant :

 
Sélectionnez
    T::~T() {
      if( !std::uncaught_exception() ) {
        // ... code that could throw ...
      } else {
        // ... code that won't throw ...
      }
    }

Est-ce une bonne technique ? Présentez des arguments pour et contre.

3. Y a-t-il un autre bon usage de uncaught_exception ? Discutez et tirez vos conclusions.

II. Solution

II-A. Que fait std::uncaught_exception() ?

Pour citer directement la norme (15.5.3/1) :

La fonction bool uncaught_exception() retourne true après une évaluation de l'objet à lever jusqu'à réalisation de l'initialisation de la déclaration d'exception dans la routine de concordance (_lib.uncaught_). Cela inclut le déroulement de pile. Si l'exception est à nouveau levée (_except.throw_), uncaught_exception() retourne true à partir du point de nouvelle levée jusqu'à ce que l'exception nouvellement levée soit à nouveau capturée.

En l'occurrence, cette spécification est trompeusement proche de l'utilité.

II-B. Considérez le code suivant :

 
Sélectionnez

T::~T() {
    if( !std::uncaught_exception() ) {        // ... code that could throw ...    } else {        // ... code that won't throw ...    }}

Est-ce une bonne technique ? Présentez des arguments pour et contre.

II-B-1. Contexte : Le problème

Si un destructeur lève une exception, il peut se passer des problèmes. Spécifiquement, considérez un code comme le suivant :

 
Sélectionnez
    //  Le problème
    //
    class X {
    public:
      ~X() { throw 1; }
    };
 
    void f() {
      X x;
      throw 2;
    } // appelle X::~X (qui lève), puis appelle terminate()

Si un destructeur lève une exception alors qu'une autre exception est déjà active (ex. pendant un déroulement de pile), le programme est refermé. Habituellement, ce n'est pas une bonne chose.

II-B-2. La mauvaise solution

"Ha ha", beaucoup de gens - y compris de beaucoup d'experts - ont dit "utilisons uncaught_exception() pour déterminer si nous pouvons lever ou pas !" Et c'est de là que vient le code dans la Question 2... c'est une tentative pour résoudre le problème illustré :

 
Sélectionnez
    //  La mauvaise solution
    //
    T::~T() {
      if( !std::uncaught_exception() ) {
        // ... code that could throw ...
      } else {
        // ... code that won't throw ...
      }
    }

L'idée est que "nous utiliserons le chemin qui aussi longtemps qu'il sera sûr". Cette philosophie est mauvaise à deux titres : d'abord, ce code ne fait pas ça ; ensuite (et c'est plus important), cette philosophie en elle-même est une erreur.

II-B-2-a. La mauvaise solution : Pourquoi le code est mauvais

Un problème est que le code ci-dessus ne va pas réellement fonctionner comme attendu dans certaines situations. Voyez :

 
Sélectionnez
    //  Pourquoi la mauvaise solution est mauvaise
    //
    U::~U() {
      try {
        T t;
        // do work
      } catch( ... ) {
        // clean up
      }
    }

Si un objet U est détruit à cause d'un déroulement de pile pendant une propagation d'exception, T::~T échouera à utiliser le "code qui pourrait jeter" un chemin bien qu'il puisse.

Notez que ce code n'est pas matériellement différent du suivant :

 
Sélectionnez
    //  Variante : Une autre mauvaise solution
    //
    Transaction::~Transaction() {
      if( uncaught_exception() ) {
        RollBack();
      }
    }

Une fois encore, notez que ça ne marche pas si l'on tente une transaction dans un destructeur qui pourrait être appelé pendant un déroulement de pile :

 
Sélectionnez
    //  Variante : Pourquoi la mauvaise solution est encore mauvaise
    //
    U::~U() {
      try {
        Transaction t( /*...*/ );
        // do work
      } catch( ... ) {
        // clean up
      }
    }

II-B-2-b. La mauvaise solution : Pourquoi l'approche est immorale

De mon point de vue, néanmoins, le problème "ça ne marche pas" n'est pas le problème principal ici. Mon plus grand problème avec cette solution n'est pas technique, mais moral : c'est une faiblesse de conception que de donner deux sémantiques différentes à T::~T(), pour la simple et bonne raison que c'est toujours une faiblesse de conception que de permettre à une opération de rapporter la même erreur de deux façons différentes. Non seulement cela complique l'interface et la sémantique, mais en plus cela complique la vie de l'appelant parce que l'appelant devra être capable de traiter les deux saveurs du rapport d'erreur - et ce quand beaucoup trop de programmeurs ne vérifient pas bien les premiers !

II-B-3. La bonne solution

La bonne réponse au problème est beaucoup plus simple :

 
Sélectionnez
    //  La bonne solution
    //
    T::~T() /* throw() */ {
      // ... code that won't throw ...
    }
 
Sélectionnez
    //  Bonne solution alternative
    //
    T::Close() {
      // ... code that could throw ...
    }
 
    T::~T() /* throw() */ {
      try {
        Close();
      } catch( ... ) {
      }
    }

(Note : cf. GotW n°66 en ce qui concerne pourquoi ce bloc try est à l'intérieur du corps du destructeur et ne doit pas être un bloc try de fonction destructeur)

Cela suit précisément le principe "une fonction, une responsabilité"... un problème dans le code d'origine était que la même fonction était responsable à la fois de la destruction de l'objet et du nettoyage/rapport final.

à partir des normes de codage GotW :

- destructeurs

- ne jamais produire une exception à partir d'un destructeur (Meyers96: 58-61)

- si un destructeur appelle une fonction qui pourrait lever une exception, incorporez toujours l'appel dans un bloc try/catch qui empêche escape sur l'exception

- préférez déclarer les destructeurs comme "throw()"

Si ce numéro de GotW ne vous a pas convaincus de suivre cette norme de codage, il est possible que mes autres articles sur l'exception safety le feront. Reportez-vous au prochain CD à paraître de Scott Meyers extrait de ses ouvrages "Effective C++" et "More Effective C++", qui inclut aussi des articles sur le traitement d'exception que moi-même et Jack Reeves avons écrits. Dans mon chapitre, notez en particulier la partie titrée "Destructors That Throw and Why They're Evil" pour voir pourquoi vous ne pouvez pas appliquer de façon fiable new[] sur un ensemble d'objets dont les destructeurs peuvent appliquer "throw" (si vous conservez d'anciens numéros des C++ Report, vous trouverez les mêmes articles dans les numéros d'octobre 1997 et de novembre/décembre 1997).

II-C. Y a-t-il un autre bon usage de uncaught_exception ? Discutez et tirez vos conclusions..

Malheureusement, je ne connais aucun usage sûr de std::uncaught_exception. à mon avis : Ne vous en servez pas.

III. Remerciements

Cet article est une traduction en français par l'équipe de la rubrique C++ de l'article de Herb Sutter publié sur Guru of the Week. Vous pouvez retrouver cet article dans sa version originale sur le site de Guru of the Week : Uncaught Exceptions.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2009 Herb Sutter. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.