Герб Саттер - Стандарты программирования на С++. 101 правило и рекомендация
Добавим пару слов об определении предусловий функций. Условие является предусловием функции f тогда и только тогда, когда имеются основания ожидать, что весь вызывающий код проверяет выполнение данного условия перед вызовом функции f. Например, было бы неверно полагать предусловием нечто, что может быть проверено только путем выполнения существенной работы самой функцией, либо путем доступа к закрытой информации. Такая работа должна выполняться в функции и не дублироваться вызывающим ее кодом.
Например, функция, которая получает объект string, содержащий имя файла, обычно не должна делать условие существования файла предусловием, поскольку вызывающий код не в состоянии надежно гарантировать, что данный файл существует, не используя блокировки файла (при проверке существования файла без блокировки другой пользователь или процесс могут удалить или переименовать этот файл в промежутке между проверкой существования файла вызывающим кодом и попыткой открытия вызываемым кодом). Единственный корректный способ сделать существование файла предусловием — это потребовать, чтобы вызывающий код открыл его, а параметром функции сделать ifstream или его эквивалент (что к тому же безопаснее, поскольку работа при этом выполняется на более высоком уровне абстракции; см. рекомендацию 63), а не простое имя файла в виде объекта string. Многие предусловия таким образом могут быть заменены более строгим типизированием, которое превратит ошибки времени выполнения в ошибки времени компиляции (см. рекомендацию 14).
ПримерыПример 1. std::string::insert (ошибка предусловия). При попытке вставить новый символ в объект string в определенной позиции pos, вызывающий код должен проверить корректность значения pos, которое не должно нарушать документированные требования к данному параметру; например, чтобы не выполнялось соотношение pos > size(). Функция insert не может успешно выполнить свою работу, если для нее не будут созданы корректные начальные условия.
Пример 2. std::string::append (ошибка постусловия). При добавлении символа к объекту string сбой при выделении нового буфера, если заполнен существующий, не позволит функции выполнить документированные действия и получить документированные же постусловия, так что такой сбой является ошибкой.
Пример 3. Невозможность получения возвращаемого значения (ошибка постусловия). Получение корректного возвращаемого объекта является постусловием для функции, которая возвращает значение. Если возвращаемое значение не может быть корректно создано (например, если функция возвращает double, но значение double с требуемыми математическими свойствами не существует), то это является ошибкой.
Пример 4. std::string::find_first_of (не ошибка в контексте string). При поиске символа в объекте string, невозможность найти искомый символ — вполне законный итог поиска, ошибкой не являющийся. Как минимум, это не ошибка при работе с классом string общего назначения. Если владелец данной строки предполагает, что символ должен наличествовать в строке, и его отсутствие, таким образом, является ошибкой в соответствии с высокоуровневым инвариантом, то высокоуровневый вызываемый код должен соответствующим образом уведомить об ошибке инварианта.
Пример 5. Различные условия ошибок в одной функции. Несмотря на увеличивающуюся надежность дисковых носителей, запись на диск традиционно сопровождается ожиданием ошибок. Если вы разрабатываете класс File, в одной-единственной функции Filе::Write(const char*buffer, size_t size), которая требует, чтобы файл был открыт для записи, а указатель buffer имел ненулевое значение, вы можете предпринимать следующие действия.
• Если buffer равен NULL, сообщить об ошибке нарушения предусловия.
• Если файл открыт только для чтения: сообщить об ошибке нарушения предусловия.
• Если запись выполнена неуспешно: сообщить об ошибке нарушения постусловия, поскольку функция не в состоянии выполнить свою работу.
Пример 6. Различный статус одного и того же условия. Одно и то же условие может быть корректным предусловием для одной функции и не быть таковым для другой. Выбор зависит от автора функции, который определяет семантику интерфейса. В частности, std::vector предоставляет два пути для выполнения индексированного доступа: оператор operator[], который не выполняет проверок выхода за пределы диапазона, и функцию at, которая такую проверку выполняет. И оператор operator[], и функция at требуют выполнения предусловия, состоящего в том, что аргумент не должен выходить за пределы диапазона. Поскольку от оператора operator[] не требуется проверка его аргумента, должно быть четко документировано, что вызывающий код отвечает за то, чтобы аргумент оператора находился в допустимом диапазоне значений; понятно, что данная функция небезопасна. Функция же at в той же ситуации вполне безопасна, поскольку документировано, что она проверяет принадлежность своего аргумента к допустимому диапазону значений, и если аргумент выходит за пределы допустимого диапазона значений, то она сообщает об ошибке (путем генерации исключения std::out_of_range).
Ссылки[Abrahams01b] • [Meyer00] • [Stroustrup00] §8.3.3, §14.1, §14.5 • [Sutter04b]
71. Проектируйте и пишите безопасный в отношении ошибок код
В каждой функции обеспечивайте наиболее строгую гарантию безопасности, какой только можно добиться без дополнительных затрат со стороны вызывающего кода, не требующего такого уровня гарантии. Всегда обеспечивайте, как минимум, базовую гарантию безопасности.
Убедитесь, что при любых ошибках ваша программа всегда остается в корректном состоянии (в этом и заключается базовая гарантия). Остерегайтесь ошибок, нарушающих инвариант (включая утечки, но не ограничиваясь ими).
Желательно дополнительно гарантировать, что конечное состояние либо является исходным состоянием (в результате отката после происшедшей ошибки), либо корректно вычисленным целевым состоянием (если ошибок не было). Это — строгая гарантия безопасности.
Еще лучше гарантировать, что сбой в процессе операции невозможен. Хотя для большинства функций это невозможно, такую гарантию следует обеспечить для таких функций, как деструкторы и функции освобождения ресурсов. Данная гарантия — гарантия бессбойности.
ОбсуждениеБазовая, строгая гарантии и гарантия бессбойности (известная также как гарантия отсутствия исключений) впервые были описаны в [Abrahams96] и получили широкую известность благодаря публикациям [GotW], [Stroustrup00, §E.2] и [Sutter00], посвященным вопросам безопасности исключений. Эти гарантии применимы к обработке любых ошибок, независимо от конкретного использованного метода, так что мы можем воспользоваться ими при описании безопасности обработки ошибок в общем случае. Гарантия бессбойности является строгим подмножеством строгой гарантии, а строгая гарантия, в свою очередь, является строгим подмножеством базовой гарантии.
В общем случае каждая функция должна обеспечивать наиболее строгую гарантию, которую она в состоянии обеспечить без излишних затрат для вызывающего кода, которому не требуется такая степень гарантии. Там, где это возможно, следует дополнительно обеспечить достаточную функциональность для того, чтобы требующий более строгую гарантию код мог получить ее (см. в примерах к данной рекомендации случай vector::insert).
В идеале следует писать функции, которые всегда успешно выполняются и, таким образом, могут обеспечить гарантию бессбойности. Некоторые функции должны всегда обеспечивать такую гарантию, в частности, деструкторы, функции освобождения ресурсов и функции обмена (см. рекомендацию 51).
Однако в большинстве функций могут произойти сбои. Если ошибка возможна, наиболее безопасным будет гарантировать транзакционное поведение функции: либо функция выполняется успешно и программа переходит из начального корректного состояния в корректное целевое состояние, либо — в случае сбоя — программа остается в том же состоянии, в котором находилась перед вызовом функции, т.е. видимые состояния всех объектов после сбойного вызова оказываются теми же, что и до него (например, значение глобальной целой переменной не может измениться с 42 на 43), и любое действие, которое вызывающий код мог предпринять до сбойного вызова, должно остаться возможным (с тем же смыслом) и после сбойного вызова (например, ни один итератор контейнера не должен стать недействительным; применение оператора ++ к упомянутой глобальной целой переменной даст значение 43, а не 44). Такая гарантия безопасности называется строгой.