Повторное возбуждение исключений
Оператор throw без параметров, находящийся внутри обработчика исключения, используется для повторного возбуждения того же самого исключения, которое было поймано в текущем блоке catch. Он применяется в том случае, когда исключение не может быть полностью обработано в данном блоке catch, и его обработка будет закончена где-то в другом месте. Чаще всего оператор try c блоком catch, содержащим повторное возбуждение исключения, используется для освобождения захваченных ресурсов перед тем, как продолжить раскрутку стека.
Рассмотрим пример. Пусть дан текстовый файл, содержащий последовательность целых чисел. Вначале в файле записано количество чисел N, а затем − сами числа, разделенные пробелами и/или переводами строк. Требуется написать функцию readIntegers, которая считывает все числа в массив и возвращает его в качестве результата. Если количество чисел окажется меньше, чем N, или в файле встретится не число, то функция должна возбудить исключение WrongFileFormatException:
// Пример 4.7 - повторное возбуждение исключения
class FileNotOpenedException{};
class WrongFileFormatException{};
int* readIntegers(const std::string &fileName) {
std::ifstream inp(fileName);
if (!inp.is_open()) throw FileNotOpenedException(fileName);
int n;
inp >> n;
if (!inp.good()) throw WrongFileFormatException();
int* a = new int [n];
try{
for (int i = 0; i < n; i++) {
inp >> a[i];
if (!inp.good()) throw WrongFileFormatException();
}
} catch(...) {
delete[] a;
throw; // повторное возбуждение исключения
}
return a;
}
Если при вводе чисел произойдёт исключение любого типа, то управление передастся блоку catch, в котором произойдёт освобождение памяти, выделенной под массив. Заметим, что автоматически эта память не освободилась бы: из стека удалилась бы лишь локальная переменная-указатель a, но захваченный в куче блок памяти так и остался бы занятым.
Мы предотвратили утечку памяти, но функция всё-таки не смогла выполнить свою работу − прочитать N чисел из файла. Поэтому повторно возбуждаем исключение, чтобы оно могло быть поймано другим обработчиком (например, находящимся в функции main).
Заметим, что в хорошо написанной программе повторное возбуждение исключений используется редко. Авторы рекомендуют по возможности всегда помещать код освобождения ресурсов в деструкторы классов. В этом случае нет необходимости в лишних блоках try-catch, а также нет опасности забыть выполнить освобождение ресурсов. Например, вышеописанную функцию правильней было бы написать так, чтобы она возвращала не массив, а вектор. В случае исключения деструктор вектора будет вызван автоматически. Приведём улучшенный вариант данной функции с использованием вектора вместо массива:
// Пример 4.8 - улучшенный вариант функции readIntegers
std::vector<int> readIntegers(const std::string &fileName) {
std::ifstream inp(fileName);
if (!inp.is_open()) throw FileNotOpenedException(fileName);
int n;
inp >> n;
if (!inp.good()) throw WrongFileFormatException();
std::vector<int> a(n);
for (int i = 0; i < n; i++) {
inp >> a[i];
if (!inp.good()) throw WrongFileFormatException();
}
return a;
}
Как видим, код получился более коротким и понятным, он не содержит лишних блоков try-catch, а выделение и освобождение памяти скрыто в реализации класса std::vector.
4.5 "Аппаратные" и "программные" исключения
До сих пор мы рассматривали исключения, которые можно условно назвать "программными" − они возбуждаются с помощью вызова оператора throw (неважно, написан ли он нами, или скрыт где-то внутри реализации используемых библиотечных функций). Возникает вопрос, а можно ли на языке C++ обрабатывать так называемые "аппаратные" исключения, источником которых являются события в процессоре и/или в операционной системе. Простейшим примером является деление целого числа на ноль. Будет ли корректно работать следующий код?
int x, a, b;
try { x = a / b;}
catch(…) {std::cout << "Деление на 0\n";}
Разберёмся, что происходит при делении двух целых чисел. Если делитель оказывается равным нулю, то внутри процессора происходит прерывание. Выполнение программы приостанавливается, и вызывается обработчик этого прерывания − специальная подпрограмма операционной системы. В результате операционная система узнаёт о том, что произошло, а также в каком именно процессе (работающей программе) это случилось. Вопрос заключается в том, может ли данная информация каким-то образом быть передана операционной системой нашему процессу. При этом ещё и должна выполниться конвертация этого события в определённый тип исключений языка C++.
Возможно ли это, зависит от конкретной операционной системы и компилятора. Например, в ОС Windows встроен механизм, который называется "структурная обработка исключений" (Structured Exception Handling, SEH). Поддержка таких исключений имеется в компиляторе Microsoft Visual C++ (по умолчанию она выключена, для включения нужно использовать ключ компиляции /EHa либо соответствующую настройку проекта). Более того, в MS Visual C++ даже имеются специализированные операторы try-except и try-finally для работы с SHE (поскольку данные операторы отсутствуют в стандарте языка С++, в данной книге мы их рассматривать не будем).
Заметим, что поддержка подобных исключений в настоящее время отсутствует в текущей версии компилятора GNU C++ для Windows (из пакета mingw). Однако, версия компилятора GNU C++ для ОС Linux похожий вид исключений поддерживает.
Таким образом, можно сделать вывод, что при написании переносимого кода, который должен компилироваться разными компиляторами под разные платформы, использовать "аппаратные" исключения в настоящее время не рекомендуется.