Čtení: 30 minHodnotí se: úlohy 1 a 2, zkouška

5 Výjimky a RAII

Po přečtení umíš
  • 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

Definice 5.2 (Tři úrovně)
  • 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:

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

5.7 Kvíz

Kvíz

1. Co říká silná záruka?
2. Co se stane, když destruktor vyhodí výjimku ven?
3. Typ má přesun bez noexcept a kopírování. Co udělá vektor při realokaci?
4. Vysvětli RAII jednou větou a dej příklad.
Ukázat řešeníŽivotnost zdroje je svázaná se životností objektu: konstruktor získá, destruktor uvolní. Příklady: unique_ptr, lock_guard.

5.8 Příklady na procvičení

Cvičení 5.3

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.

Cvičení 5.4

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

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().

Zdroje

  • ni_epc_01: záruky bezpečnosti výjimek strany 35–37
  • ni_epc_10: destruktory a výjimky strany 3–7, realokace vektoru 9–20, RAII 21–26, scope guard 27–34