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

Оператор 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 похожий вид исключений поддерживает.

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

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