Еще раз о том, почему плохо кидать исключения в деструкторах Многие эксперты по 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().Я тестировал этот пример на нескольких версиях GCC (8.2, 7.3) и Кланг (6.0, 5.0), продвижение стека продолжается везде.В ситуация, когда при поиске обработчика (18.3) наталкивается на самый внешний блок функции с спецификация негенерирующих исключений (18.4), зависит от реализации, разматывается ли стек, частично размотана или не размотана вообще до вызова std::terminate().
Если вы встретите компилятор, где реализация определена иначе, напишите об этом в комментариях.
Следует также отметить, что станд::прервать() При раскручивании стека он вызывается только тогда, когда за пределами деструктора выбрасывается исключение.
Если внутри деструктора есть блок 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, в результате чего мы опять же не увидим ошибки.
- Вынесите выпуск ресурса в отдельную функцию, которая возвращает значение или выдает исключение, и заставьте пользователя класса вызывать ее самостоятельно.
Это плохо, потому что пользователь может вообще забыть это сделать, и мы получим утечку ресурсов.
- Выбросить исключение.
Хорошо в обычных случаях, потому что.
пользователь класса может перехватить исключение и получить информацию о произошедшей ошибке стандартным способом.
Это плохо при раскрутке стека, т.к.
приводит к станд::прервать() .
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++-
Так Что Же Такое Ньян Кэт?
19 Oct, 24 -
Анненков Павел Васильевич.
19 Oct, 24 -
Будьте Осторожны С Blackberry Native Sdk.
19 Oct, 24 -
Покажи Мне Свои Настройки И Я Скажу Кто Ты
19 Oct, 24 -
Улучшенная Стабилизация На Youtube
19 Oct, 24 -
Инстинкт Bbdo Поздравляет Ооо «Марс»
19 Oct, 24