Přeskočit na obsah

Výjimečné záruky bezpečnosti

Naučte se tři úrovně bezpečnosti výjimek – základní, silná a bez možnosti vyhazování – a jak je poskytují RAII a kopírování a výměna.

Tři záruky

Bezpečnost výjimek není o zabránění výjimkám – jde o definování toho, co váš kód zaručuje, když k výjimce dojde. Každá funkce, kterou napíšete, implicitně poskytuje jednu z těchto úrovní:

1. Záruka neházení (nejsilnější): Funkce nikdy nehází. Vždy se to podaří. Příklady: destruktory,swap, jednoduché getry. Označenonoexcept.

2. Silná záruka (commit-or-rollback): Pokud funkce vyvolá, stav programu je přesně takový, jaký byl před voláním. Je to, jako by funkce nebyla nikdy volána. Představte si databázovou transakci, která se buď plně potvrdí, nebo úplně vrátí zpět.

3. Základní záruka (minimum přijatelné): Pokud funkce hodí, program je v platném, ale nespecifikovaném stavu. Žádné zdroje neunikají, všechny invarianty platí, ale přesný stav se může lišit od toho, jaký byl předtím.

Existuje také čtvrtá, nepřijatelná úroveň: bez záruky — kde výjimka může nechat program v poškozeném stavu s uniklými zdroji. Výrobní kód nesmí nikdy poskytovat nižší než základní záruku.

RAII: Základ výjimečné bezpečnosti

RAII (Resource Acquisition Is Initialization) spojuje životnost zdroje s životností objektu. Konstruktor získá zdroj; destruktor ho uvolní. Protože destruktory běží během odvíjení zásobníku, RAII poskytuje základní záruku automaticky.

raii_db.cpp
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>

class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& conn_str)
        : conn_str_(conn_str), connected_(false) {
        // Simulate connection
        if (conn_str.empty()) {
            throw std::invalid_argument("empty connection string");
        }
        connected_ = true;
        std::cout << "Connected to: " << conn_str_ << '\n';
    }

    ~DatabaseConnection() {
        if (connected_) {
            // Cleanup never throws
            std::cout << "Disconnected from: " << conn_str_ << '\n';
            connected_ = false;
        }
    }

    void execute(const std::string& query) {
        if (!connected_) throw std::runtime_error("not connected");
        // Even if this function throws, the destructor
        // will still close the connection during unwinding
        std::cout << "Executing: " << query << '\n';
    }

    // Non-copyable, movable
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    DatabaseConnection(DatabaseConnection&& other) noexcept
        : conn_str_(std::move(other.conn_str_)),
          connected_(other.connected_) {
        other.connected_ = false;
    }

private:
    std::string conn_str_;
    bool connected_;
};

void process_data() {
    DatabaseConnection db("postgres://localhost/mydb");
    db.execute("SELECT * FROM users");
    // If execute() throws, ~DatabaseConnection() still runs
    // — the connection is always properly closed
}

Zkopírujte a vyměňte za silnou záruku

Styl copy-and-swap poskytuje silnou záruku výjimek pro operátory přiřazení. Myšlenka: nejprve vytvořte kopii (která může házet), poté vyměňte s kopií (která nikdy nehází). Pokud kopie hází, původní objekt je nedotčen.

copy_and_swap.cpp
#include <algorithm>
#include <utility>
#include <cstddef>

class Matrix {
public:
    Matrix(std::size_t rows, std::size_t cols)
        : rows_(rows), cols_(cols),
          data_(new double[rows * cols]{}) {}

    // Copy constructor — may throw (allocates memory)
    Matrix(const Matrix& other)
        : rows_(other.rows_), cols_(other.cols_),
          data_(new double[other.rows_ * other.cols_]) {
        std::copy(other.data_,
                  other.data_ + rows_ * cols_,
                  data_);
    }

    // Move constructor — never throws
    Matrix(Matrix&& other) noexcept
        : rows_(other.rows_), cols_(other.cols_),
          data_(other.data_) {
        other.data_ = nullptr;
        other.rows_ = other.cols_ = 0;
    }

    ~Matrix() { delete[] data_; }

    // No-throw swap — the key ingredient
    friend void swap(Matrix& a, Matrix& b) noexcept {
        using std::swap;
        swap(a.rows_, b.rows_);
        swap(a.cols_, b.cols_);
        swap(a.data_, b.data_);
    }

    // Copy-and-swap assignment: STRONG guarantee
    // Takes parameter by value (invokes copy/move ctor)
    Matrix& operator=(Matrix other) noexcept {
        swap(*this, other);
        return *this;
        // 'other' is destroyed here, freeing old data
    }

private:
    std::size_t rows_, cols_;
    double* data_;
};

noexcept a Move Operations

STL silně spoléhá nanoexceptpro konstruktory pohybu. Zvažtestd::vector::push_back: když se vektor musí přerozdělit, potřebuje přenést prvky ze starého bufferu do nového.

- Pokud je konstruktor přesunu prvkunoexcept, vektor přesune prvky (rychle).
- Pokud by konstruktor přesunu mohl hodit, vektor místo toho zkopíruje prvky (pomalé, ale bezpečné – pokud hází kopie, původní vyrovnávací paměť je stále nedotčená).

To je důvod, proč označování konstruktorů přesununoexceptnení jen dobrým zvykem – může to být 10x nebo 100x rozdíl ve výkonu pro typy uložené v kontejnerech.

Můžete to zkontrolovat v době kompilace pomocístd::is_nothrow_move_constructible_v. STL používá tuto vlastnost interně k rozhodování o své strategii.

Identifikace úrovně záruky

Při psaní funkce myslete na to, jakou záruku poskytuje. Zde je příklad všech tří úrovní v jedné třídě.

guarantee_levels.cpp
#include <vector>
#include <string>
#include <algorithm>
#include <stdexcept>

class UserRegistry {
public:
    // NO-THROW: simple getter, can never fail
    std::size_t size() const noexcept {
        return users_.size();
    }

    // STRONG GUARANTEE: either the user is added or nothing changes
    void add_user(const std::string& name) {
        if (name.empty()) {
            throw std::invalid_argument("name cannot be empty");
        }
        // If push_back throws (e.g., bad_alloc during reallocation),
        // vector guarantees the original data is intact.
        users_.push_back(name);
    }

    // BASIC GUARANTEE: if an exception occurs partway through,
    // some users may have been processed and some may not.
    // But the object is still in a valid state (no leaks, no corruption).
    void normalize_all() {
        for (auto& user : users_) {
            // std::transform could theoretically throw
            // (e.g., if user string allocation fails during tolower)
            std::transform(user.begin(), user.end(), user.begin(),
                          [](unsigned char c) { return std::tolower(c); });
        }
    }

private:
    std::vector<std::string> users_;
};
Osvědčený postup

Chcete-li konzistentně psát kód bezpečný pro výjimky, postupujte podle těchto pokynů.

  • Používejte RAII všude: Nikdy nedržte surové zdroje – zabalte je do chytrých ukazatelů nebo obalů RAII
  • Označte konstruktory přesunu a swapnoexcept: To umožňuje optimalizaci STL a poskytuje záruku bez házení
  • Před úpravou stavu proveďte veškerou práci, kterou je možné vyvolat: Nejprve alokujte, vypočítejte a ověřte – poté potvrďte pomocí operací bez házení, jako jeswap
  • Upřednostňujte idiom kopírování a výměny pro operátory přiřazení, abyste získali silnou záruku s minimálním úsilím
  • Destruktory nesmí nikdy házet: Jsou implicitněnoexceptv C++11 a novějších
Úskalí

Tyto vzory porušují výjimečné záruky bezpečnosti a vedou k jemným chybám.

  • Úprava stavu před dokončením všech vrhacích operací: Pokud hází krok 2 ze 3, krok 1 již změnil předmět
  • Zapomínáme na to, ženewmůže způsobitstd::bad_alloc: Každá dynamická alokace je potenciálním bodem vyvolání
  • Konstruktory přesunu jiné nežnoexcept: STL se tiše vrátí ke kopírování, čímž sníží výkon
  • Příliš široce zachycující výjimky:catch(...)bez opětovného házení skrývá skutečný problém
Klíčové poznatky
  • Tyto tři záruky jsou: no-throw (vždy úspěšné), silné (commit-or-rollback) a basic (platný, ale nespecifikovaný stav)
  • RAII je základ – destruktory běží během odvíjení a zajišťují, že zdroje jsou vždy uvolněny
  • Copy-and-swap poskytuje silnou záruku: nejprve proveďte veškerou práci s házením, potéswap(což jenoexcept)
  • Konstruktory přesununoexceptjsou kritické – bez nich sestd::vectorvrátí ke kopírování při přerozdělení
  • Každá funkce by měla směřovat alespoň k základní garanci; neházet tam, kde je to možné

Kvíz — Otestujte své znalosti

(20 XP)

1. Co slibuje záruka silné výjimky?

2. Proč`std::vector`kopíruje prvky místo toho, aby je přesouval, když konstruktor přesunu není`noexcept`?

3. Proč ve idiomu copy-and-swap operátor přiřazení bere svůj parametr jako hodnotu?