Если для обработки особых ситуаций мы используем иерархию классов,
то, естественно, каждый обработчик должен разбираться только с
частью информации, передаваемой при особых ситуациях. Можно сказать,
что, как правило, особая ситуация перехватывается обработчиком
ее базового класса, а не обработчиком класса, соответствующего
именно этой особой ситуации. Именование и перехват обработчиком особой
ситуации семантически эквивалентно именованию и получению параметра
в функции. Проще говоря, формальный параметр инициализируется
значением фактического параметра. Это означает, что запущенная
особая ситуация "низводится" до особой ситуации, ожидаемой
обработчиком. Например:
class Matherr {
// ...
virtual void debug_print();
};
class Int_overflow : public Matherr {
public:
char* op;
int opr1, opr2;;
int_overflow(const char* p, int a, int b)
{ cerr << op << '(' << opr1 << ',' << opr2 << ')'; }
};
void f()
{
try {
g();
}
catch (Matherr m) {
// ...
}
}
При входе в обработчик Matherr особая ситуация m является объектом
Matherr, даже если при обращении к g() была запущена Int_overflow.
Это означает, что дополнительная информация, передаваемая в
Int_overflow, недоступна.
Как обычно, чтобы иметь доступ к дополнительной информации можно
использовать указатели или ссылки. Поэтому можно было написать так:
int add(int x, int y) // сложить x и y с контролем
{
if (x > 0 && y > 0 && x > MAXINT - y
|| x < 0 && y < 0 && x < MININT + y)
throw Int_overflow("+", x, y);
// Сюда мы попадаем, либо когда проверка
// на переполнение дала отрицательный результат,
// либо когда x и y имеют разные знаки
return x + y;
}
void f()
{
try {
add(1,2);
add(MAXINT,-2);
add(MAXINT,2); // а дальше - переполнение
}
catch (Matherr& m) {
// ...
m.debug_print();
}
}
Здесь последнее обращение к add приведет к запуску особой ситуации,
который, в свою очередь, приведет к вызову Int_overflow::debug_print().
Если бы особая ситуация передавалась по значению, а не по ссылке, то
была бы вызвана функция Matherr::debug_print().
Нередко бывает так, что перехватив особую ситуацию, обработчик
решает, что с этой ошибкой он ничего не сможет поделать. В таком
случае самое естественное запустить особую ситуацию снова в надежде,
что с ней сумеет разобраться другой обработчик:
void h()
{
try {
// какие-то операторы
}
catch (Matherr) {
if (can_handle_it) { // если обработка возможна,
// сделать ее
}
else {
throw; // повторный запуск перехваченной
// особой ситуации
}
}
}
Повторный запуск записывается как оператор throw без параметров.
При этом снова запускается исходная особая ситуация, которая была
перехвачена, а не та ее часть, на которую рассчитан обработчик
Matherr. Иными словами, если была запущена Int_overflow, вызывающая
h() функция могла бы перехватить ее как Int_overflow, несмотря на
то, что она была перехвачена в h() как Matherr и запущена снова:
void k()
{
try {
h();
// ...
}
catch (Int_overflow) {
// ...
}
}
Полезен вырожденный случай перезапуска. Как и для функций,
эллипсис ... для обработчика означает "любой параметр", поэтому
оператор catch (...) означает перехват любой особой ситуации:
void m()
{
try {
// какие-то операторы
}
catch (...) {
// привести все в порядок
throw;
}
}
Этот пример надо понимать так: если при выполнении основной части
m() возникает особая ситуация, выполняется обработчик, которые
выполняет общие действия по устранению последствий особой ситуации,
после этих действий особая ситуация, вызвавшая их, запускается
повторно.
Поскольку обработчик может перехватить производные особые ситуации
нескольких типов, порядок, в котором идут обработчики в проверяемом
блоке, существенен. Обработчики пытаются перехватить особую
ситуацию в порядке их описания. Приведем пример:
try {
// ...
}
catch (ibuf) {
// обработка переполнения буфера ввода
}
catch (io) {
// обработка любой ошибки ввода-вывода
}
catch (stdlib) {
// обработка любой особой ситуации в библиотеке
}
catch (...) {
// обработка всех остальных особых ситуаций
}
Тип особой ситуации в обработчике соответствует типу запущенной
особой ситуации в следующих случаях: если эти типы совпадают, или
второй тип является типом доступного базового класса запущенной ситуации,
или он является указателем на такой класс, а тип ожидаемой ситуации
тоже указатель ($$R.4.6).
Поскольку транслятору известна иерархия классов, он способен
обнаружить такие нелепые ошибки, когда обработчик catch (...) указан
не последним, или когда обработчик ситуации базового класса
предшествует обработчику производной от этого класса ситуации
($$R15.4). В обоих случая, последующий обработчик (или обработчики)
не могут быть запущены, поскольку они "маскируются" первым
обработчиком.
9.4 Запросы ресурсов
Если в некоторой функции потребуются определенные ресурсы, например,
нужно открыть файл, отвести блок памяти в области свободной
памяти, установить монопольные права доступа и т.д., для дальнейшей
работы системы обычно бывает крайне важно, чтобы ресурсы были
освобождены надлежащим образом. Обычно такой "надлежащий способ"
реализует функция, в которой происходит запрос ресурсов и освобождение
их перед выходом. Например:
void use_file(const char* fn)
{
FILE* f = fopen(fn,"w");
// работаем с f
fclose(f);
}
Все это выглядит вполне нормально до тех пор, пока вы не поймете,
что при любой ошибке, происшедшей после вызова fopen() и до вызова
fclose(), возникнет особая ситуация, в результате которой мы
выйдем из use_file(), не обращаясь к fclose().Ь
Ь Стоит сказать, что та же проблема возникает и в языках, не
поддерживающих особые ситуации. Так, обращение к функции longjump()
из стандартной библиотеки С может иметь такие же неприятные
последствия.
Если вы создаете устойчивую к ошибкам системам, эту проблему
придется решать. Можно дать примитивное решение:
void use_file(const char* fn)
{
FILE* f = fopen(fn,"w");
try {
// работаем с f
}
catch (...) {
fclose(f);
throw;
}
fclose(f);
}
Вся часть функции, работающая с файлом f, помещена в проверяемый
блок, в котором перехватываются все особые ситуации, закрывается
файл и особая ситуация запускается повторно.
Недостаток этого решения в его многословности, громоздкости
и потенциальной расточительности. К тому же всякое многословное
и громоздкое решение чревато ошибками, хотя бы в силу усталости
программиста. К счастью, есть более приемлемое решение. В общем
виде проблему можно сформулировать так:
void acquire()
{
// запрос ресурса 1
// ...
// запрос ресурса n
// использование ресурсов
// освобождение ресурса n
// ...
// освобождение ресурса 1
}
Как правило бывает важно, чтобы ресурсы освобождались в обратном
по сравнению с запросами порядке. Это очень сильно напоминает порядок
работы с локальными объектами, создаваемыми конструкторами и
уничтожаемыми деструкторами. Поэтому мы можем решить проблему запроса и
освобождения ресурсов, если будем использовать подходящие объекты
классов с конструкторами и деструкторами. Например, можно определить
класс FilePtr, который выступает как тип FILE* :
class FilePtr {
FILE* p;
public:
FilePtr(const char* n, const char* a)
{ p = fopen(n,a); }
FilePtr(FILE* pp) { p = pp; }
~FilePtr() { fclose(p); }
operator FILE*() { return p; }
};
Построить объект FilePtr можно либо, имея объект типа FILE*, либо,
получив нужные для fopen() параметры. В любом случае этот
объект будет уничтожен при выходе из его области видимости, и
его деструктор закроет файл. Теперь наш пример сжимается до такой
функции:
void use_file(const char* fn)
{
FilePtr f(fn,"w");
// работаем с f
}
Деструктор будет вызываться независимо от того, закончилась ли функция
нормально, или произошел запуск особой ситуации.
Достарыңызбен бөлісу: |