5 Výjimky a RAII
- rozlišit záruky výjimek
- vysvětlit, proč destruktor nesmí vyhazovat
- popsat, proč vektor někdy kopíruje místo přesunu
- použít RAII a scope guard
5.1 Ukázka: únik paměti
// chyba
void leak() {
int* p = new int[100];
risky(); // výjimka: delete se nezavolá
delete[] p;
}
Oprava je dát zdroj do objektu, jehož destruktor zavolá překladač sám:
#include <cassert>
#include <memory>
#include <stdexcept>
int alive = 0;
struct Tracked {
Tracked() { ++alive; }
~Tracked() { --alive; }
};
void risky() { throw std::runtime_error("selhání"); }
void safe() {
auto owner = std::make_unique<Tracked>();
risky();
}
int main() {
try {
safe();
} catch (const std::runtime_error&) {
}
assert(alive == 0);
}
To je RAII: získání zdroje je inicializace objektu a uvolnění je jeho destrukce.
5.2 Záruky výjimek
- Basic: nic neuniklo, invarianty platí, stav může být jiný.
- Strong: operace buď uspěje, nebo je stav úplně jako předtím.
- No-throw: operace nevyhodí výjimku.
Pořadí pro silnou záruku: nejdřív práce, která může selhat, stranou. Až pak změna stavu operacemi, které selhat nemohou.
5.3 Destruktory
Destruktory jsou implicitně noexcept. Vyhozená výjimka z nich tedy znamená std::terminate. Vyhození během zpracování jiné výjimky by navíc nemělo jak dopadnout. Chybu v destruktoru zaloguj, nebo nabídni metodu close().
5.4 Realokace vektoru: kopie, nebo přesun
Vektor při růstu přenáší prvky do nového úložiště a chce silnou záruku. Přesun je rychlý, ale kdyby selhal uprostřed, zdroj už je vykradený a nejde se vrátit. Proto:
- přesun
noexcept→ přesouvá - jinak, pokud jde kopírovat → kopíruje
- typ jen přesouvatelný → přesouvá a silná záruka padá
Tomu slouží std::move_if_noexcept.
#include <cassert>
#include <vector>
template <bool NoThrow>
struct Item {
static inline int copies = 0;
Item() = default;
Item(const Item&) { ++copies; }
Item(Item&&) noexcept(NoThrow) {}
};
template <bool NoThrow>
int copies_on_growth() {
std::vector<Item<NoThrow>> v(4);
Item<NoThrow>::copies = 0;
v.reserve(100);
return Item<NoThrow>::copies;
}
int main() {
assert(copies_on_growth<true>() == 0);
assert(copies_on_growth<false>() == 4);
}
5.5 Scope guard
Když zdroj není objekt, pomůže stráž s lambdou. Commit/rollback: pokud vše dopadne dobře, stráž vypneš.
#include <cassert>
#include <stdexcept>
#include <utility>
#include <vector>
template <typename F>
class ScopeGuard {
F action_;
bool active_ = true;
public:
explicit ScopeGuard(F action) : action_(std::move(action)) {}
~ScopeGuard() {
if (active_) action_();
}
ScopeGuard(const ScopeGuard&) = delete;
void dismiss() { active_ = false; }
};
void add_both(std::vector<int>& v, bool fail) {
v.push_back(1);
ScopeGuard undo([&] { v.pop_back(); });
if (fail) throw std::runtime_error("selhání");
v.push_back(2);
undo.dismiss();
}
int main() {
std::vector<int> v;
try {
add_both(v, true);
} catch (const std::runtime_error&) {
}
assert(v.empty());
add_both(v, false);
assert(v.size() == 2);
}
5.6 Typické chyby
- Změna proměnných dřív, než se povede riskantní krok.
- Vyhazování z destruktoru.
try/catchvšude místo RAII.- Chybějící
noexceptna přesunu.
5.7 Kvíz
5.8 Příklady na procvičení
V cvičení 3 byl kopírovací konstruktor s try { data_ = new T[n]; ... } catch (...) { delete[] data_; throw; }, kde data_ nemá počáteční hodnotu. Co je špatně?
Řešení
Když selže new, data_ je neinicializované a delete[] pracuje s náhodnou adresou. Pomůže inicializovat data_ na nullptr, nebo dát new před try.
Jakou záruku má funkce, která nejdřív změní členskou proměnnou a pak volá něco, co může vyhodit?
Řešení
Nejvýš basic, a to jen pokud invarianty platí. Silnou záruku získáš prohozením pořadí: nejdřív riskantní krok, pak změna stavu.
5.9 Tahák
- Basic / strong / no-throw.
- Silná záruka: riskantní stranou, pak nepadající výměna.
- Destruktor nevyhazuje.
- Vektor: přesun jen při
noexcept, jinak kopie. - RAII: zdroj v objektu.
- Scope guard: rollback s
dismiss().