CHAP8_3 (1018811), страница 4
Текст из файла (страница 4)
Листинг 15 показывает простую систему определений класса для возбуждения исключений. Я могу перехватить ошибки чтения или записи подобным образом:
try
{
file f("name", "rw");
buffer b;
b = f.read() f.write( b );
}
catch( file::open_error &r )
{
// Файл не существует или не может быть открыт.
}
catch( file::io_error &r )
{
// Какая-то из неисправимых ошибок ввода/вывода.
}
Если меня волнует лишь то, что произошла ошибка определенного вида, и не волнует, какого конкретно, то я могу сделать так:
file f;
try
{
buffer b;
b = f.read()
f.write( b );
}
catch( file::error &r )
{
// ...
}
Листинг 15. Классы исключений
-
class file
-
{
-
public:
-
class error {};
-
class open_error : public error {};
-
class io_error : public error {};
-
// ...
-
}
Этот код работает, потому что объект file::read_error является объектом типа file::error (так как относится к производному классу). Вы всегда можете перехватить объект производного класса, используя ссылку или указатель базового класса.
Я мог бы также предложить другой класс, использующий тот же самый механизм:
class long_double
{
public:
class error {};
class didvide_by_zero : public error {};
// ...
};
Так как классы error являются вложенными определениями, то именами на самом деле являются file::error и long_double::error, поэтому здесь нет конфликта имен.
Для упрощения сопровождения я всегда использую error в качестве своего базового класса для исключений. (Я не мог использовать производный класс, даже если здесь был бы возможен всего один вид ошибки). Таким образом, я знаю, что имея возбуждающий исключение класс some_class, можно перехватить это исключение при помощи:
catch(some_class::error &r)
Эту ошибку искать не придется. Если применяется наследование, то я использую базовый класс error таким образом:
class employee
{
public:
class error {};
class database_access_error : public error {};
};
class peon : public employee
{
class error : public employee::error {};
class aagh : public error {};
};
Этим способом исключение aagh может быть перехвачено как peon::aagh, peon::error или employee::error.
Нет смысла создавать класс глобального уровня error, от которого наследуются все локальные классы error, потому что для обработки этой ситуации вы можете использовать обработчик catch(...).
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
-
}
-
}
Заключение
Так-что вот. Множество правил, которые я считаю полезными и которые, надеюсь, будут полезны и для вас. Конечно, многие из представленных здесь правил дискуссионны. Пожалуйста, я готов с вами о них поспорить. Несомненно, я не считаю себя каким-то законодателем в стиле С++ и сам нарушаю многие из этих правил при случае, но я искренне верю, что следование этим правилам сделает меня лучшим программистом, и надеюсь, что вы их тоже оцените.
Я закончу вопросом. Сколько времени потребуется программисту на С++ для того, чтобы заменить электрическую лампочку? Ответ - нисколько, а вы, кажется, все еще мыслите процедурно. Правильно спроектированный класс электрическая_лампа должен наследовать метод замены от базового класса лампа. Просто создайте объект производного класса и пошлите ему сообщение заменить_себя.
Об авторе
Ален Голуб - программист, консультант и преподаватель, специализирующийся на С++, объектно-ориентированном проектировании и операционных системах Microsoft. Он проводит семинары по приглашению частных фирм повсюду на территории США и преподает в филиалах Калифорнийского университета, расположенных в Беркли и Санта-Круз. Он также работает программистом и консультантом по объектно-ориентированному проектированию, используя С и С++ в операционных средах Micrisoft Windows, Windows 95, Windows NT и UNIX.















