Во многих книгах пишут, что нужно избегать exception-ов в конструкторах и деструкторах. Exception в конструкторе нам как бы «подсказывает», что объект не был сконструирован до конца и следовательно, нет смысла вызывать деструктор для такого объекта. Такое поведение чревато утечками ресурсов, поскольку как правило ресурсы освобождаются в деструкторе.
Пример:
#include <iostream> #include <memory> using namespace std; class B { public: B() { cout << __FUNCTION__ << endl; } ~B() { cout << __FUNCTION__ << endl; } }; class A { B* PtrToB; auto_ptr<B> autoB; public: A() { PtrToB = new B; autoB.reset(new B); cout << __FUNCTION__ << endl; throw 1; } ~A() { delete PtrToB; cout << __FUNCTION__ << endl; } }; void main() { cout << __FUNCTION__ << endl; { A a; } }
Результат:
main
B::B
B::B
A::A
!Exception!
Как видно из этого примера, даже деструктор автоматически созданного объекта не вызывался.
(Note: В действительности если бы это был не unhandled exception, то деструктор для B вызывался. Смотри обсуждение в комментариях.)
Добавляем try/catch в конструктор:
class B { public: B() { cout << __FUNCTION__ << endl; } ~B() { cout << __FUNCTION__ << endl; } }; class A { B* PtrToB; auto_ptr<B> autoB; public: A() { try { PtrToB = new B; autoB.reset(new B); cout << __FUNCTION__ << endl; throw 1; } catch (...) { cout << "I got it" << endl; } cout << "End of function" << endl; } ~A() { delete PtrToB; cout << __FUNCTION__ << endl; } }; void main() { cout << __FUNCTION__ << endl; { A a; } }
И результат:
main
B::B
B::B
A::A
I got it
End of function
B::~B
A::~A
B::~B
Exception поймали и работает как часы! Но, что делать, если exception происходит в конструкторе или деструкторе наследуемого объекта:
... class C { public: C() { cout << __FUNCTION__ << endl; throw 1; } ~C() { cout << __FUNCTION__ << endl; } }; class A: public C { B* PtrToB; auto_ptr<B> autoB; public: A() ...
Результат:
main
C::C
!Exception!
Попробуем так:
class A: public C { B* PtrToB; auto_ptr<B> autoB; public: A() try { PtrToB = new B; autoB.reset(new B); cout << __FUNCTION__ << endl; throw 1; } catch (...) { cout << "I got it" << endl; /* here is hidden throw */ } ...
Результат:
main
C::C
I got it
!Exception!
Вроде бы поймали, а вроде бы и нет ;). Exception поймали, но почему-то ничего дальше не произошло. Открываем disassembler и видим:
Сразу в конце блока catch компилятор вставил throw. Идея в том, что, если exception произошёл в констукторе базового класса, то это уже «фатально» и мы можем только освободить свои ресурсы, не более. Что делать, если всё таки хочется заглушить exception? Для этого есть «легальный» способ — добавить return до окончания блока catch:
class A: public C { B* PtrToB; auto_ptr<B> autoB; public: A() try { PtrToB = new B; autoB.reset(new B); cout << __FUNCTION__ << endl; throw 1; } catch (...) { cout << "I got it" << endl; return; /* here is hidden throw */ } ...
Результат:
main
C::C
I got it
B::~B
!Exception!
Почти добились цели, теперь конструктор не падает с exception-ом. Но PtrToB не проинициализирован и как результат программа падает при вызове в A::~A на освобождении памяти delete PtrToB. Переписываем ещё раз код в надежде, что теперь всё будет работать:
class A: public C { B* PtrToB; auto_ptr<B> autoB; void Init() throw() { PtrToB = new B; cout << "before autoB.reset() " << endl; autoB.reset(new B); cout << __FUNCTION__ << endl; } public: A() try { Init(); cout << __FUNCTION__ << endl; } catch (...) { Init(); cout << "I got it" << endl; return; /* here is hidden throw */ } ...
Результат:
main
C::C
B::B
before autoB.reset()
B::B
B::~B
!exception!
Проблема в том, что компилятор не вызвал конструктор объекта autoB и в нём хранится мусор. Как результат во время вызова auto_ptr<B>::reset внутренний указатель проверяется на NULL, поскольку указатель является !NULL, то метод reset пытается удалить не существующий объект. Можно найти много вариантов как обойти эту проблему, но я оставлю это за рамками изложения данного материала.
Что делать если у вас exception происходит в списке инициализации конструктора? Необходимо написать try сразу после объявления конструктора, но до списка инициализации, вот таким образом:
class B { public: B() { throw 1; cout << __FUNCTION__ << endl; } ~B() { cout << __FUNCTION__ << endl; } }; class C { public: C() { cout << __FUNCTION__ << endl; } ~C() { cout << __FUNCTION__ << endl; } }; class A: public C { B* PtrToB; public: A() try: C(), PtrToB(new B) { cout << __FUNCTION__ << endl; } catch (...) { //Hack: if an exception happens, PtrToB will have a garbage PtrToB = 0; cout << "I got it" << endl; return; /* here is hidden throw */ } ~A() { delete PtrToB; cout << __FUNCTION__ << endl; } };
Результат:
main
C::C
C::~C
I got it
A::~A
C::~C
Наконец-то программа доработала до конца без exception-ов. На этой оптимистической ноте хотел бы закончить.
P.S. Хотя в коде остался memory leak. Есть варианты его исправления, но поиск решения оставлю читателю.
Интересные выдержки из стандарта на тему:
[15.3 (Handling an exception).14] «If a return statement appears in a handler of the function-try-block of a constructor, the program is ill-formed.»
[15.3 (Handling an exception).15] «The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block
of a constructor or destructor. Otherwise, a function returns when control reaches the end of a handler for
the function-try-block (6.6.3). Flowing off the end of a function-try-block is equivalent to a return with no
value; this results in undefined behavior in a value-returning function (6.6.3).»
Простое следование стратегии RAII делает ненужным перехват исключений в конструкторах и все последующие хитроумные приседания.
Утверждение неверно. В первом примере специально был добавлен auto_ptr< B > autoB, чтобы показать, что RAII не работает для таких use-case-ов.
«Результат:
main
B::B
B::B
A::A
!Exception!»
Т.е. не был вызван деструктор класса B, хотя autoB был успешно проинициализирован.
В С++ есть простое правило: для любого (полностью) сконструированного объекта вызывается деструктор. Поэтому RAII всегда работает.
Первый пример некорректен. В main следует добавить блок try/catch. Тогда вывод будет следующий:
main
B::B
B::B
A::A
B::~B
Потому что вызов деструкторов при исключении происходит при раскрутке стека. А если блока try/catch нет, то работа приложения фактически завершается в момент генерации исключения и никакой раскрутки не выполняется.
Согласен отчасти, такой код тоже выход:
Однако, в случае unhandled exception RAII не работает => работает не всегда. (Добавил комментарий в текст поста по этому поводу.)
В действительности, если код является библиотекой, то нет гарантий как он будет вызываться. А при описанных выше «хитроумных приседаниях» можно по крайней мере вывести лог если произойдёт (unhandled) exception в конструкторе базового класса, например, если этот конструктор выкидывает exception по спецификации (throw (…)).
P.S. Сергей, спасибо за комментарии.
На практике RAII работает всегда. Если в программе появилось необработанное исключение, то вопрос об утечке ресурсов теряет всякий смысл. Операционная система подчистит всё что следует.
Для трассировки ошибок что-то из описанного возможно и пригодится, но это другой разговор.
>>Операционная система подчистит всё что следует.
[AK] Зависит от OS. Например, windows делает почти всё: «When the system is terminating a process, it does not terminate any child processes that the process has created.»Как этого можно достигнуть другая тема. Мысль в том, что use-case-ы использования function-try-block в конструкторах существуют. Конечно, есть альтернативы, но не для всех use-case-ов. И конечно в тексте поста есть некая «искусственность» примеров, но я пиши текст специально, чтобы примеры были наиболее наглядны и понятны, чтобы читать их было не сложно.
Ещё один интересный пример как в С++ не явно вызывается delete для памяти выделяемой для объекта через new, если в конструкторе происходит exception.
Результат:
main
new pointer:00424990 size: 1
B::B
free ptr:00424990
Exception