Практическое руководство. Проектирование безопасности исключений

Одним из преимуществ механизма исключения является то, что выполнение вместе с данными об исключении переходит непосредственно из инструкции, которая вызывает исключение в первую инструкцию catch, которая обрабатывает его. Обработчик может быть любым количеством уровней в стеке вызовов. Функции, вызываемые между оператором try и оператором throw, не требуются для того, чтобы знать ничего о создаваемом исключении. Тем не менее, они должны быть разработаны таким образом, чтобы они могли выйти из область "неожиданно" в любой момент, когда исключение может распространяться снизу, и сделать это, не оставляя за частично созданными объектами, утечкой памяти или структурами данных, которые находятся в неиспользуемых состояниях.

Основные методы

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

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

Сохранение простых классов ресурсов

При инкапсуле ручного управления ресурсами в классах используйте класс, который ничего не делает, кроме управления одним ресурсом. Сохраняя класс простым, вы снижаете риск возникновения утечек ресурсов. Используйте интеллектуальные указатели , если это возможно, как показано в следующем примере. Этот пример намеренно искусственный и упрощенный, чтобы выделить различия при shared_ptr использовании.

// old-style new/delete version
class NDResourceClass {
private:
    int*   m_p;
    float* m_q;
public:
    NDResourceClass() : m_p(0), m_q(0) {
        m_p = new int;
        m_q = new float;
    }

    ~NDResourceClass() {
        delete m_p;
        delete m_q;
    }
    // Potential leak! When a constructor emits an exception,
    // the destructor will not be invoked.
};

// shared_ptr version
#include <memory>

using namespace std;

class SPResourceClass {
private:
    shared_ptr<int> m_p;
    shared_ptr<float> m_q;
public:
    SPResourceClass() : m_p(new int), m_q(new float) { }
    // Implicitly defined dtor is OK for these members,
    // shared_ptr will clean up and avoid leaks regardless.
};

// A more powerful case for shared_ptr

class Shape {
    // ...
};

class Circle : public Shape {
    // ...
};

class Triangle : public Shape {
    // ...
};

class SPShapeResourceClass {
private:
    shared_ptr<Shape> m_p;
    shared_ptr<Shape> m_q;
public:
    SPShapeResourceClass() : m_p(new Circle), m_q(new Triangle) { }
};

Используйте идиом RAII для управления ресурсами

Для обеспечения безопасности исключений функция должна гарантировать, что объекты, выделенные с помощью или malloc уничтоженные, и все ресурсы, такие как дескриптор файлов, закрыты или new освобождены даже при возникновении исключения. Приобретение ресурсов — инициализация (RAII) связывает управление такими ресурсами с сроком существования автоматических переменных. Когда функция выходит из область, возвращается обычно или из-за исключения, вызываются деструкторы для всех полностью созданных автоматических переменных. Объект оболочки RAII, например смарт-указатель, вызывает соответствующую функцию удаления или закрытия в деструкторе. В защищенном от исключений коде важно немедленно передать владение каждым ресурсом в какой-то объект RAII. Обратите внимание, что vectorклассы , и stringmake_sharedfstreamаналогичные классы обрабатывают получение ресурса. Однако и традиционные shared_ptr конструкции являются особыми, unique_ptr так как приобретение ресурсов выполняется пользователем, а не объектом, поэтому они считаются освобождением ресурса является уничтожением, но сомнительными как RAII.

Три гарантии исключения

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

Гарантия без сбоя

Гарантия no-fail (или "no-throw") является самой сильной гарантией того, что функция может предоставить. Оно указывает, что функция не будет вызывать исключение или разрешать распространение. Однако вы не можете надежно предоставить такую гарантию, если (a) вы не знаете, что все функции, вызываемые этой функцией, также не являются неудачными, или (b) вы знаете, что все исключения, которые возникают, прежде чем они достигают этой функции, или (c) вы знаете, как перехватывать и правильно обрабатывать все исключения, которые могут достичь этой функции.

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

Надежная гарантия

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

Базовая гарантия

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

Классы, безопасные для исключений

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

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

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

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

  • Рассмотрите, следует ли хранить все состояния класса в элементе данных, который упакован в умный указатель, особенно если класс имеет концепцию "инициализации, которая разрешена сбоем". Хотя C++ позволяет неинициализировать элементы данных, он не поддерживает неинициализированные или частично инициализированные экземпляры классов. Конструктор должен либо завершиться успешно, либо завершиться сбоем; Объект не создается, если конструктор не выполняется до завершения.

  • Не позволяйте исключениям экранироваться от деструктора. Основной аксиомой C++ является то, что деструкторы никогда не должны разрешать исключение распространяться по стеку вызовов. Если деструктор должен выполнить потенциально исключение, он должен сделать это в блоке try catch и проглотить исключение. Стандартная библиотека обеспечивает эту гарантию для всех деструкторов, которые он определяет.

См. также

Современные рекомендации по C++ по исключению и обработке ошибок
Практическое руководство. Интерфейс между кодом с исключениями и без исключений