Шаг 121.
Библиотека STL.
Ошибки и исключения внутри STL. Обработка исключений
На этом шаге мы рассмотрим особенности обработки исключений.
Проверка логических ошибок в STL практически отсутствует, поэтому сама библиотека STL почти не генерирует исключения,
связанные с логикой. Фактически существует только одна функция, для которой в стандарте прямо указано на возможность возникновения
исключения: речь идет о функции at() векторов и деков (проверяемой версии оператора индексирования). Во всех остальных
случаях стандарт требует лишь стандартных исключений типа bad_alloc при нехватке памяти или исключений, возникающих
при пользовательских операциях. Когда генерируются исключения и что при этом происходит с компонентами STL? В течение долгого
времени, пока шел процесс стандартизации, строгих правил на этот счет не существовало. В сущности, любое исключение приводило к
непредсказуемым последствиям. Даже уничтожение контейнера STL после того, как во время выполнения одной из
поддерживаемых им операций произошло исключение, тоже приводило к непредсказуемым последствиям (например, аварийному завершению
программы). Из-за этого библиотека STL оказывалась бесполезной там, где требовалось гарантированное четко определенное
поведение, потому что не была предусмотрена даже возможность раскрутки стека.
Обработка исключений стала одним из последних вопросов, обсуждавшихся в процессе стандартизации. Найти хорошее решение
оказалось нелегко. На это понадобилось много времени, что объяснялось двумя основными причинами.
- Было очень трудно определить, какую степень безопасности должна обеспечивать стандартная библиотека C++. Напрашивается
мысль, что всегда следует обеспечивать максимальный уровень из всех возможных. Например, можно потребовать, чтобы вставка
нового элемента в произвольной позиции вектора либо завершалась успешно, либо не вносила изменений. Однако исключение может
произойти в процессе копирования элементов, расположенных после позиции вставки, на следующую позицию для освобождения
места под новый элемент; в этом случае полное восстановление невозможно. Чтобы добиться указанной цели, вставку пришлось бы
реализовать с копированием всех элементов вектора в новую область памяти, что серьезно отразилось бы на быстродействии. Если
приоритетной задачей проектирования является высокое быстродействие (как в случае STL), обеспечить идеальную обработку
исключений для всех возможных ситуаций все равно не удастся. Приходится искать компромисс между безопасностью и скоростью
работы.
- Многие беспокоились о том, что присутствие кода обработки исключений отрицательно скажется на быстродействии. Это
противоречило бы главной цели проектирования - обеспечению максимального быстродействия. Впрочем, разработчики компиляторов
утверждают, что в принципе обработка исключений реализуется без сколько-нибудь заметного снижения быстродействия (причем
такие реализации уже существуют). Несомненно, лучше иметь гарантированные четко определенные правила обработки исключений
без заметного снижения быстродействия, чем рисковать системными сбоями из-за исключений. В результате этих обсуждений
стандартная библиотека C++ теперь предоставляет базовую гарантию безопасности исключений: возникновение исключений
в стандартной библиотеке C++ не приводит к утечке ресурсов или нарушению контейнерных инвариантов.
К сожалению, во многих случаях этого недостаточно. Довольно часто требуется более твердая гарантия того, что при возникновении
исключения произойдет возврат к состоянию, имевшему место перед началом операции. О таких операциях говорят как об атомарных
по отношению к исключениям, а если воспользоваться терминологией баз данных, можно сказать, что эти операции должны
обеспечивать возможность либо принятия, либо отката, то есть транзакционную безопасность. В том, что касается этих требований,
стандартная библиотека C++ теперь гарантирует две вещи.
- Для всех узловых контейнеров (списки, множества, мультимножества, отображения и мультиотображения) неудачная попытка
создания узла просто оставляет контейнер в прежнем состоянии. Более того, удаление узла не может завершиться неудачей (если
исключение не произойдет в деструкторе). Однако при вставке нескольких элементов в ассоциативный контейнер из-за необходимости
сохранения порядка сортировки обеспечивать полное восстановление было бы непрактично. Таким образом, только одноэлементная
вставка в ассоциативные контейнеры обеспечивает транзакционную безопасность (то есть либо завершается успешно, либо не вносит
изменений). Кроме того, гарантируется, что все операции удаления (как одного, так и нескольких элементов) всегда завершаются
успешно.
Для списков даже вставка нескольких элементов обладает транэакционной безопасностью. Более того, любые операции со списками,
кроме remove(), remove_if(), merge(), sort() и unique(), либо завершаются успешно, либо не вносят изменений. Для
некоторых из перечисленных операций стандартная библиотека C++ предоставляет условные гарантии. Отсюда можно
сделать вывод: если вам требуется контейнер, обладающий транэакционной безопасностью, выбирайте список.
- Контейнеры на базе массивов (векторы и деки) не обеспечивают полного восстановления при вставке элементов.
Для этого пришлось бы копировать все элементы, находящиеся за позицией вставки, а на обеспечение полного восстановления для
всех операций копирования ушло бы слишком много времени. Тем не менее операции присоединения и удаления элементов в конце
контейнера не требуют копирования существующих элементов, поэтому при возникновении исключений гарантируется откат
(возвращение к прежнему состоянию). Более того, если элементы относятся к типу, у которого операции копирования (копирующий
конструктор и оператор присваивания) не генерируют исключений, то любые операции с этими элементами либо завершаются успешно,
либо не вносят изменений.
Учтите, что все гарантии основаны на запрете исключений в деструкторах (который в C++ должен выполняться всегда). Стандартная
библиотека С ++ соблюдает это требование; его должны соблюдать и прикладные программисты.
Если вам понадобится контейнер с полными гарантиями транзакционной безопасности, используйте либо список (без вызова функций
sort() и unique()), либо ассоциативный контейнер (без вызова многоэлементных операций вставки). Тогда вам не
придется создавать копии данных перед модификацией, чтобы предотвратить возможную потерю данных. Копирование контейнеров
иногда обходится очень дорого.
Если вы не используете узловой контейнер, но нуждаетесь в полноценной поддержке транзакционной безопасности, для всех
критических операций придется делать промежуточные копии данных. Например, следующая функция обеспечивает почти безопасную
вставку значения в заданную позицию произвольного контейнера:
template <class Т, class Cont, class Iter>
void insert (Cont& coll, const Iter& pos, const T& value)
{
Cont tmp(coll); // Копирование контейнера со всеми элементами
tmp.insert(pos,value); // Модификация копии
coll.swap(tmp); // Использование копии (если
// модификация выполнена без исключений)
}
"Почти" означает, что эта функция не идеальна. Дело в том, что функция swap() тоже может генерировать исключение, если
оно имеет место в критерии сравнения ассоциативного контейнера. Как видите, безупречная обработка исключений - дело весьма
непростое.
На следующем шаге мы рассмотрим расширение STL.
Предыдущий шаг
Содержание
Следующий шаг