Μετάβαση στο περιεχόμενο

Εξαίρεση Εγγυήσεις Ασφαλείας

Μάθετε τα τρία επίπεδα ασφάλειας εξαίρεσης — βασικό, ισχυρό και μη ρίψη — και πώς τα παρέχουν το RAII και το copy-and-swap.

Οι Τρεις Εγγυήσεις

Η ασφάλεια των εξαιρέσεων δεν αφορά την αποτροπή εξαιρέσεων — πρόκειται για τον καθορισμό του τι εγγυάται ο κώδικάς σας όταν συμβαίνει μια εξαίρεση. Κάθε συνάρτηση που γράφετε παρέχει έμμεσα ένα από αυτά τα επίπεδα:

1. Εγγύηση μη ρίψης (πιο ισχυρή): Η συνάρτηση δεν εκτοξεύει ποτέ. Πάντα πετυχαίνει. Παραδείγματα: καταστροφείς,swap, απλοί λήπτες. Με την ένδειξηnoexcept.

2. Ισχυρή εγγύηση (commit-or-rollback): Εάν η συνάρτηση ρίξει, η κατάσταση του προγράμματος είναι ακριβώς όπως ήταν πριν από την κλήση. Είναι σαν να μην κλήθηκε ποτέ η συνάρτηση. Σκεφτείτε μια συναλλαγή βάσης δεδομένων που είτε δεσμεύεται πλήρως είτε επαναφέρει πλήρως.

3. Βασική εγγύηση (ελάχιστη αποδεκτή): Εάν η συνάρτηση ρίξει, το πρόγραμμα βρίσκεται σε έγκυρη αλλά απροσδιόριστη κατάσταση. Δεν υπάρχουν διαρροές πόρων, διατηρούνται όλα τα αμετάβλητα, αλλά η ακριβής κατάσταση μπορεί να διαφέρει από αυτή που ήταν πριν.

Υπάρχει επίσης ένα τέταρτο, απαράδεκτο επίπεδο: καμία εγγύηση — όπου μια εξαίρεση μπορεί να αφήσει το πρόγραμμα σε κατεστραμμένη κατάσταση με διαρροές πόρων. Ο κωδικός παραγωγής δεν πρέπει ποτέ να παρέχει λιγότερο από τη βασική εγγύηση.

RAII: The Foundation of Exception Safety

Το RAII (Resource Acquisition Is Initialization) δεσμεύει τη διάρκεια ζωής ενός πόρου με τη διάρκεια ζωής ενός αντικειμένου. Ο κατασκευαστής αποκτά τον πόρο. ο καταστροφέας το απελευθερώνει. Εφόσον οι καταστροφείς λειτουργούν κατά το ξετύλιγμα της στοίβας, το RAII παρέχει τη βασική εγγύηση αυτόματα.

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
}

Αντιγραφή και Ανταλλαγή για την Ισχυρή Εγγύηση

Το ιδίωμα αντιγραφής και ανταλλαγής παρέχει την ισχυρή εγγύηση εξαίρεσης για τους χειριστές εκχώρησης. Η ιδέα: δημιουργήστε πρώτα ένα αντίγραφο (που μπορεί να πετάξει), μετά αλλάξτε το με το αντίγραφο (που ποτέ δεν πετάει). Εάν το αντίγραφο πετάξει, το αρχικό αντικείμενο παραμένει ανέγγιχτο.

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 και Λειτουργίες Μετακίνησης

The STL relies heavily onnoexceptfor move constructors. Considerstd::vector::push_back: when the vector must reallocate, it needs to transfer elements from the old buffer to the new one.

- If the element's move constructor isnoexcept, the vector moves elements (fast).
- If the move constructor might throw, the vector copies elements instead (slow, but safe — if a copy throws, the original buffer is still intact).

This is why marking move constructorsnoexceptis not just good practice — it can be a 10x or 100x performance difference for types stored in containers.

You can check at compile time withstd::is_nothrow_move_constructible_v. The STL uses this trait internally to decide its strategy.

Identifying the Guarantee Level

Όταν γράφετε μια συνάρτηση, σκεφτείτε ποια εγγύηση παρέχει. Ακολουθεί ένα παράδειγμα και των τριών επιπέδων σε μία τάξη.

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_;
};
Βέλτιστη πρακτική

Ακολουθήστε αυτές τις οδηγίες για να γράψετε με συνέπεια κώδικα που είναι ασφαλής για εξαιρέσεις.

  • Use RAII everywhere: Never hold raw resources — wrap them in smart pointers or RAII wrappers
  • Mark move constructors and swapnoexcept: This enables STL optimizations and provides the no-throw guarantee
  • Do all the work that can throw before modifying state: Allocate, compute, and validate first — then commit with no-throw operations likeswap
  • Prefer the copy-and-swap idiom for assignment operators to get the strong guarantee with minimal effort
  • Destructors must never throw: They are implicitlynoexceptin C++11 and later
Παγίδα

Αυτά τα μοτίβα παραβιάζουν τις εγγυήσεις ασφαλείας και οδηγούν σε ανεπαίσθητα σφάλματα.

  • Τροποποίηση κατάστασης πριν από την ολοκλήρωση όλων των λειτουργιών ρίψης: Εάν το βήμα 2 από τις 3 ρίψεις, το βήμα 1 έχει ήδη αλλάξει το αντικείμενο
  • Ξεχνώντας ότι τοnewμπορεί να ρίξει τοstd::bad_alloc: Κάθε δυναμική κατανομή είναι ένα πιθανό σημείο εκτόξευσης
  • Κατασκευαστές κινήσεων που δεν είναιnoexcept: Το STL θα επιστρέψει σιωπηλά στην αντιγραφή, μειώνοντας την απόδοση
  • Πολύ ευρεία σύλληψη εξαιρέσεων: Τοcatch(...)χωρίς επαναφορά κρύβει το πραγματικό πρόβλημα
Βασικά συμπεράσματα
  • Οι τρεις εγγυήσεις είναι: μη ρίψη (πάντα πετυχαίνει), ισχυρή (δέσμευση ή επαναφορά) και βασική (έγκυρη αλλά απροσδιόριστη κατάσταση)
  • Το RAII είναι το θεμέλιο — οι καταστροφείς λειτουργούν κατά την εκτύλιξη, διασφαλίζοντας ότι οι πόροι είναι πάντα ελεύθεροι
  • Η αντιγραφή και η εναλλαγή παρέχει την ισχυρή εγγύηση: κάντε πρώτα όλες τις εργασίες ρίψης και μετάswap(που είναιnoexcept)
  • Οι κατασκευαστές κινήσεωνnoexceptείναι κρίσιμοι — χωρίς αυτούς, τοstd::vectorεπιστρέφει στην αντιγραφή κατά την ανακατανομή
  • Κάθε λειτουργία πρέπει να στοχεύει τουλάχιστον στη βασική εγγύηση. μη ρίψη όπου είναι δυνατόν

Κουίζ — Δοκιμάστε τις γνώσεις σας

(20 XP)

1. Τι υπόσχεται η ισχυρή εξαίρεση εγγύηση;

2. Γιατί το`std::vector`αντιγράφει στοιχεία αντί να τα μετακινεί όταν ο κατασκευαστής μετακίνησης δεν είναι`noexcept`;

3. Στο ιδίωμα copy-and-swap, γιατί ο τελεστής εκχώρησης παίρνει την παράμετρό του ανά τιμή;