Исключения C++: достоинства и недостатки

ОГЛАВЛЕНИЕ

Объективный анализ хороших и плохих доводов в пользу использования исключений по сравнению с кодами ошибок.

Оглавление

  1. Введение
  2. Доводы в пользу использования исключений
    1. Исключения отделяют код обработки ошибок от нормального алгоритма программы, тем самым повышая разборчивость, надежность и расширяемость кода.
    2. Генерация исключения – единственный чистый способ сообщить об ошибке из конструктора.
    3. Исключения трудно игнорировать, в отличие от кодов ошибок.
    4. Исключения легко передаются из глубоко вложенных функций.
    5. Исключения могут быть, и часто являются, определяемыми пользователем типами, несущими гораздо больше информации, чем код ошибки.
    6. Объекты исключений сопоставляются с обработчиками с помощью системы типов.
  3. Доводы против использования исключений
    1. Исключения нарушают структуру кода, создавая множество скрытых точек выхода, что затрудняет чтение и изучение кода.
    2. Исключения легко вызывают утечки ресурсов, особенно в языке, не имеющем встроенного сборщика мусора и блоков finally(в конце).
    3. Тяжело научиться писать безопасный код исключений.
    4. Исключения дорогостоящие и нарушают обещание платить лишь за используемое.
    5. Исключения тяжело ввести в устаревший код.
    6. Исключения неверно используются для выполнения задач, относящихся к нормальному алгоритму программы.
  4. Вывод

1. Введение

Исключения входят в состав C++ с начала 1990-х и одобрены стандартом как механизм для написания ошибкоустойчивого кода на данном языке. Однако многие разработчики по разным причинам решают не использовать исключения, и многие сомневаются в эффективности данной возможности языка: статья Рэймонда Чена « Чище, более элегантно, и ошибочно», блог Джоела Сполски «Исключения и Руководство Google по стилю C++», и  другие часто цитируемые тексты не советуют использовать исключения.

Не принимая ничью сторону в данном споре, представим гармоничную оценку достоинств и недостатков использования исключений. Цель данной статьи – не убедить читателей использовать исключения или коды ошибок, а помочь им принять обоснованное решение, наилучшее для их конкретного проекта. Статья построена в виде списка из шести доводов в пользу использования исключений и шести доводов против них, часто приводимых в сообществе C++.


2. Доводы в пользу использования исключений

2.1 Исключения отделяют код обработки ошибок от нормального алгоритма программы, тем самым повышая разборчивость, надежность и расширяемость кода.

Чтобы проиллюстрировать довод, сравним использование двух простых библиотек сокетов, различающихся лишь механизмом обработки ошибок. Ниже показан способ их использования для извлечения html из веб-сайта:

// пример 1: функция, использующая исключения

string get_html(const char* url, int port)
{
    Socket client(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    client.connect(url, port);
   
    stringstream request_stream;
    request_stream << "GET / HTTP/1.1\r\nHost: " << url << "\r\nConnection: Close\r\n\r\n";

    client.send(request_stream.str());

    return client.receive();
}

Теперь изучим версию, использующую коды ошибок:

// пример 2: функция, использующая коды ошибок

Socket::Err_code get_html(const char* url, int port, string* result)
{
    Socket client;
    Socket::Err_code err = client.init(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (err) return err;
       
    err = client.connect(url, port);
    if (err) return err;
   
    stringstream request_stream;
    request_stream << "GET / HTTP/1.1\r\nHost: " << url << "\r\nConnection: Close\r\n\r\n";

    err = client.send(request_stream.str());
    if (err) return err;

    return client.receive(result);
}

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

Легко заметить, что пример с исключениями имеет более чистый и простой алгоритм, не прерываемый ни одним ветвлением if. Обработка ошибок полностью скрыта, и виден лишь "нормальный" алгоритм кода. Инфраструктура для распространения исключений генерируется компилятором: в случае исключения стек будет раскручиваться правильно, то есть локальные переменные во всех кадрах стека будут разрушены правильно – включая выполнение деструкторов.

Кстати, показанный здесь пример 2 даже проще и чище, чем был бы в большинстве случаев: используется лишь одна библиотека внутри функции, и возвращается код ошибки этой библиотеки. На практике пришлось бы учитывать разные типы кодов ошибок, обнаруживающиеся внутри функции, а затем пришлось бы сопоставлять все эти коды ошибок с типом, возвращаемым из функции.

Как насчет ошибкоустойчивости? При использовании исключений компилятор создает код для "пути ошибки", и программист не делает это вручную, что означает меньшую вероятность совершить ошибки. Это особенно важно при изменении кода – легко забыть обновить часть обработки ошибок при внесении изменения в нормальный путь кода или совершить ошибку при внесении изменений.

2.2 Генерация исключения – единственный чистый способ сообщить об ошибке из конструктора.

Назначение конструктора - создать инвариант класса. Чтобы сделать это, ему часто требуется запрашивать ресурсы системы или вообще выполнять операцию, способную отказать. Если конструктор не сумеет создать инвариант, объект находится в неправильном состоянии, и вызывающая функция должна быть извещена.

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

Какие имеются альтернативы генерации исключения из отказавшего конструктора? Одна из самых популярных – идиома "двухэтапного создания", использованная в примере 2. Процесс создания объекта разбит на два этапа: конструктор выполняет только часть инициализации, не способную отказать (т.е. установка членов простых типов), а часть, способная отказать, выделяется в отдельную функцию, обычно имеющую такое имя, как init() или create(), и возвращающую код ошибки. Создание инварианта класса в данном случае требует не только создания объекта, но и вызова другой функции и проверки возвращенного кода ошибки. Недостатки такого подхода очевидны: правильная инициализация объекта требует больших усилий, и легко получить объект в неправильном состоянии, не зная об этом. Более того, копирующий конструктор нельзя реализовать таким образом – нет способа приказать компилятору вставить второй этап и проверить код ошибки. С другой стороны, эта идиома вполне эффективно используется во многих библиотеках, и при определенной тренировке она будет успешной.

Еще одной альтернативой генерации исключения из конструктора является сохранение "флага плохого состояния" в виде переменной члена, установка этого флага в конструкторе и предоставление функции для проверки флага. Стандартные потоки ввода/вывода используют этот подход:

// пример 3: использование флага плохого состояния

ifstream fs("somefile.txt");
if (fs.bad())
    return err_could_not_open;

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

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

2.3 Исключения трудно игнорировать, в отличие от кодов ошибок.

Чтобы проиллюстрировать этот довод, уберем проверку ошибок из примера 2 – он нормально скомпилируется и при условии отсутствия ошибок при выполнении будет работать столь же успешно. Однако представьте, что была ошибка в вызове init(): объект client будет в неправильном состоянии, и при вызове его функций-членов может произойти практически что угодно в зависимости от  внутренней реализации класса Socket, операционной системы, и т.д.; программа может сразу дать сбой, или она даже может выполнить все функции, но ничего не сделать и возвратиться без признаков произошедшей ошибки – за исключением результата. Зато, если исключение было сгенерировано из конструктора, неправильный объект вообще не был бы создан, и выполнение продолжилось бы в обработчике исключения. Обычная фраза такова: "мы бы получили исключение прямо нам в лицо".

Но действительно ли так трудно проигнорировать исключение? Поднимемся по стеку и рассмотрим вызывающий оператор get_html():

// пример 4: "Подавление исключений"

try {
    string html = get_html(url);
}
catch (...)
{}

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

Однако даже если их легко игнорировать, исключения проще обнаружить, чем коды ошибок. На многих платформах можно прерваться при генерации исключения, если процесс запущен из отладчика. Например, GNU gdb поддерживает "точки захвата" для этой цели, а отладчики Windows поддерживают опцию "прерваться, если сгенерировано исключение". Гораздо труднее, если вообще возможно, получить аналогичную функциональность с помощью кодов ошибок.

2.4 Исключения легко передаются из глубоко вложенных функций.

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

Вернемся к примеру 1 с классом сокетов. Предположим, get_html() вызывается функцией get_title(), вызываемой обработчиком события пользовательского интерфейса. Нечто вроде:

// пример 5: функция, вызывающая get_html() примера 1

string get_title(const string& url)
{
    string markup = get_html(url);

    HtmlParser parser(markup);
   
    return parser.get_title();
}

Исключения обрабатываются на уровне обработчика события пользовательского интерфейса:

// пример 6: обработчик исключений

void AppDialog::on_button()
{
    try {
        string url = url_text_control.get_text();
        result_pane.set_text(
            get_title(url);
    }
    catch(Socket::Exception& sock_exc) {
        display_network_error_message();
    }
    catch(Parser::Exception& pars_exc) {
        display_parser_errorMessage();
    }
    catch(...) {
        display_unknown_error_message();
    }
}

В примере выше get_title() не содержит код для передачи информации об ошибке из get_html() в on_button(). Если бы использовались коды ошибок вместо исключений, get_title() пришлось бы проверять возвращаемое значение после вызова get_html(), сопоставлять это значение с его собственным кодом ошибки и передавать новый код ошибки обратно в on_button(). Использование исключений заставило бы get_title() выглядеть подобно примеру 1, а коды ошибок превратили бы его в нечто, похожее на пример 2. Нечто вроде:

// пример 7: get_title с кодами ошибок

enum Get_Title_Err {Get_Title_Err_OK, Get_Title_Err_NetworkError, Get_Title_Err_ParserError};
Get_Title_Err get_title(const string& url, string* title)
{
    string markup;
    Socket::Err_code sock_err = get_html(url.c_str(), &markup);
    if (sock_err) return Get_Title_Err_NetworkError;

    HtmlParser parser;
    HtmlParser::Err_code parser_err = parser.init(markup);
    if (parser_err) return Get_Title_Err_ParserError;
   
    return parser.get_title();
}

Как и пример 2, пример 7 запросто может еще больше усложниться, если попытаться передавать больше специфичных кодов ошибок. В таком случае ветвлениям if пришлось бы сопоставлять коды ошибок из библиотек с соответствующими значениями Get_Title_Err. Также заметьте, что показана лишь часть вложенной функции в этом примере – легко представить работу, требуемую для передачи кода ошибки из глубин кода парсера в функцию get_title.

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

Код ошибки обычно является целым типом и может содержать мало информации об ошибке. Тип HRESULT Microsoft является весьма впечатляющей попыткой упаковать как можно больше информации в 32-битное целое, но она явно показывает ограничения такого подхода. Конечно, можно создать код ошибки, являющийся полноценным объектом, но затраты на многократное копирование такого объекта, до того как он доберется до обработчика ошибок, делают данный способ непригодным.

Зато исключения обычно являются объектами и могут содержать много полезной информации об ошибке. Исключение обычно содержит информацию о файле исходного кода и строке, в которой оно было сгенерировано (с помощью таких макросов, как _FILE_ и _LINE_), и исключение может автоматически отправлять сообщения системе протоколирования.

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

2.6 Объекты исключений сопоставляются с обработчиками с помощью системы типов.

Немного дополним пример 6 и выведем специальное сообщение об ошибке, если ошибка произойдет при установлении соединения сокета:

// пример 8: обработчик исключения

void AppDialog::on_button()
{
    try {
        string url = url_text_control.get_text();
        result_pane.set_text(
            get_title(url));
    }
    catch(Socket::SocketConnectionException& sock_conn_exc) {
        display_network_connection_error_message();
    }
    catch(Socket::Exception& sock_exc) {
        display_general_network_error_message();
    }
    catch(Parser::Exception& pars_exc) {
        display_parser_errorMessage();
    }
    catch(...) {
        display_unknown_error_message();
    }
}

Пример 8 показывает использование системы типов для классификации исключений. Обработчики идут от самых специфичных к самому общему исключению, и это выражается с помощью подходящего механизма языка: наследование. В данном примере Socket::SocketConnectionException унаследован от Socket::Exception.

Если бы использовались коды ошибок вместо исключений, обработчик ошибок был бы оператором switch(выбора), и каждый отдельный case(вариант) обрабатывал бы значение для кода ошибки. default(по умолчанию) соответствовал бы catch(...). Это выполнимо, но не тождественно. В примере 8 используется обработчик для Socket::Exception, чтобы обрабатывать все исключения из библиотеки сокетов, кроме SocketConnectionException, обрабатываемого прямо над ней; в случае кодов ошибок пришлось бы явно перечислять все прочие коды ошибок. Если забыть один код ошибки или добавить новый код ошибки позже, но забыть обновить список кодов ошибок... вы понимаете, в чем дело.


3. Доводы против использования исключений

3.1 Исключения нарушают структуру кода, создавая множество скрытых точек выхода, что затрудняет чтение и изучение кода.

Это звучит прямо противоположно тому, что говорилось в 2.1. Как исключения могут делать код одновременно трудночитаемым и легкочитаемым? Чтобы понять это мнение, вспомните, что путем использования исключений нам удалось отобразить только "нормальный ход выполнения". Код обработки ошибок все же генерируется компилятором и имеет свой собственный ход выполнения, независимый от хода выполнения написанного нами кода. По сути, нормальный ход выполнения кода более чистый, разборчивый и понятный с исключениями, но это лишь часть вопроса. Теперь нам надо знать, что в любой вызов функции мы вводим невидимое ветвление, которое сразу же существует в случае исключения (или переходы\а к обработчику, если он находится в той же функции). Это выглядит хуже, чем goto(переход) и сравнимо с longjmp, но на деле исключения C++ гораздо безопасней использовать, чем longjmp: как было показано в разделе 2.1, компилятор генерирует код, раскручивающий стек.

Все же проблема с разборчивостью не обязательно ложная: если бегло взглянуть на кусок кода, чтобы понять, что он может делать, нужен лишь нормальный ход выполнения. Но что если надо тщательно разобраться в куске кода? Например, представьте, что вам надо проанализировать код или расширить функцию – предпочли бы вы иметь все возможные ветви кода прямо перед глазами или быть вынужденным гадать, какая функция может сгенерировать исключение и при каких обстоятельствах? Используйте исключения только обработки ошибок и изначально пишите код с учетом защищённости от исключений.

3.2 Исключения легко вызывают утечки ресурсов, особенно в языке, не имеющем встроенного сборщика мусора и блоков finally(в конце).

Чтобы понять, о чем здесь говорится, пока забудем объектно-ориентированное программирование с его классами, деструкторами и исключениями и вернемся к старому доброму C. Многие функции принимают следующий вид:

// пример 9: получение и освобождение ресурсов

void func()
{
    acquire_resource_1();
    acquire_resource_2();

    use_resources_1();
    use_resources_2();
    use_resources_3();

    release_resource_2();
    release_resource_1();
}

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

Так, где проблема? Функция из примера 9 - образец хорошо структурированного кода с одним входом и одним выходом, и ресурсы всегда освобождаются в конце функции. Вообразите, что нечто может дать сбой в use_resources_1(). В этом случае мы не выполняем остальные функции use_resources_..., а сообщаем об ошибке и сразу завершаем выполнение. Ну, не сразу – сначала надо освободить ресурсы. Как лучше всего это сделать с помощью языка C? Данная тема вызывает более горячие споры, чем дилемма "исключения по сравнению с кодами ошибок": некоторые люди копируют и вставляют код освобождения ресурсов и вызывают его всегда при завершении выполнения; другие создают макрос для этой цели по возможности; есть разработчики, сохраняющие структуру "SESE" функции и вводящие ветвления if-else для каждой функции, способной отказать. Такие функции похожи на закрывающую угловую скобку ">", и иногда это называют "стреловидный антишаблон". Многие разработчики C используют goto для перехода к части функции, освобождающей ресурсы.

В любом случае, это беспорядок даже без исключений. Что если мы переключимся на C++, и use_resources_1() сгенерирует исключение в случае ошибки? При такой манере написания функции часть освобождения ресурсов не выполнится, и произойдет утечка. Как бы здесь помогли сборщик мусора и блоки finally? Притворимся, что Java имеет поддержку работы с сетью в стандартной библиотеке, и что мы используем нечто похожее на пример 1 на Java:

// пример 10: версия примера 1 на Java
public String getHtml(String url, int port) throws (Socket.Exception)
{
    Socket client = null;
    try {
        client = new Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        client.connect(url, port);

        StringBuffer requestStream = new StringBuffer("GET / HTTP/1.1\r\nHost: ");
        requestStream.append(url).append("\r\nConnection: Close\r\n\r\n");

        client.send(requestStream.toString());

        return client.receive();
    }
    finally {
        if (client != null)
            client.close();
    }
}

В методе getHtml выше было получено несколько ресурсов: сокет, строковый буфер и пара временных строковых объектов. Сборщик мусора следит за объектами, которые в итоге должны освободить только память системы, и сокет закрывается в блоке finally, выполняющемся при выходе из блока try(попытка) – нормальном или через исключение.

C++ не имеет встроенного сборщика мусора и не поддерживает блоки finally; почему же функция из примера 1 (или примера 2 в связи с этим) не дает утечку? Ответ обычно называется RAII (получение ресурса является инициализацией) и означает, что объекты, объявленные как локальные переменные, разрушаются после выхода за пределы области видимости, независимо от того, как они выходят из области видимости. Когда они разрушаются, сначала выполняются деструкторы, а затем память возвращается системе. Деструкторы освобождают все ресурсы, полученные объектами – по сути, деструктор класса Socket, использованного в примере 1, похож на тело блока finally в примере 10. Плюс в том, что в C++ приходится писать код освобождения ресурсов лишь единожды, и он выполняется автоматически всякий раз, когда объект выходит за пределы области видимости. Еще один плюс RAII в том, что он обращается со всеми ресурсами одинаково – нет необходимости различать собираемые сборщиком мусора ресурсы и неуправляемые ресурсы, оставляя первые сборщику мусора и освобождая вторые в блоках finally: освобождение ресурсов скрыто, но надежно осуществляется, сродни оповещению об ошибках с помощью исключений.

Отсутствие сборщика мусора и блоков finally – не повод не использовать исключения в C++, поскольку RAII – лучший метод управления ресурсами? Да и нет. RAII лучше, чем комбинация сборщика мусора и блоков finally, и является очень простой и легкой в использовании идиомой. Но чтобы получить преимущества RAII, его надо использовать последовательно. Существует поразительное количество кода на C++, выглядящего так:

// пример 11: пример кода, не защищенного от исключений

string get_html(const char* url, int port)
{
    Socket client = new Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    client->connect(url, port);
   
    stringstream request_stream;
    request_stream << "GET / HTTP/1.1\r\nHost: " << url << "\r\nConnection: Close\r\n\r\n";

    client->send(request_stream.str());

    string html = client->receive();

    delete client;
    return html;
}

В примере 11 RAII используется для всех ресурсов, кроме сокета, но этот код наверняка даст сбой: если исключение генерируется где-нибудь в функции, происходит утечка сокета. Если кто-то отключает сетевой кабель – программа может быстро отказать. Можно обернуть функцию в блок try-catch(попытка-перехват) и затем закрыть сокет в блок catch и после него – тем самым смоделировав несуществующую конструкцию finally, но такая однообразная и трудоемкая задача когда-нибудь забывается, и получается серьезная ошибка.

Но насколько распространен такой вид кода? Создание объектов в стеке при наличии возможности не только безопасней, но и легче, чем спаривание new и delete – можно было бы ожидать, что большинство программистов использует RAII всегда. К сожалению, это не так. Есть много кода, выглядящего как пример 11. Например, найдите официальный пример кода для парсера SAX2 популярной библиотеки Xerces-C – он создает объекты в куче и удаляет их после того, как они использованы – в случае исключения есть утечка. В начале 1990-х считалось "более объектно-ориентированным" создавать объект в куче, даже если не было оснований так поступать. Сейчас многие молодые программисты на C++ сначала изучают Java в университетах и пишут код на C++ в Java-подобной манере, а не на идиоматическом C++. Так или иначе, существует много кода, не полагающегося на определенные деструкторы в автоматическом освобождении ресурсов. Если вы имеете дело с такой базой кода, используйте коды ошибок и выключите исключения.

Предотвращение утечек ресурсов – лишь часть задачи защиты от исключений. Иногда функция должна быть транзакционной: она должна достигать цели или не менять состояние. В такой ситуации надо отменять операцию в случае отказа перед выходом из функции. Неудивительно, что для запуска такой операции при генерации исключения применяются деструкторы. Данная идиома называется "защита области видимости".

3.3 Тяжело научиться писать безопасный код исключений.

Обычное возражение на данное заявление заключается в том, что обработка ошибок вообще трудная, и исключения ее облегчают, а не затрудняют. Хотя это во многом верно, остается проблема кривой обучения: сложные средства языка часто помогают опытным программистам и упрощают их код, но новички чаще всего видят дополнительные трудности по изучению. Чтобы использовать исключения в коде готового программного продукта, программист C++ должен не только изучить механизм языка, но и лучшие способы программирования с исключениями. Как быстро новичок научится вообще не генерировать исключение из деструктора? Или использовать исключения только для исключительных условий, и никогда для нормального хода выполнения кода? Или перехватывать по ссылке, а не по значению или указателю? Как насчет полезности (или ее отсутствия) спецификаций исключений? Гарантий защиты от исключений? Темные углы, такие как полиморфная генерация исключений?

С позиции "стакан наполовину полон", с исключениями или без них, C++ всегда был языком для специалистов, обеспечивающим мало поддержки новичкам. Вместо того, чтобы полагаться на ограничения языка, хороший программист C++ обращается к сообществу, чтобы оно научило его лучшим способам использования языка. Указания сообщества меняются быстрее, чем сам язык: некто, перешедший с C++ на Java в 1997 году, часто теряется, сталкиваясь с современным кодом C++ - он выглядит иначе, но сам язык не сильно изменился. Вернемся к исключениям. Сообщество C++ узнало много о том, как эффективно их использовать, с тех пор как они были впервые введены в язык; некто, читающий новые публикации и группы новостей, должен научиться лучшим способам с определенными усилиями. Хороший стандарт кодирования, отвечающий рекомендациям сообщества, очень помогает с кривой обучения.

3.4 Исключения дорогостоящие и нарушают обещание платить лишь за используемое.

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

Чтобы понять влияние обработки исключений на производительность в целом, настоятельно рекомендуется прочитать главу 5.4 Технического отчета по производительности C++. В нем довольно подробно разобраны источники потерь производительности: код, добавленный в блоки try-catch компилятором, влияние на регулярные функции и затраты на фактическую генерацию исключения. Он также сравнивает два самых распространенных подхода к реализации: подход "код" –  когда код добавляется в каждый блок try-catch; и подход "таблица" – когда компилятор генерирует статические таблицы.

После ознакомления с вариантами влияния исключений на производительность в целом изучите тему для вашей конкретной платформы и компилятора. Очень хорошую и относительно новую презентацию, созданную Кевином Фреем из Microsoft, можно найти в Северо-западной группе пользователей C++. Она охватывает компилятор Visual C++ для 32- и 64- битной платформы Windows. Другой очень интересный текст, касающийся компилятора GNU C++ на Itanium, - Itanium C++ ABI(двоичный интерфейс приложений): обработка исключений.

Текущие реализации C++ не дают гарантий предсказуемости для исключений, что делает их непригодными для систем жёсткого реального времени.

3.5 Исключения тяжело ввести в устаревший код.

Защита от исключений никогда не должна быть запоздалой мыслью. Если код написан без учета исключений, лучше вообще не добавлять в него исключения или полностью переписать код – слишком многое может разладиться. Это напоминает превращение кода в поточно-ориентированный, если изначально он был написан без учета безопасности потоков – просто не делайте этого! Не только тяжело проверить код на наличие потенциальных проблем, но что более важно, его тяжело тестировать. Как вы собираетесь запускать все возможные сценарии, приводящие к генерации исключений?

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

3.6 Исключения неверно используются для выполнения задач, относящихся к нормальному алгоритму программы.

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

Почти всегда использование исключений для воздействия на "нормальный" ход выполнения – плохая идея. Как уже было сказано в разделе 3.1, исключения генерируют невидимые ветви кода. Эти ветви кода, вероятно, допустимы, если они выполняются только в сценариях обработки ошибок. Но если исключения применяются для любой другой цели, "нормальное" выполнение кода делится на видимую и невидимую часть, что делает код трудночитаемым, плохо понятным и плохо расширяемым.

Даже если согласиться, что исключения должны использоваться только для обработки ошибок, порой неясно, представляет ли собой  ветвь кода сценарий обработки ошибок или нормальный ход выполнения. Например, если std::map::find() дает в результате «значение не найдено» -  это ошибка или нормальный ход выполнения? Иногда легче найти ответ, если мыслить в категориях "исключительного", а не "ошибочного" состояния. В данном примере является ли исключительным случаем то, что значение не найдено в карте? Вероятно, ответ "нет" – есть много "нормальных" сценариев, в которых проверяется, хранится ли значение в карте, и ветвление основано на результате – ни одно из этих ветвлений не является ошибочным состоянием. Оттого map::find не генерирует исключение, если в карте нет запрошенного ключа.

Как насчет функциональности, зависящей от ввода данных пользователем? Если пользователь вводит неверные данные, лучше генерировать исключение или нет? Введенные пользователем данные лучше считать неверными, если не доказано обратное, и желательно иметь функцию, проверяющую правильность введенных данных перед их обработкой. Для функции проверки правильности неверные данные являются ожидаемым результатом, поэтому не нужно генерировать исключение. Проблема в том, что часто невозможно проверить правильность введенных данных до их фактической обработки (например, парсеры); в таких случаях использование исключений для прерывания обработки и сообщения об ошибке часто является хорошим подходом. HtmlParser из примера 5 генерирует исключение, если не может разобрать неверный HTML.

Никогда нельзя генерировать исключение, если программа работает в нормальном режиме и с допустимыми данными. То есть при тестировании программы путем запуска ее из отладчика обязательно установите "точку перехвата", срабатывающую всегда, когда генерируется исключение. Если выполнение программы прерывается из-за генерации исключения, это сигнализирует об ошибку, которую надо устранить, или о неправильном использовании механизма обработки исключений, что также надо устранить. Конечно, исключение может сгенерироваться из-за ненормальных условий окружения (например, сетевое подключение внезапно рвется, или выделение дескриптора шрифта дает сбой) или неверных введенных данных, и в этом случае исключение генерируется обоснованно.

4. Вывод

Нет простого ответа на вопрос "исключения или коды ошибок". Решение должно приниматься исходя из конкретной ситуации, с которой сталкивается группа разработчиков. Примерные рекомендации следующие:
•    Если у вас есть хороший процесс разработки и стандарты написания кода, которые действительно соблюдаются, если вы пишете код C++ в современном стиле, основанном на освобождении ресурсов с помощью RAII, если ваша база кода модульная –  желательно использовать исключения.
•    Если вы работаете с кодом, написанным без учета защиты от исключений, если в вашей группе разработчиков отсутствует порядок, или если вы разрабатываете системы жёсткого реального времени –  не стоит использовать исключения.