Деструктивные Исключения



Еще раз о том, почему плохо кидать исключения в деструкторах Многие эксперты по C++ (например, Герб города Саттер ) научите нас, что выбрасывать исключения в деструкторах — это плохо, потому что деструктор может сработать во время раскрутки стека, когда исключение уже было выброшено, и если в этот момент будет выброшено другое исключение, то будет вызван результат станд::прервать() .

Стандарт языка C++17 (здесь и далее я имею в виду свободно доступную версию проекта N4713 ) по этой теме говорит нам следующее:

18.5.1 Функция std::terminate() [кроме.

terminate] 1 В некоторых ситуациях от обработки исключений следует отказаться в пользу менее тонких методов обработки ошибок.

[Примечание: Эти ситуации: … (1.4) когда разрушение объекта во время раскрутки стека (18.2) завершается выдачей исключения, или … -конец примечания]

Давайте проверим это на простом примере:
  
  
  
  
  
  
  
  
  
  
  
   

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

Результат:

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

Обратите внимание, что деструктор PrintInDestructor по-прежнему вызывается, т.е.

после выдачи второго исключения раскрутка стека не прерывается.

В Стандарте (тот же пункт 18.5.1) на эту тему сказано следующее:

2… В ситуации, когда соответствующий обработчик не найден, зависит от реализации, разматывается ли стек перед вызовом std::terminate().

В ситуация, когда при поиске обработчика (18.3) наталкивается на самый внешний блок функции с спецификация негенерирующих исключений (18.4), зависит от реализации, разматывается ли стек, частично размотана или не размотана вообще до вызова std::terminate().

Я тестировал этот пример на нескольких версиях GCC (8.2, 7.3) и Кланг (6.0, 5.0), продвижение стека продолжается везде.

Если вы встретите компилятор, где реализация определена иначе, напишите об этом в комментариях.

Следует также отметить, что станд::прервать() При раскручивании стека он вызывается только тогда, когда за пределами деструктора выбрасывается исключение.

Если внутри деструктора есть блок try/catch, который перехватывает исключение и не выбрасывает его дальше, это не прерывает раскрутку стека внешнего исключения.



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

дисплеи

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

Как избежать неприятных ситуаций? Теоретически все просто: никогда не бросайте исключения в деструкторе.

Однако на практике красиво и элегантно реализовать это простое требование не так-то просто.



Если не можешь, но очень хочешь.

Сразу отмечу, что я не пытаюсь оправдать выдачу исключений из деструктора и вслед за Саттером, Мейерсом и другими гуру C++ призываю вас пытаться никогда не делайте этого (по крайней мере, в новом коде).

Однако в реальной практике программист вполне может столкнуться с устаревшим кодом, который не так-то просто привести к высоким стандартам.

Кроме того, описанные ниже методы часто могут быть полезны в процессе отладки.

Например, мы разрабатываем библиотеку с классом-оберткой, инкапсулирующим работу с определенным ресурсом.

Согласно принципам RAII, мы захватываем ресурс в конструкторе и должны освободить его в деструкторе.

Но что, если попытка освободить ресурс закончится ошибкой? Варианты решения этой проблемы:

  • Не обращайте внимания на ошибку.

    Это плохо, потому что мы скрываем проблему, которая может повлиять на другие части системы.

  • Напишите в лог.

    Лучше, чем просто игнорировать, но все равно плохо, потому что.

    наша библиотека ничего не знает о политиках журналирования, принятых системой, которая ее использует. Стандартный лог можно перенаправить в /dev/null, в результате чего мы опять же не увидим ошибки.

  • Вынесите выпуск ресурса в отдельную функцию, которая возвращает значение или выдает исключение, и заставьте пользователя класса вызывать ее самостоятельно.

    Это плохо, потому что пользователь может вообще забыть это сделать, и мы получим утечку ресурсов.

  • Выбросить исключение.

    Хорошо в обычных случаях, потому что.

    пользователь класса может перехватить исключение и получить информацию о произошедшей ошибке стандартным способом.

    Это плохо при раскрутке стека, т.к.

    приводит к станд::прервать() .

Как нам понять, находимся ли мы сейчас в процессе развертывания стека по исключению или нет? В C++ для этого есть специальная функция.

std::uncaught_Exception() .

С его помощью мы можем спокойно бросить исключение в обычной ситуации или сделать что-то менее правильное, но не приводящее к выбрасыванию исключения при размотке стека.



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

Результат:

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!

Обратите внимание, что функция std::uncaught_Exception() является устарел начиная со стандарта C++17, поэтому для компиляции примера необходимо подавить соответствующее предупреждение (см.

репозиторий с примерами из статьи ).

Проблема этой функции в том, что она проверяет, находимся ли мы в процессе раскручивания стека из-за исключения.

Но понять, вызывается ли текущий деструктор при раскрутке стека с помощью этой функции, невозможно.

В результате, если стек раскручивается, но деструктор какого-то объекта вызывается обычным образом, std::uncaught_Exception() все равно верну его истинный .



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

Результат:

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

В новом стандарте C++17 для замены std::uncaught_Exception() функция была введена std::uncaught_Exceptions() (обратите внимание на множественное число), которое вместо логического значения возвращает количество активных в данный момент исключений (здесь подробности оправдание ).

Вот как описанная выше проблема решается с помощью 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; }

Результат:

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



Когда вы очень, очень хотите сгенерировать несколько исключений одновременно

std::uncaught_Exceptions() позволяет избежать звонков станд::прервать() , но не помогает правильно обрабатывать несколько исключений.

В идеале мне хотелось бы иметь механизм, который позволил бы мне хранить все выброшенные исключения и затем обрабатывать их в одном месте.

Я хотел бы еще раз напомнить вам, что предложенный мной ниже механизм служит только для демонстрации концепции и не рекомендуется для использования в реальном промышленном коде.

Суть идеи — перехватывать исключения и хранить их в контейнере, а затем извлекать их одно за другим и обрабатывать.

Для хранения объектов исключений в языке C++ предусмотрен специальный тип.

std::Exception_ptr .

Структура типа в Стандарте не раскрыта, но сказано, что по сути она общий_ptr к объекту исключения.

Как тогда обрабатывать эти исключения? Для этого есть функция std::rethrow_Exception() , который принимает указатель std::Exception_ptr и выдает соответствующее исключение.

Нам нужно только перехватить его соответствующей секцией catch и обработать, после чего мы можем перейти к следующему объекту исключения.



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

Результат:

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

В приведенном выше примере для хранения объектов исключений используется стек, но обработка исключений будет осуществляться по принципу FIFO (т.е.

логически это очередь — первым будет обработано исключение, выброшенное первым).



выводы

Выбрасывать исключения в деструкторах объектов — очень плохая идея, и в любом новом коде я настоятельно рекомендую не делать этого при объявлении деструкторов.

нет, кроме .

Однако при поддержке и отладке устаревшего кода может возникнуть необходимость корректной обработки исключений, выдаваемых деструкторами, в том числе при раскручивании стека, и современный C++ предоставляет нам механизмы для этого.

Надеюсь, идеи, представленные в этой статье, помогут вам на этом нелегком пути.



Ссылки

Репозиторий с примерами из статьи Теги: #программирование #C++ #исключения C++
Вместе с данным постом часто просматривают: