Правила программирования на С и С++. Главы 7-8 - Возбуждение исключений из конструктора ненадежно
ОГЛАВЛЕНИЕ
161. Возбуждение исключений из конструктора ненадежно.
Я начну этот раздел с замечания о том, что компиляторы, которые соответствуют рабочим документам комитета ISO/ANSI по С++, не имеют большей части из рассматриваемых здесь проблем. Тем не менее, многие компиляторы (один из которых компилятор Microsoft) им не соответствуют.
Ошибки в конструкторах являются действительной проблемой С++. Так как они не вызываются явно, то и не могут возвратить коды ошибок обычным путем. Задание для конструируемого объекта "неверного" значения в лучшем случае громоздко и иногда невозможно. Возбуждение исключения может быть здесь решением, но при этом нужно учесть множество вопросов. Рассмотрим следующий код:
class c
{
class error {};int *pi;
public: c() { throw error(); }// ...
};void f( void )
{
try{
c *cp = new c; // cp не инициализируется, если не выполняется// ... // конструктор,
delete cp; // эта строка в любом случае не выполнится.
}catch( c::error ?err )
{
printf ("Сбой конструктора\n");delete cp; // Дефект: cp содержит теперь мусор
}}Проблема состоит в том, что память, выделенная оператором new, никогда не освобождается. То есть, компилятор сначала выделяет память, затем вызывает конструктор, который возбуждает объект error. Затем управление передается прямо из конструктора в catch-блок. Код, которым возвращаемое значение оператора new присваивается cp, никогда не выполняется - управление просто перескакивает через него. Следовательно, отсутствует возможность освобождения памяти, потому что у вас нет соответствующего указателя. Чтение мной рабочих документов комитета ISO/ANSI по С++ показало, что такое поведение некорректно - память должна освобождаться неявно. Тем не менее, многие компиляторы делают это неправильно.
Вот простой способ исправить эту сложную ситуацию (я поместил тело функции в определение класса лишь для того, чтобы сделать пример покороче):
class с{
int *pi;public: c() { /*...*/ throw this; }};void f( void )
{
try{
c *cp = NULL;cp = new c;
c a_c_object();
}catch( c *points_at_unconstructed_object )
{
if( !cp ) // если конструктор, вызванный посредством 'new', не выполняется delete points_at_unconstructed_object;}}Ситуация усложняется, когда некоторые объекты размещаются при помощи new, а другие - из динамической памяти. Вы должны сделать что-то похожее на следующее, чтобы понять, в чем дело:void f( void )
{
c *cp = NULL; // cp должен быть объявлен снаружи try-блока, потому что // try-блок образует область действия, поэтому cp не может// быть доступным в catch-блоке будучи объявлен в try-блоке.
try{
c a_c_object;cp = new c;
}catch( c *points_at_unconstructed_object )
{
if( !cp ) // если конструктор, вызванный посредством 'new', не выполняется delete points_at_unconstructed_object;}}Вы не можете решить эту проблему внутри конструктора, потому что для конструктора нет возможности узнать, получена ли инициализируемая им память от new, или из стека.
Во всех предыдущих примерах деструктор для сбойных объектов вызывается, даже если конструктор не выполнился и возбудил исключение. (Он вызывается или косвенно посредством оператора delete, или неявно при выходе объекта из области действия, даже если он покидает ее из-за возбуждения исключения).
Аналогично, вызов delete косвенно вызывает деструктор для этого объекта. Я сейчас вернусь к этой ситуации. Перед выходом из этого деструктора незавершенный конструктор должен привести объект в исходное состояние перед тем, как сможет возбудить ошибку. С учетом предшествующего определения класса c следующий код будет работать при условии, что отсутствует ошибка до оператора new int[128] и new выполнен успешно:
c::c( )
{
if( some_error() ) throw error(this); // ДЕФЕКТ: pi неинициализирован.// ...pi = new int[128]; // ДЕФЕКТ: pi неинициализирован, если
// ... // оператор new возбуждает исключение.
if( some_other_error() )
{
delete [] pi; // Не забудьте сделать это.throw error(this); // Это возбуждение безопасно.
}}c::~c( )
{
delete pi;}Запомните, что pi содержит мусор до своей инициализации оператором new. Если возбуждается исключение до вызова new или сам оператор new возбудит исключение, то тогда pi никогда не инициализируется. (Вероятно, оно не будет содержать NULL, а будет просто неинициализированно). Когда вызывается деструктор, то оператору delete передается это неопределенное значение. Решим проблему, инициализировав этот указатель безопасным значением до того, как что-то испортится:
c::c( ) : pi(NULL) // инициализируется на случай, если оператор 'new' даст сбой
{
if( some_error() ) throw error(this); // Это возбуждение теперь безопасно.// ...pi = new int[128]; // Сбой оператора new теперь безопасен.
// ...
if( some_other_error() )
{
delete [] pi; // Не забудьте высвободить динамическую память.throw error(this); // Это возбуждение безопасно.
}}c::~c( )
{
if( pi ) delete pi;}Вы должны запомнить, что нужно освободить успешно выделенную память, если исключение возбуждается после операции выделения, так, как было сделано ранее.
У вас есть возможность почистить предложенный выше код при его использовании с учетом моего совета из предыдущего правила о возбуждении исключения объекта error и скрытия всех сложностей в этом объекте. Однако определение этого класса получается значительно более сложным. Реализация в листинге 16 опирается на том факт, что деструктор явно объявленного объекта должен вызываться при выходе из try-блока, перед выполнением catch-блока. Деструктор для объекта, полученного при помощи new, не будет вызван до тех пор, пока память не будет передана оператору delete, что происходит в сообщении destroy(), посланном из оператора catch. Следовательно, переменная has_been_destroyed будет означать истину, если объект получен не при помощи new и исключение возбуждено из конструктора, и ложь - если объект получен посредством new, потому что деструктор еще не вызван.
Конечно, вы можете вполне резонно заметить, что у меня нет причин проверять содержимое объекта, который по теории должен быть уничтожен. Здесь уже другая проблема. Некоторые компиляторы (в том числе компилятор Visual C++ 2.2 Microsoft) вызывают деструктор после выполнения оператора catch, даже если объекты, определенные в try-блоке, недоступны из catch-блока. Следовательно, код из листинга 16 не будет работать с этими компиляторами. Вероятно, лучшим решением состояло бы в написании варианта operator new(), который мог бы надежно указывать, получена память из кучи, или из стека.
Листинг 16. except.cpp - возбуждение исключения из конструктора
- class с
- {
- public:
- class error
- {
- c *p; // NULL при успешном выполнении конструктора
- public:
- error( c *p_this );
- void destroy( void );
- };
- private:
- unsigned has_been_destroyed : 1;
- int *pi;
- private: friend class error;
- int been_destroyed( void );
- public:
- c() ;
- ~c();
- };
- //=============================================================
- c::error::error( c *p_this )
- : p( p_this )
- {}
- //----------------------------------------------------------------------------------------------------------------
- void c::error::destroy( void )
- {
- if( p ?? !p->been_destroyed() )
- delete p;
- }
- //=============================================================
- c::c() : has_been_destroyed( 0 )
- {
- // ...
- throw error(this);
- // ...
- }
- //----------------------------------------------------------------------------------------------------------------
- c::~c()
- {
- // ...
- has_beeb_destroyed = 1;
- }
- //--------------------------------------------------------------
- int c::been_destroyed( void )
- {
- return has_been_destroyed;
- }
- //===============================================================
- void main( void )
- {
- try
- {
- c *cp = new c;
- c a_c_object;
- delete cp;
- }
- catch( c::error ?err )
- {
- err.destroy(); // деструктор вызывается, только если объект создан оператором new
- }
- }