Спецификация исключений, возбуждаемых функцией

При объявлении функции в языке С++ можно явным образом объявить, какие исключения могут быть сгенерированы из этой функции. Для этого используется ключевое слово throw. Например, можно явно объявить, что функция firstLine способна возбудить исключение типа FileNotOpenedException.

std::string firstLine(const std::string &fileName)

throw (FileNotOpenedException) {

тело функции

}

Важно отметить, что на этапе компиляции не будет проверяться, что функция firstLine способна генерировать исключения только типа FileNotOpenedException. Такая проверка производится непосредственно во время выполнения программы: если в функции возникает исключение, не указанное в спецификации, то вызовется функция unexpected() из стандартной библиотеки C++, которая по умолчанию вызывает функцию terminate() для аварийного завершения программы.

Проверка соответствия исключений спецификации только во время выполнения программы не даёт особых плюсов, но при этом замедляет время работу программы (для сравнения, в языке Java подобная проверка выполняется на этапе компиляции). Поэтому в настоящее время спецификация исключений с помощью ключевого слова throw в языке C++ объявлена устаревшей и не рекомендуется к использованию.

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

inline int sqr(int x) noexcept {

return x * x;

}

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

Во-вторых, в некоторых случаях это может привести к повышению производительности. Например, согласно стандарту C++11, стандартные алгоритмы и контейнеры не должны использовать конструктор перемещения, если он способен выбрасывать исключения. В результате, если вы даже реализовали в классе конструктор перемещения, но не указали ключевое слово noexcept, то вполне вероятно, что при работе с STL всё равно будет использоваться более медленный конструктор копирования (хотя это может зависеть от конкретного компилятора).

Примечание. Кроме ключевого слова noexcept, в C++ имеется ещё оператор noexcept − он проверяет (во время компиляции), что аргумент специфицирован как не бросающий исключение. В качестве аргумента обычно выступает имя функции. Например, если функция sqr у нас объявлена с ключевым словом noexcept, то следующий код поместит true в переменную res:

bool res = noexcept(sqr);

Исключения в конструкторах при наследовании

Иногда возникает необходимость в конструкторе производного класса поймать исключение, возникшее в конструкторе родителя (на практике это требуется редко, но всё же иногда может понадобиться). Обычным образом это сделать нельзя. Однако, в C++ имеется специальный синтаксис, позволяющий решить эту задачу. Рассмотрим его на примере. Пусть в конструкторе базового класса A может возникнуть исключение типа std::invalid_argument. В конструкторе дочернего класса B мы хотим поймать это исключение.

// Пример 4.10 - перехват исключения из конструктора предка

class A {

public:

A(int x) {

if (x <= 0) throw std::invalid_argument("x <= 0");

}

};

class B : public A {

public:

B(int x) try : A(x) {

// тело конструктора

} catch (std::invalid_argument) {

std::cerr << "Перехвачено исключение из конструктора родителя";

}

};

Интересной особенностью такой конструкции try-catch является то, что исключение, пойманное в блоке catch, не будет считаться обработанным даже несмотря на отсутствие оператора throw для повторного возбуждения исключения. Действительно, поскольку объект класса B не смог корректно создаться, то нормальное продолжение программы невозможно, и будет выполнен поиск следующего обработчика данного исключения.

Исключения в деструкторах

Большинство профессиональных программистов на языке C++ рекомендуют разрабатывать деструкторы классов по возможности так, чтобы они не могли возбуждать исключения (кроме исключений, обработанных внутри самого деструктора). Причина заключается в следующем. При обработке исключения происходит раскрутка стека, при этом вызываются деструкторы локальных объектов. Если в деструкторе локального объекта случится ещё одно исключение, не обработанное внутри деструктора, то будет вызвана функция terminate для аварийного завершения программы. При этом заранее сложно предсказать, в какой момент это произойдёт, то есть поведение программы будет близко к неопределённому.

Возникает вопрос: что делать, если в деструкторе нужно вызвать функцию, способную возбудить исключение? Понятно, что вызов такой функции можно поместить в блок try-catch, но не всегда ясно, что именно делать в блоке catch. Один из вариантов − корректно завершить работу программы, записав в файл журнала подробную информацию об ошибке.

Наши рекомендации