71. Проектируйте и пишите безопасный в отношении ошибок код

71. Проектируйте и пишите безопасный в отношении ошибок код

Резюме

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

Убедитесь, что при любых ошибках ваша программа всегда остается в корректном состоянии (в этом и заключается базовая гарантия). Остерегайтесь ошибок, нарушающих инвариант (включая утечки, но не ограничиваясь ими).

Желательно дополнительно гарантировать, что конечное состояние либо является исходным состоянием (в результате отката после происшедшей ошибки), либо корректно вычисленным целевым состоянием (если ошибок не было). Это — строгая гарантия безопасности.

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

Обсуждение

Базовая, строгая гарантии и гарантия бессбойности (известная также как гарантия отсутствия исключений) впервые были описаны в [Abrahams96] и получили широкую известность благодаря публикациям [GotW], [Stroustrup00, §E.2] и [Sutter00], посвященным вопросам безопасности исключений. Эти гарантии применимы к обработке любых ошибок, независимо от конкретного использованного метода, так что мы можем воспользоваться ими при описании безопасности обработки ошибок в общем случае. Гарантия бессбойности является строгим подмножеством строгой гарантии, а строгая гарантия, в свою очередь, является строгим подмножеством базовой гарантии.

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

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

Однако в большинстве функций могут произойти сбои. Если ошибка возможна, наиболее безопасным будет гарантировать транзакционное поведение функции: либо функция выполняется успешно и программа переходит из начального корректного состояния в корректное целевое состояние, либо — в случае сбоя — программа остается в том же состоянии, в котором находилась перед вызовом функции, т.е. видимые состояния всех объектов после сбойного вызова оказываются теми же, что и до него (например, значение глобальной целой переменной не может измениться с 42 на 43), и любое действие, которое вызывающий код мог предпринять до сбойного вызова, должно остаться возможным (с тем же смыслом) и после сбойного вызова (например, ни один итератор контейнера не должен стать недействительным; применение оператора ++ к упомянутой глобальной целой переменной даст значение 43, а не 44). Такая гарантия безопасности называется строгой.

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

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

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

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

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

Примеры

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

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

Пример 3. std::vector::insert. Поскольку внутреннее представление vector<T> использует непрерывный блок памяти, вставка элемента в средину требует перемещения ряда имеющихся значений на одну позицию для освобождения места для вставляемого элемента. Перемещение выполняется с использованием копирующего конструктора T::T(const T&) и оператора присваивания T::operator=, и если одна из этих операций может сбоить (генерировать исключение), то единственный способ обеспечить строгую гарантию — это сделать полную копию контейнера, выполнить операцию над копией, а затем обменять оригинал и копию с использованием бессбойной функции vector<T>::swap.

Однако выполнение всех указанных действий обходится излишне дорого как в плане требуемой памяти, так и процессорного времени. Таким образом, обеспечение строгой гарантии даже тогда, когда она не является необходимой, оказывается чрезмерно расточительным. Поэтому строгую гарантию вызывающий код должен при необходимости обеспечивать самостоятельно — для этого шаблон vector предоставляет все необходимые инструменты. (В лучшем случае, если тип содержимого контейнера не генерирует исключений в копирующем конструкторе и копирующем операторе присваивания, никаких дополнительных действий не требуется. В худшем следует создать копию вектора, выполнить вставку в копию и после успешного завершения данной операции обменять копию и исходный вектор.)

Пример 4. Запуск спутника. Рассмотрим функцию f, в которой частью ее работы является запуск спутника, и используемую ею функцию LaunchSatellite, обеспечивающую гарантию не ниже строгой. Если функция f может выполнить всю работу, при которой может произойти сбой, до запуска спутника, то f способна обеспечить строгую гарантию. Но если f должна выполнить некоторые операции, в процессе которых может произойти сбой, уже после запуска спутника, то обеспечение строгой гарантии оказывается невозможным — вернуть запущенный спутник на стартовую площадку уже нельзя. (Такую функцию f следует разделить по крайней мере на две, поскольку одна функция не должна даже пытаться выполнить несколько различных действий такой важности; см. рекомендацию 5.)

Ссылки

[Abrahams96] • [Abrahams01b] • [Alexandrescu03d] • [Josuttis99] §5.11.2 • [Stroustrup00] §14.4.3, §E.2-4, §E.6 • [Sutter00] §8-19, §40-41, §47 • [Sutter02] §17-23 • [Sutter04] §11-13 • [Sutter04b]