Чтение онлайн

ЖАНРЫ

Программирование. Принципы и практика использования C++ Исправленное издание
Шрифт:

// ...

int* q = new int[s];

vector<double> v2;

// ...

delete[] p;

delete[] q;

}

Обратите внимание на то, что, если оператор

new
не сможет выделить свободную память, он сгенерирует стандартное исключение
bad_alloc
. Прием
try ... catc
h в этом примере также успешно работает, но нам потребуется несколько блоков
try
, и код станет повторяющимся и ужасным. Мы не любим повторяющиеся и запутанные программы, потому что повторяющийся
код сложно сопровождать, а запутанный код не только сложно сопровождать, но и вообще трудно понять.

ПОПРОБУЙТЕ

Добавьте блоки

try
в последний пример и убедитесь, что все ресурсы будут правильно освобождаться при любых исключениях.

19.5.2. Получение ресурсов — это инициализация

К счастью, нам не обязательно копировать инструкции

try...catch
, чтобы предотвратить утечку ресурсов. Рассмотрим следующий пример:

void f(vector<int>& v, int s)

{

vector<int> p(s);

vector<int> q(s);

// ...

}

Это уже лучше. Что еще более важно, это очевидно лучше. Ресурс (в данном случае свободная память) занимается конструктором и освобождается соответствующим деструктором. Теперь мы действительно решили нашу конкретную задачу, связанную с исключениями. Это решение носит универсальный характер; его можно применить ко всем видам ресурсов: конструктор получает ресурсы для объекта, который ими управляет, а соответствующий деструктор их возвращает. Такой подход лучше всего зарекомендовал себя при работе с блокировками баз данных (database locks), сокетами (sockets) и буферами ввода-вывода (I/O buffers) (эту работу делают объекты класса
iostream
). Соответствующий принцип обычно формулируется довольно неуклюже: “Получение ресурса есть инициализация” (“Resource Acquisition Is Initialization” — RAII).

Рассмотрим предыдущий пример. Как только мы выйдем из функции

f
, будут вызваны деструкторы векторов
p
и
q
: поскольку переменные
p
и
q
не являются указателями, мы не можем присвоить им новые значения, инструкция
return
не может предотвратить вызов деструкторов и никакие исключения не генерируются.

Это универсальное правило: когда поток управления покидает область видимости, вызываются деструкторы для каждого полностью созданного объекта и активизированного подобъекта. Объект считается полностью созданным, если его конструктор закончил свою работу. Исследование всех следствий, вытекающих из этих двух утверждений, может вызвать головную боль. Будем считать просто, что конструкторы и деструкторы вызываются, когда надо и где надо.

В частности, если хотите выделить в области видимости свободную память переменного размера, мы рекомендуем использовать класс
vector
, а не “голые” операторы
new
и
delete
.

19.5.3. Гарантии

Что делать, если вектор невозможно ограничить только одной областью (или подобластью) видимости? Рассмотрим пример.

vector<int>* make_vec // создает заполненный вектор

{

vector<int>* p = new vector<int>; // выделяем свободную память

// ...заполняем вектор
данными;

// возможна генерация исключения...

return p;

}

Это довольно распространенный пример: мы вызываем функцию, чтобы создать сложную структуру данных, и возвращаем эту структуру как результат. Однако, если при заполнении вектора возникнет исключение, функция

make_vec
потеряет этот объект класса
vector
. Кроме того, если функция успешно завершит работу, то кто-то будет должен удалить объект, возвращенный функцией
make_vec
(см. раздел 17.4.6).

Для того чтобы сгенерировать исключение, мы можем добавить блок

try
.

vector<int>* make_vec // создает заполненный вектор

{

vector<int>* p = new vector<int>; // выделяет свободную память

try {

// ...заполняем вектор данными;

// возможна генерация исключения...

return p;

}

catch (...) {

delete p; // локальная очистка

throw; // повторно генерируем исключение,

// чтобы вызывающая

// функция отреагировала на то, что функция

// make_vec не сделала то, что требовалось

}

}

Функция
make_vec
иллюстрирует очень распространенный стиль обработки ошибок: программа пытается выполнить свое задание, а если не может, то освобождает все локальные ресурсы (в данном случае свободную память, занятую объектом класса
vector
) и сообщает об этом, генерируя исключение. В данном случае исключение генерируется другой функцией (
(vector::at
); функция
make_vec
просто повторяет генерирование с помощью оператора
throw
;.

Это простой и эффективный способ обработки ошибок, который можно применять систематически.

Базовая гарантия. Цель кода

try ... catch
состоит в том, чтобы гарантировать, что функция
make_vec
либо завершит работу успешно, либо сгенерирует исключение без утечки ресурсов. Это часто называют базовой гарантией (basic guarantee). Весь код, являющийся частью программы, которая восстанавливает свою работу после генерирования исключения, должна поддерживать базовую гарантию.

Жесткая гарантия. Если кроме базовой гарантии, функция также гарантирует, что все наблюдаемые значения (т.е. все значения, не являющиеся локальными по отношению к этой функции) после отказа восстанавливают свои предыдущие значения, то говорят, что такая функция дает жесткую гарантию (strong guarantee). Жесткая гарантия — это идеал для функции: либо функция будет выполнена так, как ожидалось, либо ничего не произойдет, кроме генерирования исключения, означающего отказ.

Гарантия отсутствия исключений (no-throw guarantee). Если бы мы не могли выполнять простые операции без какого бы то ни было риска сбоя и без генерирования исключений, то не могли бы написать код, соответствующий условиям базовой и жесткой гарантии. К счастью, практически все встроенные средства языка С++ поддерживают гарантию отсутствия исключений: они просто не могут их генерировать. Для того чтобы избежать генерирования исключений, просто не выполняйте оператор

throw
,
new
и не применяйте оператор dynamic_cast к ссылочным типам (раздел A.5.7).

Поделиться с друзьями: