RAII и гарантии при исключениях
Пока исключение летит вниз, каждый пройденный этаж закрывается, и лампы его локальных объектов гаснут сами: деструкторы выполняются при раскрутке стека. Но у объекта из new такого выключателя нет. Эту лампу гасят только вручную, через delete, а до этой строки выполнение может не дойти.
Исключение — ещё один путь выхода из функции, и на нём тоже нужно освободить ресурсы. Раскрутка стека вызывает деструкторы локальных объектов, но у обычного указателя нет деструктора, который удалил бы объект:
#include <iostream>
#include <memory>
#include <stdexcept>
struct Noisy { ~Noisy() { std::cout << "~Noisy "; } };
int main() {
try {
Noisy* raw = new Noisy; // ✗
auto owned = std::make_unique<Noisy>();
throw std::runtime_error("fail");
delete raw;
} catch (const std::exception& e) {
std::cout << e.what() << '\n'; // ~Noisy fail
}
}Давайте разберём, почему деструктор выполнился один раз, хотя объектов Noisy два:
rawполучает адрес первогоNoisyиз кучи.ownedвладеет вторымNoisyчерезstd::unique_ptr.throwпрерывает блокtry, и доdelete rawвыполнение не доходит.- При раскрутке стека
ownedуничтожается и удаляет свой объект: печатается~Noisy. - Первый
Noisyостаётся в куче — это утечка.
Владелец на каждом пути
Чтобы исключение не оставляло утечек, ресурсы отдают RAII-владельцам вроде std::unique_ptr, а не окружают каждый delete блоком try. Деструктор владельца выполняется на любом пути выхода из функции, в том числе при исключении, если его где-то поймают.
Три уровня гарантий
На случай исключения функция может дать одну из трёх гарантий. Их часто спрашивают на собеседовании, поэтому разберём каждую.
Базовая гарантия
Базовая гарантия: ничего не утекает, а все объекты остаются пригодными к работе, хотя их значения могли измениться. RAII-владельцы дают первую часть базовой гарантии: при исключении утечек нет.
Строгая гарантия
Строгая гарантия: если вызов завершился исключением, состояние программы такое же, как до вызова. Обычно её добиваются так: новое значение собирают в локальной переменной и только в конце отдают его наружу.
void add_pair(std::vector<int>& v, int a, int b) {
std::vector<int> copy = v;
copy.push_back(a);
copy.push_back(b);
v = std::move(copy);
}Разберём по строкам:
copy— копия вектораv, и дальше функция меняет только её.- Любой
push_backможет бросить исключение, если не хватит памяти. Тогда раскрутка стека уничтожитcopy, аvостанется прежним. v = std::move(copy)переносит готовый результат вvи исключений не бросает.
Если бы функция добавляла элементы прямо в v, исключение во втором push_back оставило бы в нём только a. Это базовая гарантия, но не строгая.
Гарантия отсутствия исключений
Гарантия отсутствия исключений: функция не бросает исключений совсем. Такое обещание записывают в объявлении функции словом noexcept.
Главное
- Исключение — ещё один путь выхода из функции, и ресурсы на нём освобождает только деструктор.
- Объект из
newбез владельца утекает, если исключение обойдётdelete; RAII-владелец освобождает его сам. - Базовая гарантия: утечек нет, объекты пригодны к работе; строгая: при исключении состояние не изменилось.
- Строгую гарантию получают, собирая результат в копии и отдавая его наружу в конце.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.