Об опасностях, таящихся в мороженом корме коде, генерирующем исключения.
Исключения в C++ достаточно опасны, именно поэтому я стараюсь их не использовать.
Давайте разберёмся вот с этим кодом:
#include <stdio.h> class x { public: x() { printf("x()\n"); } ~x() { printf("~x()\n"); } }; class e { public: e() { printf("e()\n"); } e(const e& src) { printf("e(src)\n"); } ~e() { printf("~e()\n"); } }; void g(void) { throw e(); } void f(void) { x* ptr = new x(); g(); delete ptr; } int main(void) { try { f(); } catch (e) { printf("Caught exception e\n"); } } |
Казалось бы, достаточно простой код, но он таит в себе Memory leak: объект класса x создаётся, но никогда не уничтожается. Это очень вырожденный пример, но он передаёт очень важную суть: если вы не ожидаете от функции g(), что она будет генерировать исключение, и пишете код, то когда функция g() начинает генерировать исключения, код становится работающим некорректно, в чём вы и можете убедиться, запустив программу:
x() e() e(src) Caught exception e ~e() ~e()
Обойти это можно, сделав класс-обвязку (а-ля smart ptr):
template <class ptr> class p { private: ptr *m_ptr; public: p(): m_ptr(0) {} p(ptr *pp): m_ptr(pp) {} ~p() { if (m_ptr!=0) delete m_ptr; } ptr *drop() { ptr *res = m_ptr; if(m_ptr!=0) { delete m_ptr; m_ptr = 0; } return res; } ptr &operator *() { return *m_ptr; } ptr &operator ->() { return *m_ptr; } bool validate() { return m_ptr != 0; } operator bool() { return m_ptr != 0; } }; |
И переписав функцию f():
void f(void) { p<x> ptr(new x()); if (ptr) printf("OK, validated\n"); g(); } |
Получаем вполне нормальное поведение:
x() OK, validated e() ~x() e(src) Caught exception e ~e() ~e()
Но… Если функция g() была использована во многих местах, это же получается тотальный рефакторинг кода! И не надо меня убеждать в том, что изначально надо было пользоваться штуками типа smart ptr!
Решить проблему можно, сделав сеппуку для функции g() и разбив её на две функции: враппер и, собственно, саму реализацию, то есть:
void _g(void) { throw e(); } void g(void) { try { _g(); } catch (e) { printf("Caught exception e\n"); } } void f(void) { x *ptr = new x(); g(); delete ptr; } int main(void) { f(); } |
То есть, мы избежим тотального рефакторинга, но при этом сможем пользоваться новой функцией _g(), которая будет кидать исключения. С другой стороны, это не всегда корректно. Представим себе ситуацию, что генерация исключения e() требует обязательного освобождения какого-либо ресурса, о котором функция _g() не знает (и ввиду её специфичной реализации не должна знать). Тогда если код, который будет отлавливать исключение e(), будет неявно пользоваться и g(), и _g(), то он рискует не освободить ресурс, когда это надо. Характерный пример:
class x { public: x() { printf("x()\n"); } ~x() { printf("~x()\n"); } void alarm() { printf("alarm!\n"); } }; void _g(void) { throw e(); } void g(void) { try { _g(); } catch (e) { printf("Caught exception e\n"); } } void z(bool r) { if (r) g(); else _g(); } void f(bool r) { x *ptr = new x(); try { z(r); } catch (e) { ptr->alarm(); } delete ptr; } int main(void) { f(true); f(false); } |
В результате выполнения получаем:
x() e() e(src) Caught exception e ~e() ~e() ~x() x() e() e(src) alarm! ~e() ~e() ~x()
То есть, «Caught exception e» для нас – это неожиданная ситуация, так как мы ожидаем, что если _g() не отработала, то мы должны сами словить исключение и получить «alarm!».
Таким образом, исключения в C++ таят достаточно большую опасность, если их использовать неаккуратно. Попытка обойти подобную ситуацию есть в java – это специальная директива throws у метода, которая вынуждает разработчика либо ловить исключение, либо передавать его дальше, что, хоть, и не спасает от генерации Runtime Exception, но помогает сразу найти места, где вызывается метод, который генерирует исключение (потому что иначе просто ничего не скомпилится).
В общем, на этом пока мысли останавливаются.
> Таким образом, исключения в C++ таят достаточно большую опасность, если их использовать неаккуратно.
Ну так не юзайте гуан, выберайте среду, функционал которой достаточен и должен быть избыточным для реализации. Исключение с точки зрения треда с кпл 3 это всеголишь сохранение контекста и передача управления на одну из фиксированных точек, которые по своей сути являются ядерными калбэками. Нет абсолютно никаких проблем с обработкой сепшенов(за исключением разрушения стека). Кстате кресты относятся к быдлокодерским языкам
А кто сказал, что C++ – гуано
?
Просто нужно представлять, какие элементы какую опасность таят.
> А кто сказал, что C++ – гуано ?
На лукоморье сказано
> Просто нужно представлять, какие элементы какую опасность таят.
Надстройку сделайте собственную, зачем тогда сех.