На этом уроке мы рассмотрим, что такое спецификации исключений в языке С++, а также использование спецификатора noexcept.
- Спецификации исключений
- Спецификатор noexcept
- Спецификатор noexcept с параметром типа bool
- Не выбрасывающие и потенциально выбрасывающие исключения функции
- Оператор noexcept
- Гарантии безопасности исключений
- Когда следует использовать спецификатор noexcept
- Почему полезно отмечать функции, как не выбрасывающие исключений функции
- Динамические спецификации исключений
Спецификации исключений
В языке C++ все функции условно можно разделить на 2 типа:
функции, не выбрасывающие исключений;
функции, которые потенциально могут выбросить исключения.
Рассмотрим следующее объявление функции:
1 |
int doSomething(); // может ли эта функция выбросить исключение или нет? |
Глядя на типичное объявление функции, не представляется возможным определить, может ли функция выбросить исключение или нет. Несмотря на то, что комментарии могут помочь обозначить, выбрасывает ли функция исключения или нет (и если да, то какие именно исключения), у нас нет никакого специального компилятора для комментариев, а документация может устареть.
Спецификации исключений — это механизм языка C++, который изначально был разработан для документирования в пределах объявления функции того, какие исключения она может выбрасывать. Хотя большинство спецификаций исключений теперь устарели или удалены из стандарта, но в качестве замены была добавлена одна полезная спецификация исключений, которую мы рассмотрим на данном уроке.
Спецификатор noexcept
Спецификатор noexcept определяет функцию как не выбрасывающую исключений. Чтобы определить функцию как не выбрасывающую, мы можем использовать спецификатор noexcept в объявлении функции, поместив его справа от списка параметров функции:
1 |
void doSomething() noexcept; // эта функция является не выбрасывающей исключений функцией |
Обратите внимание, что noexcept на самом деле не запрещает функции выбрасывать исключения или вызывать другие функции, которые потенциально могут выбросить исключения. Скорее всего, при возникновении исключения, если оно происходит из noexcept-функции, будет вызвана функция std::terminate(). И обратите внимание, что если std::terminate() вызывается внутри noexcept-функции, то раскручивание стека может происходить, а может и не происходить (в зависимости от реализации и оптимизации). А это означает, что ваши объекты могут быть уничтожены должным образом до завершения работы, а может и не произойти этого уничтожения.
Подобно тому, как функции, отличающиеся только своими возвращаемыми значениями, не могут быть перегружены, функции, отличающиеся только своей спецификацией исключений, также не могут быть перегружены.
Спецификатор noexcept с параметром типа bool
Спецификатор noexcept имеет необязательный параметр типа bool:
noexcept(true)
равносильно noexcept, что означает, что функция не является выбрасывающей;
noexcept(false)
означает, что функция относится к классу потенциально выбрасывающих исключения функций.
Эти параметры обычно используются только в шаблонных функциях, так что шаблонная функция может быть динамически создана как не выбрасывающая или потенциально выбрасывающая исключения на основе некоторого параметризованного значения.
Не выбрасывающие и потенциально выбрасывающие исключения функции
Функции, которые по умолчанию являются не выбрасывающими исключения:
конструкторы, заданные по умолчанию;
операторы присваивания копированием;
операторы присваивания перемещением.
Однако, если какая-либо из вышеперечисленных функций вызывает (явно или неявно) другую функцию, которая может выбросить исключение, то вызывающая функция также будет рассматриваться как потенциально выбрасывающая исключения. Например, если класс имеет элемент данных с потенциально выбрасывающим исключение конструктором, то конструкторы класса также будут рассматриваться как потенциально выбрасывающие исключения. В качестве другого примера, если оператор присваивания копированием вызывает потенциально выбрасывающий исключение оператор присваивания, то оператор присваивания копированием также будет рассматриваться как оператор, потенциально выбрасывающий исключения.
Совет: Если вы хотите, чтобы какая-либо из вышеперечисленных функций не выбрасывала исключений, то явно пометьте её как noexcept (даже если она является таковой по умолчанию), чтобы случайно данная функция не стала функцией, потенциально выбрасывающей исключение.
По умолчанию, потенциально выбрасывающими исключение функциями являются следующие объекты:
обычные функции;
пользовательские конструкторы;
некоторые операторы, такие как оператор new.
Оператор noexcept
Оператор noexcept может использоваться внутри функций. Он принимает в качестве аргумента выражение и возвращает true
или false
, если компилятор считает, что выражение не может или может выбросить исключение. Оператор noexcept проверяется статически во время компиляции и фактически не вычисляет входное выражение.
1 2 3 4 5 6 7 8 9 10 |
void foo() {throw -1;} void boo() {}; void goo() noexcept {}; struct S{}; constexpr bool b1{ noexcept(5 + 3) }; // true; целочисленные значения не выбрасывают исключений constexpr bool b2{ noexcept(foo()) }; // false; функция foo() выбрасывает исключение constexpr bool b3{ noexcept(boo()) }; // false; функция boo() - неявное noexcept(false) constexpr bool b4{ noexcept(goo()) }; // true; функция goo() - явное noexcept(true) constexpr bool b5{ noexcept(S{}) }; // true; заданный по умолчанию конструктор структуры является по умолчанию noexcept |
Оператор noexcept может использоваться для задания условия выполнения кода: является ли фрагмент кода потенциально выбрасывающим исключения или нет. Это необходимо для осуществления определенных гарантий безопасности исключений, о которых мы поговорим дальше.
Гарантии безопасности исключений
Гарантии безопасности исключений — это договоренности о том, как функции или классы будут вести себя в случае возникновения исключений. Существуют 4 уровня безопасности исключений:
Нет никаких гарантий — нет никаких гарантий относительно того, что произойдет, если возникнет исключение (например, класс может быть оставлен в непригодном для использования состоянии).
Базовая гарантия — если возникнет исключение, то утечки памяти не произойдет (все ресурсы будут освобождены корректно), и объект все еще будет использоваться, но программа может быть оставлена в измененном состоянии.
Строгая гарантия — если возникнет исключение, то утечки памяти не произойдет (все ресурсы будут освобождены корректно), состояние программы не будет изменено. Это означает, что функция должна корректно завершить свою работу, либо не иметь побочных эффектов в случае, если функция аварийно завершила свою работу. Простыми словами — если при выполнении операции возникнет исключение, то программа останется в том же состоянии, которое было до начала выполнения операции.
Гарантия отсутствия исключений/сбоев — работа функции всегда завершается успешно (без сбоев) или завершается аварийно, но без выбрасывания исключений.
Давайте рассмотрим пункт «Гарантия отсутствия исключений/сбоев» более подробно.
Гарантия отсутствия исключений: Если функция завершается аварийно, то она не будет выбрасывать исключение. Вместо этого она вернет код ошибки или проигнорирует проблему. Гарантии отсутствия исключений требуются во время раскручивания стека, когда исключение уже обрабатывается; например, все деструкторы должны иметь гарантию отсутствия исключения (как и любые функции, вызываемые этими деструкторами). Примеры кода, который относится к коду с отсутствием исключений:
деструкторы и функции освобождения/очистки памяти;
функции, которые вызывают другие функции, имеющие гарантии отсутствия исключений.
Гарантия отсутствия сбоев: Функция всегда успешно выполняет свою работу (и, следовательно, у нее никогда не будет необходимости выбрасывать исключения. Таким образом, гарантия отсутствия сбоя — это немного более сильная форма гарантии отсутствия исключения). Примеры кода, который относится к коду с отсутствием сбоев:
конструкторы перемещения и оператор присваивания перемещением;
swap-функции;
контейнерные функции clear()/erase()/reset();
операции с std::unique_ptr;
функции, которые должны вызывать другие функции, имеющие гарантии отсутствия сбоев.
Когда следует использовать спецификатор noexcept
Из того факта, что ваш код явно не выбрасывает никаких исключений, еще не следует, что вы должны начать указывать спецификатор noexcept на все подряд функции. По умолчанию, большинство функций потенциально способны выбросить исключение, поэтому, если ваша функция вызывает другие функции, есть вероятность того, что она вызывает функцию, которая потенциально способна выбросить исключение, и, следовательно, сама вызывающая функция будет относиться к потенциально выбрасывающим исключение функциям.
Принципы Стандартной библиотеки С++ состоят в том, чтобы использовать спецификатор noexcept только для тех функций, которые НЕ ДОЛЖНЫ выбрасывать исключения или аварийно завершать свою работу. Функции, которые потенциально способны выбросить исключения, но по факту не генерируют исключения (из-за реализации), как правило, не помечаются как noexcept-функции.
Советы:
Используйте спецификатор noexcept лишь в конкретных случаях, когда вы хотите явно указать на гарантию отсутствия сбоя или отсутствие выбрасывания исключения.
Если вы не уверены, должна ли функция иметь гарантию отсутствия сбоя/исключения, то лучше перестраховаться и не отмечать её с помощью noexcept. Решение использовать в таких случаях спецификатор noexcept нарушает обязательство взаимодействия с пользователем относительно поведения функции. Гораздо лучше потом при необходимости ужесточить гарантии безопасности, добавив спецификатор noexcept.
Почему полезно отмечать функции, как не выбрасывающие исключений функции
Есть пара веских причин, почему стоит помечать функции спецификатором noexcept:
Функции, которые являются noexcept, могут позволить компилятору выполнять некоторые оптимизации, которые в противном случае были бы недоступны. Поскольку
noexcept-функция не может выбрасывать исключения, то компилятору не нужно беспокоиться о сохранении стека времени выполнения в состоянии произвести раскручивание, что может позволить компилятору создавать более быстрый код.
Есть также несколько случаев, когда знание, что функция относится к noexcept, позволяет нам создавать более эффективные реализации в нашем собственном коде: стандартные библиотечные контейнеры (такие как std::vector) знают о noexcept и будут использовать его для определения того, следует ли использовать семантику перемещения (быстрее) или семантику копирования (медленнее) в определенных местах.
Динамические спецификации исключений
До C++11 и даже до C++17, динамические спецификации исключений использовались вместо noexcept. Синтаксис динамических спецификаций исключений использует ключевое слово throw для перечисления типов исключений, которые функция может прямо или косвенно генерировать:
1 2 3 |
int doSomething() throw(); // не выбрасывает исключений int doSomething() throw(std::out_of_range, int*); // может выбросить либо исключение типа std::out_of_range, либо указатель на целочисленное значение int doSomething() throw(...); // может выбросить что угодно |
Из-за таких факторов, как неполная реализация компилятора, некоторая несовместимость с шаблонными функциями, распространенное непонимание того, как они работают, и тот факт, что Стандартная библиотека С++ в основном не использует их, динамические спецификации исключений были объявлены устаревшими в C++11 и удалены из языка в C++17 и C++20. Более детально об этом читайте здесь.