пятница, 5 августа 2011 г.

Использование исключений

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

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

Теперь давайте я приведу несколько примеров, которые меня бесят. Все совпадения с реальным кодом - реальны, имена только изменены, и код немного подчищен...
type Client::receive()
{
struct special_case { };
type data;

try {
if (...) {
throw runtime_error("...");
}
... skipped many throws ...
if (...) {
throw special_case();
}
} catch (exception &) {
data = type();
} catch (special_case &) {
throw runtime_error("...");
}
return data;
}

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

Очень костыльно выглядит особый случай, который вполне легко решается одним throw, когда мы убираем все "goto"...
string Informer::get()
{
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < 0) {
throw runtime_error("...");
}
... skip more code with throws ...

Конечно, мы же на C++ пишем, зачем мы будем связываться с кодами ошибок и тд...
С кодами ошибок связываться нет необходимости, проблема не в этом. Проблема в том, что функция приватная. В публичной функции мы видим следующее:
try {
string r = get(components[i].comp);
... do something ...
} catch (const runtime_error &e) {
}


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

Следующий пример больше не по генерации, а по перехвату исключений. Так уж сложилось, что наш интерфейс базы данных бросает исключения, если выборка пуста. С этим ничего не поделаешь, приходится мириться, хотя много кода, похожего на этот:
try {
scoped_ptr<Set> cursor(db->all_records());
... do something ...
} catch (EmptySet &) {
// ничего не надо делать
}

Костыльно конечно, но что делать..
И тут я увидел это:
void convert_records(shared_ptr<RecordSet> &in, list &records) {
do {
try {
shared_ptr<RecordSet> cur_rec((*in)->get_records());
convert_records(cur_rec, records);
} catch (EmptySet) {
Record rec;
convert_record(**in, rec);
records.push_back(rec);
}
} while (!in->next());
}

Смысл в том, что там добавилась древовидность записей, одна запись получила возможность ссылаться на другие. И записи отныне делятся на листовые и групповые.

Я сперва не мог сообразить, почему две одинаковые функции используют совершенно разные параметры, буква s в конце имени функции очень плохо видна.. а за такую перегрузку - вообще бы убил.

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

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

Вот после этого я и подумал - наверное в Google все таки не дураки сидят, что запретили исключения?