Sari la conținut

Excepție garanții de siguranță

Aflați cele trei niveluri de siguranță pentru excepții — de bază, puternică și fără aruncare — și cum le oferă RAII și copierea și schimbarea.

Cele trei garanții

Siguranța excepțiilor nu se referă la prevenirea excepțiilor - este vorba despre definirea a ceea ce garantează codul tău atunci când apare o excepție. Fiecare funcție pe care o scrieți oferă implicit unul dintre aceste niveluri:

1. Garanție fără aruncare (cea mai puternică): Funcția nu aruncă niciodată. Întotdeauna reușește. Exemple: destructori,swap, getters simpli. Marcatnoexcept.

2. Garanție puternică (commit-or-rollback): Dacă funcția se aruncă, starea programului este exact așa cum era înainte de apel. Este ca și cum funcția nu a fost apelată niciodată. Gândiți-vă la o tranzacție de bază de date care fie se angajează complet, fie se derulează complet înapoi.

3. Garanție de bază (minim acceptabil): Dacă funcția se aruncă, programul este într-o stare validă, dar nespecificată. Nu există scurgeri de resurse, toate invarianții rămân valabile, dar starea exactă poate diferi de ceea ce era înainte.

Există, de asemenea, un al patrulea nivel, inacceptabil: nicio garanție — în care o excepție poate lăsa programul într-o stare coruptă cu resurse scurse. Codul de producție nu trebuie să ofere niciodată mai puțin decât garanția de bază.

RAII: Fundația siguranței excepțiilor

RAII (Resource Acquisition Is Initialization) leagă durata de viață a unei resurse de durata de viață a unui obiect. Constructorul dobândește resursa; destructorul îl eliberează. Deoarece destructorii rulează în timpul derulării stivei, RAII oferă automat garanția de bază.

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
}

Copiați și schimbați pentru o garanție puternică

Modul de copiere și schimb oferă o garanție puternică de excepție pentru operatorii de atribuire. Ideea: faceți mai întâi o copie (care poate arunca), apoi schimbați cu copia (care nu aruncă niciodată). Dacă copia este aruncată, obiectul original este neatins.

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 și Operațiuni de mutare

STL se bazează în mare măsură penoexceptpentru constructorii de mutare. Luați în considerarestd::vector::push_back: atunci când vectorul trebuie realocat, trebuie să transfere elemente din bufferul vechi în cel nou.

- Dacă constructorul de mutare al elementului estenoexcept, vectorul mută elemente (rapid).
- Dacă constructorul de mutare ar putea arunca, vectorul copiază elemente (lent, dar sigur - dacă o copie se aruncă, tamponul original este încă intact).

Acesta este motivul pentru care marcarea constructorilor de mutarenoexceptnu este doar o bună practică – poate fi o diferență de performanță de 10x sau 100x pentru tipurile stocate în containere.

Puteți verifica în timpul compilării custd::is_nothrow_move_constructible_v. STL folosește această trăsătură intern pentru a-și decide strategia.

Identificarea nivelului de garanție

Când scrieți o funcție, gândiți-vă la ce garanție oferă. Iată un exemplu de toate cele trei niveluri dintr-o singură clasă.

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_;
};
Bună practică

Urmați aceste instrucțiuni pentru a scrie codul sigur pentru excepții în mod consecvent.

  • Folosiți RAII peste tot: nu păstrați niciodată resursele brute - înfășurați-le în pointere inteligente sau ambalaje RAII
  • Marcați constructorii de mutare și schimbaținoexcept: acest lucru permite optimizările STL și oferă garanția fără aruncare
  • Fă toată munca care poate fi aruncată înainte de a modifica starea: mai întâi alocă, calculează și validează - apoi comite cu operațiuni fără aruncare precumswap
  • Preferați limbajul de copiere și schimbare pentru operatorii de misiuni pentru a obține garanția puternică cu efort minim
  • Destructorii nu trebuie să arunce niciodată: sunt implicitnoexceptîn C++11 și mai târziu
Capcană

Aceste modele încalcă garanțiile de siguranță excepționale și duc la erori subtile.

  • Modificarea stării înainte de a finaliza toate operațiunile de aruncare: dacă pasul 2 din 3 aruncă, pasul 1 a schimbat deja obiectul
  • Uitând cănewpoate aruncastd::bad_alloc: Fiecare alocare dinamică este un potențial punct de aruncare
  • Constructori de mutare non-noexcept: STL-ul va reveni în tăcere la copiere, distrugând performanța
  • Catching exceptions too broadly:catch(...)without rethrowing hides the real problem
Idei cheie
  • Cele trei garanții sunt: ​​fără aruncare (întotdeauna reușește), puternic (commit-or-rollback) și de bază (stare validă, dar nespecificată)
  • RAII este fundația — destructorii rulează în timpul desfășurării, asigurându-se că resursele sunt întotdeauna eliberate
  • Copierea și schimbarea oferă garanția puternică: faceți mai întâi toate lucrările de aruncare, apoiswap(care estenoexcept)
  • noexceptmove constructors are critical — without them,std::vectorfalls back to copying on reallocation
  • Fiecare funcție ar trebui să vizeze cel puțin garanția de bază; fără aruncare acolo unde este posibil

Test — Verifică-ți cunoștințele

(20 XP)

1. What does the strong exception guarantee promise?

2. De ce`std::vector`copie elemente în loc să le mute când constructorul de mutare nu este`noexcept`?

3. În limbajul copy-and-swap, de ce operatorul de atribuire își ia parametrul după valoare?