20.9 – Спецификации исключений и noexcept
В C++ все функции классифицируются как не выбрасывающие исключения (не генерируют исключения) или потенциально выбрасывающие исключения (могут генерировать исключения).
Рассмотрим следующее объявление функции:
int doSomething(); // может ли эта функция выбросить исключение или нет?
Глядя на типовое объявление функции, невозможно определить, может ли функция выбросить исключение или нет. Хотя комментарии могут помочь определить, генерирует ли функция исключения или нет (и если да, то какие исключения), документация может устареть, а компилятор не использует комментарии.
Спецификации исключений – это языковой механизм, который изначально был разработан как часть спецификации функций для документирования того, какие исключения может вызывать функция. Хотя большая часть спецификаций исключений теперь устарела или удалена, в качестве замены была добавлена другая полезная спецификация исключений, которую мы и рассмотрим в этом уроке.
Спецификатор noexcept
Спецификатор noexcept
определяет функцию как не вызывающую исключения и используется в объявлении функции справа от списка параметров:
void doSomething() noexcept; // эта функция не выбрасывает исключения
Обратите внимание, что noexcept
на самом деле не мешает функции генерировать исключения или вызывать другие функции, которые потенциально могут генерировать исключения. Скорее, когда генерируется исключение, если оно выходит из функции noexcept
, будет вызываться std::terminate
. И обратите внимание, что если std::terminate
вызывается из функции noexcept
, раскручивание стека может произойти, а может и не произойти (в зависимости от реализации и оптимизации), что означает, что перед завершением программы ваши объекты могут быть, а могут и не быть разрушены корректным образом.
Как и функции, отличающиеся только своими возвращаемыми значениями, функции, отличающиеся только спецификацией исключения, так же не могут быть перегружены.
Спецификатор noexcept
с логическим параметром
У спецификатора noexcept
есть необязательный логический параметр. noexcept(true)
эквивалентно noexcept
, что означает, что функция не выбрасывает исключения. noexcept(false)
означает, что функция потенциально может выбрасывать исключения. Эти параметры обычно используются только в шаблонах функций, где на основе некоторого параметризованного значения шаблонная функция может быть динамически создана как не генерирующая или как потенциально генерирующая исключения.
Какие функции не выбрасывают исключения, а какие потенциально выбрасывают исключения
Функции, которые по умолчанию не выбрасывают исключения:
- конструкторы по умолчанию;
- конструкторы копирования;
- конструкторы перемещения;
- деструкторы;
- операторы присваивания копированием;
- операторы присваивания перемещением.
Однако если какая-либо из перечисленных функций вызывает (явно или неявно) другую функцию, которая потенциально выбрасывает исключения, то указанная функция также будет рассматриваться как потенциально выбрасывающая исключения. Например, если в классе есть член данных с конструктором, потенциально выбрасывающим исключения, то конструкторы данного класса также будут рассматриваться как потенциально выбрасывающие исключения. В качестве другого примера, если оператор присваивания копированием вызывает оператор присваивания, потенциально выбрасывающий исключения, то этот оператор присваивания копированием также будет считаться бросать потенциально выбрасывающим исключения.
Лучшая практика
Если вы хотите, чтобы какие-либо из перечисленных выше функций были не выбрасывающими исключения, явно пометьте их как noexcept
(даже если они заданы таким образом по умолчанию), чтобы они случайно не стали потенциально выбрасывающими исключения.
По умолчанию потенциально выбрасывающими исключения могут быть:
- обычные функции;
- пользовательские конструкторы;
- некоторые операторы, например,
new
.
Оператор noexcept
Оператор noexcept
может использоваться внутри функций. Он принимает выражение в качестве аргумента и возвращает true
или false
, если компилятор считает, что аргумент вызовет исключение или нет. Оператор noexcept
проверяется статически во время компиляции и на самом деле не вычисляет входное выражение.
void foo() {throw -1;}
void boo() {};
void goo() noexcept {};
struct S{};
// true; int'ы не выбрасывают исключения
constexpr bool b1{ noexcept(5 + 3) };
// false; foo() выбрасывает исключение
constexpr bool b2{ noexcept(foo()) };
// false; boo() неявно объявлена noexcept(false)
constexpr bool b3{ noexcept(boo()) };
// true; goo() явно объявлена noexcept(true)
constexpr bool b4{ noexcept(goo()) };
// true; конструктор по умолчанию struct - по умолчанию noexcept
constexpr bool b5{ noexcept(S{}) };
Оператор noexcept
может использоваться для условного выполнения кода в зависимости от того, является ли его аргумент потенциально выбрасывающим исключения или нет. Это необходимо для выполнения определенных гарантий безопасности исключений, о которых мы поговорим в следующем разделе.
Гарантии безопасности исключений
Гарантия безопасности исключений – это договоренность о том, как функции или классы будут вести себя в случае возникновения исключения. Существует четыре уровня безопасности исключений:
- Нет гарантии – нет никаких гарантий относительно того, что произойдет, если будет выброшено исключение (например, объект класса может остаться в непригодном для использования состоянии).
- Базовая гарантия – при возникновении исключения утечка памяти не происходит, и объект по-прежнему можно использовать, но программа может быть оставлена в измененном состоянии.
- Строгая гарантия – если возникнет исключение, утечка памяти не произойдет и состояние программы не изменится. Это означает, что функция должна либо полностью завершиться успешно, либо не иметь побочных эффектов в случае сбоя. Это может быть просто, если сбой произошел до того, как что-либо было изменено; но также это может быть достигнуто путем отката любых изменений, чтобы программа вернулась в состояние до сбоя.
- Гарантия отсутствия выбросов исключений / сбоев – функция всегда завершается успешно (без сбоя) или со сбоем, но без выброса исключения.
Давайте рассмотрим гарантию без выбросов исключений / без сбоев более подробно:
Гарантия отсутствия выбросов исключений: если функция дает сбой, она не вызывает исключения. Вместо этого она вернет код ошибки или проигнорирует проблему. Гарантии отсутствия выбросов исключений требуются во время раскручивания стека, когда исключение уже обрабатывается; например, все деструкторы должны иметь гарантию отсутствия выбросов исключений (как и любые функции, вызываемые этими деструкторами). Примеры кода, который должен быть не выбрасывающим исключения:
- деструкторы и функции освобождения/очистки памяти;
- функции, которые должны вызываться функциями более высокого уровня, не выбрасывающими исключения.
Гарантия отсутствия сбоев: функция всегда будет успешной в том, что она пытается сделать (и, таким образом, никогда не будет необходимости генерировать исключение, поэтому отсутствие сбоев – это немного более строгая форма отсутствия выбросов исключений). Примеры кода, который должен быть без сбоев:
- конструкторы перемещения и присваивание перемещением (семантика перемещения, описана в главе M);
- функции обмена (swap-функции);
- функции очистки/стирания/сброса контейнеров;
- операции с
std::unique_ptr
(также рассматриваются в главе M); - функции, которые должны вызываться функциями более высокого уровня, не дающими сбоев.
Когда использовать noexcept
Тот факт, что ваш код не генерирует явно никаких исключений, не означает, что вы должны начать использовать noexcept
везде, где только можно. По умолчанию большинство функций потенциально выбрасывают исключения, поэтому, если ваша функция вызывает другие функции, есть большая вероятность, что она вызовет функцию, которая потенциально выбрасывает исключения, и, следовательно, так же станет потенциально выбрасывающей исключения.
Политика стандартной библиотеки заключается в использовании noexcept
только для функций, которые не должны выбрасывать исключения или давать сбой. Функции, которые потенциально могут генерировать исключения, но на самом деле не генерируют исключения (из-за реализации), обычно не помечаются как noexcept
.
Лучшая практика
Используйте спецификатор noexcept
в случаях, когда хотите показать гарантию отсутствия сбоев и выбросов исключений.
Лучшая практика
Если вы не уверены, должна ли функция иметь гарантию отсутствия сбоев / выбросов исключений, не помечайте ее как noexcept
. Отмена решения об использовании noexcept
нарушает обязательства интерфейса перед пользователем в отношении поведения функции. Повышение гарантий путем добавления noexcept
позже считается безопасным.
Почему полезно помечать функции как не выбрасывающие исключения
Есть несколько веских причин отмечать функции как не выбрасывающие исключения:
- Функции, не выбрасывающие исключения, можно безопасно вызывать из функций, которые не безопасны для исключений, например, деструкторов.
- Функции с
noexcept
могут позволить компилятору выполнить некоторые оптимизации, которые в противном случае были бы недоступны. Поскольку функцияnoexcept
не может генерировать исключение, компилятору не нужно беспокоиться о сохранении стека выполнения в нераскручиваемом состоянии, что может позволить ему создавать более быстрый код. - Также есть несколько случаев, когда знание функции
noexcept
позволяют нам создавать более эффективные реализации в нашем собственном коде: контейнеры стандартной библиотеки (например,std::vector
) не выбрасывают исключения и будут использовать операторnoexcept
, чтобы определить, использовать ли в некоторых местах семантику перемещения (быстрее) или семантику копирования (медленнее) (семантику перемещения мы рассмотрим в главе M).
Динамические спецификации исключений
Дополнительные материалы
До C++11 и до C++17 вместо noexcept
использовались динамические спецификации исключений. Синтаксис динамических спецификаций исключений использует ключевое слово throw
для перечисления типов исключений, которые функция может прямо или косвенно генерировать:
// не генерирует исключения
int doSomething() throw();
// может выбрасывать исключения либо std::out_of_range, либо указатель на int
int doSomething() throw(std::out_of_range, int*);
// может выбросить что угодно
int doSomething() throw(...);
Из-за таких факторов, как неполные реализации компиляторов, некоторая несовместимость с шаблонами функций, распространенное недопонимание того, как они работают, и тот факт, что стандартная библиотека в основном их не использовала, динамические спецификации исключений были объявлены устаревшими в C++11 и удалены из языка в C++17 и C++20. Для получения более подробной информации смотрите этот документ.