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

       

Назначение исключений — не быть пойманными


Как правило, исключение должно быть возбуждено, если:

· Нет другого способа сообщить об ошибке (например, конструкторов, перегруженных операций и т.д.).

· Ошибка неисправимая (например, нехватка памяти).

· Ошибка настолько непонятная или неожиданная, что никому не придет в голову ее протестировать (например, printf).

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

some_obj x;

if( x.is_invalid() )

// конструктор не выполнился.

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

x = a + b;

функция operator+()

может сообщить об ошибке, является возврат неверного значения, которое будет скопировано в x. Вы могли бы затем написать:

if( x == INVALID )



// ...

или нечто подобное. Снова весьма неаккуратно. Исключения также полезны для обработки ошибок, которые обычно являются фатальными. Например, большинство программ просто вызовут exit(), если функция malloc() не выполнится. Все проверки типа:

if( !(p = malloc(size)) )

   fatal_error( E_NO_MEMORY );

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

Также имеется и другая проблема. Одной из причин того, что комитет ISO/ANSI по Си++ требует, чтобы оператор new

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


значение NULL. По причинам, обсуждаемым позже, я не думаю, что исключение должно быть использовано вместо возврата ошибки просто для защиты программистов от себя самих, но оно срабатывает с new, потому что эта ошибка обычно в любом случае неисправима. Лучшим примером может быть функция printf(). Большинство программистов на Си даже не знают, что printf()
возвращает код ошибки. (Она возвращает количество выведенных символов, которое может быть равно 0, если на диске нет места). Программисты, которые не знают о возврате ошибки, склонны ее игнорировать. А это не очень хорошо для программы, которая осуществляет запись в перенаправленный стандартный вывод, продолжать, как будто все в порядке, поэтому можно считать хорошей идеей возбудить здесь исключение.
Итак, что же плохого в исключениях? На самом деле существует две проблемы. Первой является читаемость. Вам будет тяжело меня убедить, что:
some_class obj;
try
{
   obj.f();
}
catch( some_class::error r )
{
   // выполнить действие в случае ошибки
}
лучше читается, чем:
if( obj.f() == ERROR )
// выполнить действие в случае ошибки
В любом случае, если try-блок содержит более одного вызова функций, вы не сможете просто исправить ошибку, потому что вы не сможете узнать, где возникла ошибка.
Следующий пример демонстрирует вторую проблему. Класс CFile, реализующий основной ввод/вывод двоичных файлов, возбуждает исключение в случае переполнения диска при записи, чего легко добиться на дискете. Более того, функция write() не возвращает никакого кода ошибки. Перехват исключения является единственным способом обнаружения ошибки. Вот пример того, как вы должны обнаруживать ошибку чтения:
char  data[128];
Cfile f( "some_file", CFile::modeWrite );
try
{
   f.Write( data, sizeof(data) );
}
catch( CFileException r )
{
   if( r.m_cause == CfileException::diskFull )
   // что-то сделать
}
Имеется две проблемы. Первая явно связана с уродливостью этого кода. Я бы гораздо охотнее написал:


bytes_written = f.Write( data, sizeof(data));
if( bytes_written != sizeof(data) )
   // разобраться с этим
Вторая проблема одновременно более тонкая и более серьезная. Вы не сможете исправить эту ошибку. Во-первых, вы не знаете, сколько байтов было записано перед тем, как диск переполнился. Если Write()
возвратила это число, то вы можете предложить пользователю сменить диск, удалить несколько ненужных файлов или сделать еще что-нибудь для освобождения места на диске. Вы не можете тут сделать это, потому что не знаете, какая часть буфера уже записана, поэтому вы не знаете, откуда начинать запись на новый диск.
Даже когда Write()
возвратила количество записанных байтов, то вы все еще не можете исправить ошибку. Например, даже если функцию CFile
переписать, как показано ниже, то она все равно не будет работать:
char  data[128];
CFile f( "some_file", CFile::modeWrite );
int bytes_written;
try
{
   bytes_written = f.Write( data, sizeof(data) );
}
catch( CFileException r )
{
   if( r.m_cause == CFileException::diskFull )
   // что-то выполнить.
   // при этом переменная bytes_written содержит мусор.
}
Управление передается прямо откуда-то изнутри Write() в обработчик catch при возбуждении исключения, перескакивая через все операторы return внутри Write(), а также через оператор присваивания в вызывающейся функции; переменная bytes_written
остается неинициализированной. Я думаю, что вы могли бы передать Write()
указатель на переменную, которую она могла использовать для хранения числа записанных байтов перед тем, как выбросить исключение, но это не будет значительным улучшением. Лучшим решением будет отказ от возбуждения исключения и возврат или числа записанных байтов, или какого-то эквивалента индикатора ошибки.
Последней проблемой являются непроизводительные затраты. Обработка исключения вызывает очень большие непроизводительные затраты, выражающиеся в возрастании в несколько раз размера кода и времени выполнения. Это происходит даже в операционных системах типа Microsoft Windows NT, которые поддерживают обработку исключений на уровне операционной системы. Вы можете рассчитывать на 10-20% увеличение размера кода и падение скорости выполнения на несколько процентов при интенсивном использовании исключений.14
Следовательно, исключения должны использоваться лишь тогда, когда непроизводительные затраты не берутся в расчет;
обычно, при наличии возможности, предпочесть возврат ошибки.

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