Des exceptions dévastatrices

Encore une fois, pourquoi il est mauvais de lancer des exceptions dans les destructeurs


De nombreux experts C ++ (par exemple, Herb Sutter ) nous apprennent que lancer des exceptions dans les destructeurs est mauvais, car vous pouvez entrer dans le destructeur pendant la promotion de la pile lorsque l'exception est déjà levée, et si à ce moment une autre exception est levée, le résultat sera appelé std :: terminate () . La norme de langage C ++ 17 (ci-après je me réfère à la version librement disponible du projet N4713 ) sur ce sujet nous dit ce qui suit:


18.5.1 La fonction std :: terminate () [except.terminate]

1 Dans certaines situations, la gestion des exceptions doit être abandonnée pour les techniques de gestion des erreurs moins subtiles. [Remarque:

Ces situations sont:

...

(1.4) lorsque la destruction d'un objet pendant le déroulement de la pile (18.2) se termine par le lancement d'une exception, ou

...

- note de fin]

Vérifions un exemple simple:


#include <iostream> class PrintInDestructor { public: ~PrintInDestructor() noexcept { std::cerr << "~PrintInDestructor() invoked\n"; } }; void throw_int_func() { std::cerr << "throw_int_func() invoked\n"; throw 1; } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { std::cerr << "~ThrowInDestructor() invoked\n"; throw_int_func(); } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor bad; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* c) { std::cerr << "Catched const char* exception: " << c << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Résultat:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked terminate called after throwing an instance of 'int' Aborted 

Notez que le destructeur PrintInDestructor est toujours appelé, c'est-à-dire après avoir levé la deuxième exception, la promotion de la pile n'est pas interrompue. La norme (le même paragraphe 18.5.1) à ce sujet dit ce qui suit:


2 ... Dans le cas où aucun gestionnaire correspondant n'est trouvé,
il est défini par l'implémentation, que la pile soit déroulée ou non avant l'appel de std :: terminate (). Dans
la situation où la recherche d'un gestionnaire (18.3) rencontre le bloc le plus à l'extérieur d'une fonction avec un
spécification d'exception sans lancement (18.4), il est défini par l'implémentation si la pile est déroulée,
déroulé partiellement ou pas déroulé du tout avant que std :: terminate () soit appelé ...

J'ai testé cet exemple sur plusieurs versions de GCC (8.2, 7.3) et Clang (6.0, 5.0), partout où la promotion de la pile continue. Si vous rencontrez un compilateur où la définition de l'implémentation est différente, veuillez en parler dans les commentaires.


Il convient également de noter que std :: terminate () est appelé lorsque la pile est déroulée uniquement lorsqu'une exception est levée du destructeur. S'il y a un bloc try / catch à l'intérieur du destructeur qui intercepte l'exception et ne lance plus, cela n'interrompt pas la promotion de la pile de l'exception externe.


 class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { throw_int_func(); } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor good; std::cerr << "ThrowCatchInDestructor instance created\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

affiche


 ThrowCatchInDestructor instance created throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Comment éviter les situations désagréables? En théorie, tout est simple: ne lancez jamais d'exceptions dans le destructeur. Cependant, dans la pratique, il n'est pas si simple de réaliser magnifiquement et élégamment cette simple exigence.


Si vous ne pouvez pas, mais que vous voulez vraiment ...


Je noterai immédiatement que je n'essaie pas de justifier la levée d'exceptions du destructeur, et à la suite de Sutter, Meyers et d'autres gourous C ++, je vous exhorte à essayer de ne jamais faire cela (au moins dans le nouveau code). Néanmoins, dans la pratique, un programmeur peut très bien rencontrer du code hérité, ce qui n'est pas si facile à conduire à des normes élevées. De plus, les techniques souvent décrites ci-dessous peuvent être utiles pendant le processus de débogage.

Par exemple, nous développons une bibliothèque avec une classe wrapper qui encapsule le travail avec une certaine ressource. Conformément aux principes de RAII, nous saisissons la ressource dans le constructeur et devons la libérer dans le destructeur. Mais que faire si une tentative de libération d'une ressource échoue? Options pour résoudre ce problème:


  • Ignorez l'erreur. Mauvais, car nous cachons un problème qui pourrait affecter d'autres parties du système.
  • Écrivez dans le journal. Mieux que de simplement l'ignorer, mais toujours mauvais, car notre bibliothèque ne sait rien des politiques de journalisation adoptées dans le système qui l'utilise. Le journal standard peut être redirigé vers / dev / null, à la suite de quoi, encore une fois, nous ne verrons pas d'erreur.
  • Prenez la version de la ressource dans une fonction distincte qui renvoie une valeur ou lève une exception et forcez l'utilisateur de la classe à l'appeler par lui-même. C'est mauvais, car l'utilisateur peut oublier de le faire et nous recevrons une fuite de ressources.
  • Jetez une exception. Bon dans les cas ordinaires, comme l'utilisateur de classe peut intercepter l'exception et obtenir des informations sur l'erreur de manière standard. Mauvais pendant la promotion de la pile, car conduit à std :: terminate () .

Comment savoir si nous sommes actuellement en train de promouvoir la pile par exception ou non? En C ++, il existe une fonction spéciale std :: uncaught_exception () pour cela . Avec son aide, nous pouvons lever une exception en toute sécurité dans une situation normale, ou faire quelque chose de moins correct, mais sans conduire à lever une exception pendant la promotion de la pile.


 class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~ThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~ThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor normal; std::cerr << "ThrowInDestructor normal destruction\n"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } try { ThrowInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Résultat:


 ThrowInDestructor normal destruction ~ThrowInDestructor() normal case, throwing throw_int_func() invoked ~PrintInDestructor() invoked Catched int exception: 1 ThrowInDestructor stack unwinding ~ThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Notez que la fonction std :: uncaught_exception () est obsolète depuis la norme C ++ 17, par conséquent, pour compiler l'exemple, le vorning correspondant doit être supprimé (voir le référentiel avec des exemples de l'article ).


Le problème avec cette fonction est qu'elle vérifie si nous sommes en train de faire tourner la pile par exception. Mais il est impossible de comprendre si le destructeur actuel est appelé pendant le processus de promotion de la pile. Par conséquent, s'il existe une promotion de pile, mais que le destructeur d'un objet est appelé normalement, std :: uncaught_exception () renverra toujours true .


 class MayThrowInDestructor { public: ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Résultat:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Dans la nouvelle norme C ++ 17, la fonction std :: uncaught_exceptions () a été introduite pour remplacer std :: uncaught_exception () (notez le pluriel) qui, au lieu d'une valeur booléenne, renvoie le nombre d'exceptions actuellement actives (voici une justification détaillée).


Voici comment le problème décrit ci-dessus est résolu avec std :: uncaught_exceptions () :


 class MayThrowInDestructor { public: MayThrowInDestructor() : exceptions_(std::uncaught_exceptions()) {} ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exceptions() > exceptions_) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: int exceptions_; }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Résultat:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() normal case, throwing throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Quand j'ai vraiment, vraiment envie de lever quelques exceptions à la fois


std :: uncaught_exceptions () évite d'appeler std :: terminate () , mais n'aide pas à gérer correctement plusieurs exceptions. Idéalement, j'aimerais avoir un mécanisme qui me permettrait d'enregistrer toutes les exceptions levées, puis de les traiter en un seul endroit.


Je tiens à rappeler une fois de plus que le mécanisme proposé par moi ci-dessous ne sert qu'à démontrer le concept et n'est pas recommandé pour une utilisation dans le vrai code industriel.

L'essence de l'idée est d'attraper des exceptions et de les enregistrer dans un conteneur, puis de les obtenir et de les traiter une par une. Afin d'enregistrer les objets d'exception, C ++ a un type spécial std :: exception_ptr . La structure de type dans la norme n'est pas divulguée, mais il est dit qu'elle est essentiellement shared_ptr par objet d'exception.


Comment alors traiter ces exceptions? Il existe une fonction std :: rethrow_exception () pour cela , qui prend un pointeur std :: exception_ptr et lève l'exception correspondante. Nous avons seulement besoin de l'attraper avec la section catch correspondante et de la traiter, après quoi nous pouvons passer à l'objet d'exception suivant.


 using exceptions_queue = std::stack<std::exception_ptr>; // Get exceptions queue for current thread exceptions_queue& get_queue() { thread_local exceptions_queue queue_; return queue_; } // Invoke functor and save exception in queue void safe_invoke(std::function<void()> f) noexcept { try { f(); } catch (...) { get_queue().push(std::current_exception()); } } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept { std::cerr << "~ThrowInDestructor() invoked\n"; safe_invoke([]() { throw_int_func(); }); } private: PrintInDestructor member_; }; int main(int, char**) { safe_invoke([]() { ThrowInDestructor bad; throw "BANG!"; }); auto& q = get_queue(); while (!q.empty()) { try { std::exception_ptr ex = q.top(); q.pop(); if (ex != nullptr) { std::rethrow_exception(ex); } } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } } return 0; } 

Résultat:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked Catched const char* exception: BANG! Catched int exception: 1 

Dans l'exemple ci-dessus, la pile est utilisée pour enregistrer les objets d'exception, cependant, la gestion des exceptions sera effectuée selon le principe FIFO (c'est-à-dire, logiquement, c'est la file d'attente - l'exception levée en premier sera la première à être traitée).


Conclusions


Lancer des exceptions dans les destructeurs d'objets est vraiment une mauvaise idée, et dans tout nouveau code, je recommande fortement de ne pas le faire en déclarant noexcept destructors. Cependant, avec la prise en charge et le débogage du code hérité, il peut être nécessaire de gérer correctement les exceptions levées par les destructeurs, y compris pendant la promotion de la pile, et le C ++ moderne nous fournit des mécanismes pour cela. J'espère que les idées présentées dans l'article vous aideront sur cette voie difficile.


Les références


Référentiel avec des exemples de l'article

Source: https://habr.com/ru/post/fr433944/


All Articles