Правила программирования на Си и Си++

Возбуждение исключений из конструктора ненадежно


Я начну этот раздел с замечания о том, что компиляторы, которые соответствуют рабочим документам комитета 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, потому что деструктор еще не вызван.
Конечно, вы можете вполне резонно заметить, что у меня нет причин проверять содержимое объекта, который по теории должен быть уничтожен. Здесь уже другая проблема. Некоторые компиляторы (в том числе компилятор Microsoft Visual C++ 2.2) вызывают деструктор после выполнения оператора 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. Он проводит семинары по приглашению частных фирм повсюду на территории США и преподает в филиалах Калифорнийского университета, расположенных в Беркли и Санта-Круз. Он также работает программистом и консультантом по объектно-ориентированному проектированию, используя Си и Си++ в операционных средах Microsoft Windows, Windows-95, Windows NT и UNIX.
М-р Голуб регулярно пишет для различных компьютерных журналов, включая "Microsoft Systems Journal", "Windows Tech Journal" и изредка "BYTE". Его популярная колонка "Сундучок с Си", публиковавшаяся в "Dr.Dobb's Journal" с 1983 по 1987 годы, стала для многих людей первым введением в Си. В число его книг входят "Compiler Design in C", "C+C++" и "The C Companion". М-р Голуб сочиняет музыку и имеет лицензию частного пилота.


Вы можете связаться с ним через Интернет по адресу allen@holub.com или через его фирму Software Engineering Consultants, P.O.Box 5679, Berkeley, CA 94705 (телефон и факс: (510) 540-7954).
§
Буч Г. Объектно–ориентированный анализ и проектирование с примерами приложений на С++, 2–е изд./Пер. с англ.—М.; СПб.: "Издательство БИНОМ" — "Невский диалект", 1998.—560 с.—Прим. перев.
§
Уже не редкость емкость дисковой памяти, превышающая спустя 5 лет указанные автором значения на два порядка, а оперативной — на порядок. — Прим.перев.
[1]
Web описана в книге Дональда Кнута ""The WEB System of Structured Documentation" (Palo Alto: Stanford University Dept. of Computer Science, Report No.STAN-CS-83-980, 1983). Система CWeb описана в книге Дональда Е. Кнута и Сильвио Ливая "The CWeb System of Structured Documentation" (Reading: Addison Wesley, 1994). Обе публикации не только описывают как работают эти системы, но хорошо демонстрируют это. В этих книгах документируются реальные тексты программ, реализующих указанные системы.
TEX является редакционно-издательской системой Кнута. Она имеется в нескольких коммерческих версиях.
§
"До каких же пор ты, Катилина, будешь испытывать наше терпение..." — начало известной речи Цицерона. — Прим. перев.
[2] Я подозреваю, что венгерская запись так интенсивно используется вследствие того, что большая часть Microsoft Windows написана на языке ассемблера.
[3] По крайней мере, оно должно быть. Я подозреваю, что некоторые энтузиасты венгерской записи так плохо организуют свои программы, что просто не могут найти нужные объявления. Включая тип в имя, они избавляются от многих часов поисков в неудачно спроектированных листингах. Программы на языке ассемблера, которые по необходимости включают в себя множество глобальных переменных, являются очевидным исключением.
§
В августе 1998 г. стандарт ратифицирован в виде "ISO/IEC 14882, Standard for the C++ Programming Language". Популярно


изложен в книге: Страуструп Б. Язык программирования С++, 3–е изд. /Пер. с англ.—СПб.; М.: "Невский диалект" — "Издательство БИНОМ", 1999.–991 с. — Прим. перев.
§
В стандарте ISO/IEC 14882 существует тип ‘bool’. Имеет смысл заменить тип переменной is_left_child
на bool. — Ред.
§
Решение о переводе некоторых из идентификаторов, по меньшей мере, спорное. Однако, если вы не знаете английского, то будете лишены возможности оценить юмор автора, которым он оживил большую часть своих примеров. — Ред.
§
Переменная, объявленная в операторе for, не выживает после этого оператора. — Ред.
§
Кроме того, стандарт пересмотрел подход к жизни переменных, объявленных в операторе for. — Ред.
§
С этим утверждением автора, так и следующим за ним примером инкрементирования аргумента макроса нельзя согласиться. — Ред.
§
Комментарий в языке Си должен быть заключен в /*  */. — Ред.
Чтобы быть строго корректным, по крайней мере на языке выражений Си++, я должен называть поле "компонентом данных-членов". Однако довольно неудобно говорить "компонент данных-членов name",
поэтому буду использовать просто "поле", когда его значение ясно из контекста.
Они не передаются. Даже в Smalltalk есть только один объект, который или получает сообщение, или нет. Несмотря на это, интерпретаторы Smalltalk склоняются к реализации обработки сообщений при помощи нескольких таблиц указателей на функции, по одной на каждый класс. Если интерпретатор не может найти обработчик сообщения в таблице диспетчеризации производного класса, то он просматривает таблицу базового класса. Этот механизм не используется в Си++, который является компилируемым языком и поэтому не использует многоуровневый просмотр таблиц в время выполнения. Например, даже если бы все функции в базовом классе были виртуальными, то таблица виртуальных функций производного класса имела бы по ячейке для каждой виртуальной функции базового класса. Среда времени выполнения Си++ не просматривает иерархию таблиц, а просто использует таблицу для текущего объекта. Подробнее об этом позднее.


Эта цитата является отрывком из статьи, размещенной Страуструпом в телеконференции BIX в декабре 1992 г. Полностью статья опубликована в книге Мартина Хеллера "Advanced Win32 Programming"(New York: Wiley,1993), pp.72-78.
Не путайте этот процесс с объединением. У mother нет поля parent, скорее та часть mother, которая определена на уровне базового класса, изображается как "компонент parent".
На самом деле правильнее сказать, что во время компиляции компилятор не знает, от какого из базовых классов parent
объект child
наследует обработчик сообщения go_to_sleep(), хотя эта правильность и может сбить с толку. Вы можете спросить, почему неопределенность имеет значение, ведь эта функция одна и та же в обоих классах. Компилятор не может создать ветвление времени выполнения, так как не знает, какое значение присвоить указателю this, когда он вызывает функцию-член базового класса.
Пользователи MFC могут обратиться за более глубоким обсуждением этого вопроса к моей статье "Rewriting the MFC Scribble Program Using an Object-Oriented Design Approach" в августовском номере журнала "Microsoft Systems Journal" за 1995 г.
§
Утверждение автора не соответствует стандарту языка. — Ред.
§ В соответствии со стандартом должно быть int main ( void ). — Ред.

Книга Эллис и Страуструпа "The Annotated C++ Reference Manual" (Reading: Addison Wesley, 1990), использованная в качестве базового документа комитетом ISO/ANSI по Си++§.
§Имеется перевод на русский язык под редакцией А.Гутмана "Справочное руководство по языку программирования Си++ с комментариями"
(М.: Мир, 1992). —Прим.перев.

Конечно, конструкторы копий и функции operator=(), создаваемые вами (в отличие от компилятора), никогда не вызывают своих двойников из базового класса автоматически.
§
Стандартом языка для этого предусмотрено ключевое слово explicit. — Ред.

Некоторые компиляторы в действительности позволяют выполнить явный вызов конструктора, поэтому вы, вероятно, сможете сделать точно так же:
const some_class operator=( const some_class r )
{
   if( this != r )
   {
      this-~some_class();
      this-some_class::some_class( r );
   }
}
Тем не менее, такое поведение является нестандартным.
§
Функции из шаблонов генерируется, только если они используются в программе (по крайней мере, так должен поступать хороший компилятор). — Ред.
  В действительности я бы использовал множественное наследование с участием класса string. Использованный здесь код имеет цель немного упростить пример.
§
См. предыдущее примечание к правилу 156. — Ред.
  Я определил это для 32-разрядного компилятора Visual C++ Microsoft; другие компиляторы показывают или сравнимые результаты, или худшие.

Содержание раздела