Исключения в конструкторах и деструкторах

Обновлено: 19.04.2024

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Возврат кода ошибки

- Ошибку можно проигнорировать

- Делает код громозким

Поддержка со стороны C++

- При неправильном использовании могут усложнить программу

Что такое исключительная ситуация?

Ошибка которую нельзя обработать на данном уровне и игнорирование которой делает дальнейшую работу программы бессмысленной.

Гарантии безопасности исключений (exception safety)

  1. Гарантировано искючений нет (No-throw guarantee)

Операции всегда завершаются успешно, если исключительная ситуация возникла она обрабатывается внутри операции.

Также известна как коммит ролбек семантика (commit/rollback semantics). Операции могут завершиться неудачей, но неудачные операции гарантированно не имеют побочных эффектов, поэтому все данные сохраняют свои исходные значения.

Выполнение неудачных операций может вызвать побочные эффекты, но все инварианты сохраняются и нет утечек ресурсов (включая утечку памяти). Любые сохраненные данные будут содержать допустимые значения, даже если они отличаются от того, что они были до исключения.

Поиск подходящего обработчика

  1. Поиск подходящего обработчика идет в порядке следования обработчиков в коде
  2. Полного соответствия типа не требуется, будет выбран первый подходящий обработчик
  3. Если перехватывать исключение по значению, то возможна срезка до базового класса
  4. Если наиболее общий обработчик идет раньше, то более специализированный обработчик никогда не будет вызван
  5. Три точки - перехват любого исключения

Исключения ОС - не исключения С++, например, деление на ноль. Для их обработки нужно использовать средства предоставляемые конкретной платформой

Поиск подходящего обработчика вниз по стеку вызовов с вызовом деструкторов локальных объектов - раскрутка стека.

Если подходящий обработчик не был найден вызывается стандартная функция terminate.

Вызывает стандартную функцию С - abort.

abort - аварийное завершение программы, деструкторы объектов вызваны не будут.

Поведение terimnate можно изменить установив свой обработчик функцией set_terminate.

Где уместен catch (. )?

Только в main, для того, чтобы поймать необработанное исключение, чтобы избежать вызов terminate и таким образом завершить работу с вызовом деструкторов.

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

Исключения в деструкторе

Исключение покинувшее деструктор во время раскрутки стека или у глобального/статического объекта приведет к вызову terminate.

Начиная с С++11 все деструкторы компилятором воспринимаются как помеченные noexcept - теперь исключения не должны покидать деструктора никогда.

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

Клиент либо получает объект в консистентном состоянии, либо не получает ничего.

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

Говорят, что конструктор конструктор и деструктор класса не должны вырабатывать исключения?
В тоже время, я видел огромное количество кода и даже примеров из книг, где в конструкторе выделяется память при помощи оператора `new`, но ведь даже он может вырабатывать исключение `bad_alloc` — выходит, что весь этот код потенциально опасный? Как с этим бороться?

Исключения, ровно как и оператор return прерывают поток выполнения команд функции, из системного стека выбираются объекты (такие как локальные переменные) и для них вызываются деструкторы. Однако, если при выполнении оператора return раскрутка стека прекратиться в точке где была вызвана завершенная функция, то при при выполнении throw объекты из стека будут уничтожаться до тех пор, пока управление не будет передано в блок try<> , содержащий обработчик, соответствующий типу выброшенного исключения. Читать подробнее про обработку исключений [1].

Исключения в деструкторе класса

В связи с этим, в большинстве случаев разрушение объектов созданных на стеке (без использования new/malloc ) произойдет корректно — вызовом деструктора. Однако исключения в конструкторе или деструкторе могут приводить к нежелательным последствиям.

Во-первых, программа не должна вырабатывать исключения во время обработки другого исключения (когда происходит раскрутка стека) — это приведет к аварийному завершению работы программы (фактически вызову abort() ), которое уже не получится корректно обработать. Причина такой ошибки заключается в том, что один из деструкторов вырабатывает исключение или не обрабатывает исключение функции, которую вызывает:

destructor_exception_abort

Тем более очевидно, что если деструктор завершает работу исключением, то может возникать утечка памяти — в памяти могут остаться как части текущего класса, так и базовых классов. Из этого ясно, что деструктор никогда не должен вырабатывать исключения, а также обрабатывать все возможные исключения функций, которые вызывает — они могут приводить как к утечкам, так и к очень трудноуловимым ошибкам.

Еще один из аспектов работы деструкторов и исключений иллюстрирует следующий фрагмент кода:

Когда исключение покидает блок, все локальные объекты, созданные в этом блоке, уничтожаются. Если деструктор объекта, уничтожаемого во время развертки стека, генерирует исключение, то программа будет завершена досрочно, и ее уже ничего не спасет.

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

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

Исключения в конструкторе класса

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

Стандарт языка С++ гарантирует, что если исключение возникнет в конструкторе, то памяти из под членов-данных класса будет освобождена корректно вызовом деструктора — т.е. если вы используете идиому RAII [2], то проблем не будет. Часто для этого достаточно использовать std::vector/std::string вместо старых массивов и строк, и умные указатели вместо обычных [3]. Если же вы продолжите использовать сырые указатели и динамически выделять память — нужно будет очень тщательно следить за ней, например в следующем фрагменте кода нет утечки, т.к. исключение будет выработано только если память не будет выделена [4]:

Ещё раз о том, почему плохо бросать исключения в деструкторах

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

18.5.1 The std::terminate() function [except.terminate]

1 In some situations exception handling must be abandoned for less subtle error handling techniques. [ Note:

(1.4) when the destruction of an object during stack unwinding (18.2) terminates by throwing an exception, or

Проверим на простом примере:

Обратите внимание, что деструктор PrintInDestructor всё же вызывается, т.е. после выбрасывания второго исключения раскрутка стека не прерывается. В Стандарте (тот же самый пункт 18.5.1) на эту тему сказано следующее:

2… In the situation where no matching handler is found,
it is implementation-defined whether or not the stack is unwound before std::terminate() is called. In
the situation where the search for a handler (18.3) encounters the outermost block of a function with a
non-throwing exception specification (18.4), it is implementation-defined whether the stack is unwound,
unwound partially, or not unwound at all before std::terminate() is called .

Я проверял этот пример на нескольких версиях GCC (8.2, 7.3) и Clang (6.0, 5.0), везде раскрутка стека продолжается. Если вы встретите компилятор, где implementation-defined по-другому, пожалуйста, напишите об этом в комментариях.

Следует заметить также, что std::terminate() при раскрутке стека вызывается только тогда, когда исключение выбрасывается наружу из деструктора. Если внутри деструктора находится try/catch блок, который ловит исключение и не пробрасывается дальше, это не приводит к прерыванию раскрутки стека внешнего исключения.

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

Если нельзя, но очень хочется.

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

Например, мы разрабатываем библиотеку с классом-обёрткой, инкапсулирующей работу с неким ресурсом. В соответствии с принципами RAII мы захватываем ресурс в конструкторе и должны освободить его в деструкторе. Но что если попытка освободить ресурс заканчивается ошибкой? Варианты решения этой проблемы:

  • Проигнорировать ошибку. Плохо, потому что мы скрываем проблему, которая может повлиять на другие части системы.
  • Написать в лог. Лучше, чем просто проигнорировать, но всё равно плохо, т.к. наша библиотека ничего не знает о политиках логирования, принятых в системе, которая её использует. Стандартный лог может быть перенаправлен в /dev/null, в результате чего, опять же, ошибку мы не увидим.
  • Вынести освобождение ресурса в отдельную функцию, которая возвращает значение или бросает исключение, и заставлять пользователя класса вызывать её самостоятельно. Плохо, потому что пользователь вообще может забыть это сделать, и мы получим утечку ресурса.
  • Выбросить исключение. Хорошо в обычных случаях, т.к. пользователь класса может поймать исключение и стандартным образом получить информацию о возникшей ошибке. Плохо во время раскрутки стека, т.к. приводит к std::terminate().

Как же понять, находимся ли мы в данный момент в процессе раскрутке стека по исключению или нет? В C++ для этого есть специальная функция std::uncaught_exception(). С её помощью мы можем безопасно кидать исключение в обычной ситуации, либо делать что-либо менее правильное, но не приводящее к выбросу исключения во время раскрутки стека.

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

Проблема с этой функцией в том, что она проверяет находимся ли мы в процессе раскрутки стека по исключению. Но вот понять, вызван ли текущий деструктор в процессе раскрутки стека, с помощью этой функции невозможно. В результате, если происходит раскрутка стека, но деструктор какого-то объекта вызывается нормальным образом, std::uncaught_exception() всё равно вернёт true.

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

Вот как описанная выше проблема решается при помощи std::uncaught_exceptions():

Когда очень-очень хочется выбросить сразу несколько исключений

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

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

Суть идеи состоит в том, чтобы ловить исключения и сохранять их в контейнер, а затем по одному доставать и обрабатывать. Для того, чтобы сохранять объекты исключений, в языке C++ есть специальный тип std::exception_ptr. Структура типа в Стандарте не раскрывается, но говорится, что это по сути своей shared_ptr на объект исключения.

Как же потом обработать эти исключения? Для этого есть функция std::rethrow_exception(), которая принимает указатель std::exception_ptr и выбрасывает соответствующее исключение. Нам нужно только поймать его соответствующей catch-секцией и обработать, после чего можно переходить к следующему объекту исключения.

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

Выводы

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

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

Существует множество «за» и «против» такого способа перехвата и обработки исключений, но сегодня я хочу рассмотреть несколько другую тему. А именно тему обеспечения согласованного состояния приложения в свете возникновения исключения – три уровня безопасности исключений.

Три типа гарантий

В конце 90-х годов Дейв Абрахамс (Dave Abrahams) предложил три уровня безопасности исключений: базовая гарантия, строгая гарантия и гарантия отсутствия исключений. Эта идея была тепло встречена сообществом С++ разработчиков, а после ее популяризации (и некоторой модификации) Гербом Саттером, гарантии безопасности исключений стали широко применяться в boost-е, в стандартной библиотеке С++, а также при разработке прикладных приложений.

Базовая гарантия

Исходное определение: “в случае возникновения исключений не должно быть никаких утечек ресурсов”.

Современное определение: “при возникновении любого исключения в некотором методе, состояние программы должно оставаться согласованным”. Это означает, не только отсутствие утечек ресурсов, но и сохранение инвариантов класса, что является более общим критерием, по сравнению с базовым определением.

Разница между этими двумя формулировками обусловлена тем, что изначально эта гарантия были предложена для реализации библиотеки на языке С++ и не имела никакого отношения к прикладным приложениями. Но если говорить о более общем случае (т.е. о приложении, а не только о библиотеке), то можно сказать, что утечки ресурсов является лишь одним из источников багов, но далеко не единственным. Сохранение инварианта в любой устойчивый момент времени (**) является залогом того, что никакой внешний код не сможет «увидеть» рассогласованного состояния приложения, что, согласитесь, не менее важно, чем отсутствие утечек ресурсов. Мало какого пользователя банковского приложения будут интересовать утечки памяти, если при переводе денег с одного счета на другой, деньги могут «уйти» с одного счета, но «не дойти» до другого.

Строгая гарантия

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

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

Гарантия отсутствия исключений

Гарантия отсутствия исключений сводится к следующему: “ни при каких обстоятельствах функция не будет генерировать исключения”.

Во-вторых, в некоторых случаях невозможно обеспечить нормальную работу других функций, если некоторые операции не будут следовать гарантии отсутствия исключений. Так, например, в языке С++, для обеспечения даже базовой гарантии исключений (а точнее утечки ресурсов) в контейнерах необходимо, чтобы деструктор пользовательского типа не генерировал исключения.

Рассмотренные выше три гарантии безопасности исключений идут от наиболее слабой к наиболее сильной; при этом каждая последующая гарантия является надмножеством предыдущей. Это означает, что выполнение строгой гарантии автоматом влечет за собой выполнение базовой гарантии, а гарантия отсутствия исключений влечет за собой выполнение строгой гарантии. Если же код не отвечает даже базовой гарантии исключений, то он является миной замедленного действия в вашем приложении и рано или поздно приведет к неприятным последствиям, развалив к чертям его состояние.

Теперь давайте рассмотрим несколько примеров.

Примеры нарушения базовой гарантии

Главным способом предотвращения утечек памяти и ресурсов в языке C++ является идиома RAII (Resource Acquisition Is Initialization), которая заключается в том, что объект захватывает ресурс в конструкторе и освобождает его в деструкторе. А поскольку вызов деструктора осуществляется автоматически при выходе объекта из области видимости по любой причине, в том числе и при возникновении исключения, то неудивительно, что эта же идиома используется и для обеспечения безопасности исключений.

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

// Некоторый класс, содержащий управляемые ресурсы
class DisposableA : IDisposable
public void Dispose() <>
>

// Еще один класс с управляемыми ресурсами
class DisposableB : IDisposable
public DisposableB()
disposableA = new DisposableA();
throw new Exception( "OOPS!" );
>

public void Dispose() <>

private DisposableA disposableA;
>

// Где-то в приложении
using ( var disposable = new DisposableB())
// Упс! Метод Dispose не будет вызван ни для
// DisposableB, ни для DisposableA
>

* This source code was highlighted with Source Code Highlighter .

Итак, у нас есть два disposable-класса: DisposableA и DisposableB, каждый из которых захватывает некоторый управляемый ресурс в конструкторе и освобождает его в методе Dispose. Давайте пока не будем рассматривать финализатор, поскольку он никак не поможет нам гарантировать детерминированный порядок освобождения ресурсов, что в некотором случае является жизненно важным.

Эта же проблема может проявляться и более тонким образом. В рассмотренном ранее случае, явно видно, что мы создали экземпляр disposable-объекта, после чего генерируется исключение. Но бывают случаи, когда отсутствие базовой гарантии исключений увидеть немного сложнее.

class Base : IDisposable
public Base()
// Захватываем некоторый ресурс
>
public void Dispose() <>
>

class Derived : Base, IDisposable
public Derived( object data)
if (data == null )
throw new ArgumentNullException( "data" );
// OOPS!!
>
>
// И снова где-то в приложении
using ( var derived = new Derived( null ))
<>

* This source code was highlighted with Source Code Highlighter .

Генерация исключения в конструкторе класса Derived нарушает базовую гарантию исключений и приводит к утечке ресурсов, поскольку метод Dispose класса Base не вызывается (****). Опять таки, поскольку компилятор знает об интерфейсе IDisposable только через призму конструкции using, то во всех случаях, когда disposable объект является полем другого класса, за вызов метода Dispose отвечает только программист.

Помимо базового класса, подобную же шутку может сыграть и инициализаторы полей, когда конструктор одного из полей может генерировать исключение:

class ComposedDisposable : IDisposable
public void Dispose() <>

private readonly DisposableA disposableA = new DisposableA();
// А что, если конструктор DisposableB упадет? OOPS!!
private readonly DisposableB disposableB = new DisposableB();
>

* This source code was highlighted with Source Code Highlighter .

Пример строгой гарантии исключений. Object initializer и collection initializer

Инициализатор объектов и коллекций (object initializer и collection initializer) обеспечивают атомарность создания и инициализации объекта или заполнения коллекции списком элементов. Давайте рассмотрим следующий пример.

class Person
public string FirstName < get ; set ; >
public string LastName < get ; set ; >
public int Age < get ; set ; >
>

var person = new Person
FirstName = "Bill" ,
LastName = "Gates" ,
Age = 55,
>;

* This source code was highlighted with Source Code Highlighter .

С первого взгляда может показаться, что это всего лишь синтаксический сахар для следующего:

var person = new Person();
person.FirstName = "Bill" ;
person.LastName = "Gates" ;
person.Age = 55;

* This source code was highlighted with Source Code Highlighter .

Однако на самом деле, при вызове инициализатора объекта создается временная переменная, затем изменяются свойства именно этой переменной, и только потом она присваивается новому объекту:

var tmpPerson = new Person();
tmpPerson.FirstName = "Bill" ;
tmpPerson.LastName = "Gates" ;
tmpPerson.Age = 55;
var person = tmpPerson;

* This source code was highlighted with Source Code Highlighter .

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

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

Заключение

Корректная обработка исключений дело не простое, и, как показали некоторые примеры, иногда даже базовая гарантия исключений дается с трудом. Однако обеспечение подобных гарантий является жизненно необходимым условием при разработке приложений, поскольку значительно легче всю сложность по работе ресурсами упрятать в одном месте, нежели размазать ее тонким слоем по всему приложению. Золотое правило, сформулированное десяток лет назад Скоттом Мейерсом все еще остается в силе: создавайте классы, которые легко использовать правильно и сложно использовать неправильно, и гарантия исключений в этом играет явно не последнюю роль.

Если говорить о практическом применении данных гарантий, то следует помнить несколько моментов. Во-первых, код, не выполняющий базовую гарантию исключений некорректен; на его основе просто невозможно создать приложение, чье состояние не будет разламываться при его использовании или изменении (*****). Во-вторых, не стоит параноить и добиваться максимальной гарантии. Добиться гарантии отсутствия исключений на 100% вообще практически невозможно из-за наличия асинхронных исключений, но даже реализация строгой гарантии во многих случаях может быть неоправданно дорогой.

В заключение можно сказать следующее: гарантии безопасности исключений – не панацея, но отличный фундамент для построения надежных приложений.

(**) В общем случае, никто не требует сохранение инварианта всегда; обычно требуется сохранение инварианта «до» и «после» вызова открытого метода, но совсем необязательно его сохранение после вызова закрытого метода, выполняющего лишь «часть» работы.

class Resource1
public :
Resource1()
// Захватываем некоторый ресурс, будь-то выделяем память
// в куче или создаем дескриптор ОС
>
~Resource1()
// Освобождаем захваченный ресурс
>
>;

class Resource2
public :
Resource2()
// В этой точке кода объект resource1_ уже проинициализирован
throw std::exception( "Yahoo!" );
>
private :
Resource1 resource1_;
>;


// где-то в приложении создаем экземпляр класса Resource2
Resource2 resource2;

* This source code was highlighted with Source Code Highlighter .

(*****) Все сказанное в этой статье относится только к синхронным исключениям, поскольку гарантировать согласованное возникновении «асинхронных» исключений, таких как OutOfMemoryException или ThreadAbortException практически невозможно. За пруфом сюда: «О вреде метода Thread.Abort».

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


Или хранить их по ссылке:


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

Начнём по-порядку. Пусть у нас есть некий класс, конструктор которого, в некоторых случаях, может вызывать исключение (нет файла, нет связи, не подошёл пароль, недостаточно прав для выполнения операции… что угодно). Наш класс будет предельно прост и предсказуем.


Казалось бы, ничего не забыли. (Хотя, строго говоря, конечно забыли как минимум конструктор копирования и операцию присвоения, которые бы корректно работали с нашими указателями; ну да ладно.)

Воспользуемся этим классом:


И разберёмся, что и когда будет конструиваться и уничтожаться.

  • Сперва запустится процесс создания объекта Cnt.
  • В нём будет создан объект *xa
  • Начнёт создание объекта *xb.
  • … и тут произойдёт исключение

Обратите внимание, что это одна из самых неприятных ситуаций, утечка возникает не всегда, а только при определённых аргументах (первый — не ноль, а второй — ноль). Отыскать такие утечки бывает очень сложно.

Очевидно, такое решение годится только для очень простеньких программок, которые в случае любого исключения просто беспомощно валятся и всё.

Какие же есть решения?

Самое простое, надёжное и естественное решение — хранить объект по значению


Это компактно, это элегантно, это естественно… но главное — это безопасно! В этом случае компилятор следит за всем происходящим, и (по-возможности) вычищает всё, что уже не понадобится.

Результат работы кода:


То есть объект Cnt::xa был автоматически корректно уничтожен.

Безумное решение с указателями

Настоящим кошмаром может стать вот такое решение:


Представляете, что будет, если появится Cnt::xc? А если придётся поменять порядок инициализации. Надо будет приложить не мало усилий, чтобы ничего не забыть, сопровождая такой код. И, что самое обидное, это вы сами для себя же разложили везде грабли.

Лирическое отступление про исключения.

Для чего были придуманы исключения? Для того, чтобы отделить описание нормального хода программы от описания реакции на какие-то сбои.

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

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

Это делает код запутанным и трудным для понимания и поддержки.

Решение настоящих индейцев — умные указатели

Если вам всё же необходимо хранить указатели, то вы всё равно можете обезопасить свой код, если сделаете для указателей обёртки. Их можно писать самим, а можно использовать множество, уже существующих. Пример использования auto_ptr:


Мы практически вернулись к решению с хранением членов класса по значению. Здесь мы храним по значению объекты класса auto_ptr, о своевременном удалении этих объектов опять заботится компилятор (обратите внимание, теперь нам не надо самостоятельно вызывать delete в деструкторе); а они, в свою очередь, хранят наши указатели на объекты X и заботятся о том, чтобы память вовремя освобождалась.

Да! И не забудьте подключить


Там описан шаблон auto_ptr.

Лирическое отступление про new

Одно из преимуществ C++ перед C состоит в том, что C++ позволяет работать со сложными структурами данных (объектами), как с обычными переменными. То есть C++ сам создаёт эти структуры и сам удаляет их. Программист может не задумываться об освобождении ресурсов до тех пор, пока он (программист) не начнёт сам создавать объекты. Как только вы написали «new», вы обязали себя написать «delete» везде, где это нужно. И это не только деструкторы. Больше того, вам скорее всего придётся самостоятельно реализовывать операцию копирования и операцию присвоения… Одним словом, вы отказались от услуг С++ и попали на весьма зыбкую почву.

Конечно, в реальной жизни часто приходится использовать «new». Это может быть связано со спецификой алгоритма, продиктовано требованиями по производительности или просто навязано чужими интерфейсами. Но если у вас есть выбор, то наверно стоит трижды подумать, прежде, чем написать слово «new».

Читайте также: