Chuyển đến nội dung

Đảm bảo an toàn ngoại lệ

Tìm hiểu ba cấp độ an toàn ngoại lệ — cơ bản, mạnh mẽ và không ném — cũng như cách RAII và tính năng sao chép và trao đổi cung cấp các cấp độ đó.

Ba sự đảm bảo

An toàn ngoại lệ không phải là ngăn chặn ngoại lệ - mà là xác định mã của bạn đảm bảo khi ngoại lệ xảy ra. Mọi hàm bạn viết đều ngầm cung cấp một trong các cấp độ sau:

1. Đảm bảo không ném (mạnh nhất): Chức năng không bao giờ ném. Nó luôn thành công. Ví dụ: hàm hủy,swap, getter đơn giản. Đã đánh dấunoexcept.

2. Đảm bảo mạnh mẽ (cam kết hoặc khôi phục): Nếu hàm thực hiện, trạng thái chương trình sẽ chính xác như trước khi gọi. Như thể chức năng này chưa bao giờ được gọi. Hãy nghĩ về một giao dịch cơ sở dữ liệu được cam kết hoàn toàn hoặc khôi phục hoàn toàn.

3. Đảm bảo cơ bản (mức tối thiểu có thể chấp nhận): Nếu hàm gửi đi, chương trình sẽ ở trạng thái hợp lệ nhưng không xác định. Không có tài nguyên nào bị rò rỉ, tất cả các bất biến đều được giữ nguyên, nhưng trạng thái chính xác có thể khác so với trước đây.

Ngoài ra còn có cấp độ thứ tư, không thể chấp nhận được: không đảm bảo — khi một ngoại lệ có thể khiến chương trình ở trạng thái hỏng và tài nguyên bị rò rỉ. Mã sản xuất không bao giờ được cung cấp ít hơn mức đảm bảo cơ bản.

RAII: Nền tảng của An toàn Ngoại lệ

RAII (Thu thập tài nguyên là khởi tạo) liên kết thời gian tồn tại của tài nguyên với thời gian tồn tại của đối tượng. Người xây dựng có được tài nguyên; hàm hủy sẽ giải phóng nó. Vì các hàm hủy chạy trong quá trình giải phóng ngăn xếp nên RAII tự động cung cấp bảo đảm cơ bản.

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
}

Sao chép và trao đổi để đảm bảo chắc chắn

Thành ngữ sao chép và trao đổi cung cấp sự đảm bảo ngoại lệ mạnh mẽ cho các toán tử gán. Ý tưởng: tạo một bản sao trước (có thể ném), sau đó trao đổi với bản sao (không bao giờ ném). Nếu bản sao bị ném đi, đối tượng ban đầu sẽ không bị ảnh hưởng.

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_;
};

hoạt động không ngoại trừ và di chuyển

STL phụ thuộc rất nhiều vàonoexceptcho các hàm tạo di chuyển. Hãy xem xétstd::vector::push_back: khi vectơ phải phân bổ lại, nó cần chuyển các phần tử từ bộ đệm cũ sang bộ đệm mới.

- Nếu hàm tạo di chuyển của phần tử lànoexcept, vectơ di chuyển phần tử (nhanh).
- Nếu hàm khởi tạo di chuyển có thể ném đi, thì vectơ sao chép các phần tử thay vào đó (chậm nhưng an toàn - nếu một bản sao được ném ra, bộ đệm ban đầu vẫn còn nguyên).

Đây là lý do tại sao việc đánh dấu các hàm tạo di chuyểnnoexceptkhông chỉ là một phương pháp hay — nó có thể là chênh lệch hiệu suất gấp 10 hoặc 100 lần đối với các loại được lưu trữ trong vùng chứa.

Bạn có thể kiểm tra tại thời điểm biên dịch bằngstd::is_nothrow_move_constructible_v. STL sử dụng đặc điểm này trong nội bộ để quyết định chiến lược của mình.

Xác định mức độ đảm bảo

Khi viết một hàm, hãy nghĩ xem nó đảm bảo điều gì. Đây là ví dụ về cả ba cấp độ trong một lớp.

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_;
};
Thực hành tốt

Follow these guidelines to write exception-safe code consistently.

  • 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
  • Thực hiện tất cả công việc có thể thực hiện trước khi sửa đổi trạng thái: Phân bổ, tính toán và xác thực trước — sau đó cam kết với các thao tác không gửi nhưswap
  • Ưu tiên cách diễn đạt sao chép và trao đổi cho các toán tử gán để có được sự đảm bảo chắc chắn với nỗ lực tối thiểu
  • Các hàm hủy không bao giờ được ném: Chúng hoàn toàn lànoexcepttrong C++11 trở lên
Lỗi thường gặp

Những mẫu này phá vỡ các đảm bảo an toàn ngoại lệ và dẫn đến các lỗi tinh vi.

  • Sửa đổi trạng thái trước khi hoàn thành tất cả thao tác ném: Nếu bước 2 trong 3 lần ném, bước 1 đã thay đổi đối tượng
  • Quên rằngnewcó thể némstd::bad_alloc: Mọi phân bổ động đều là một điểm ném tiềm năng
  • Trình tạo di chuyển không phảinoexcept: STL sẽ âm thầm quay lại sao chép, làm giảm hiệu suất
  • Bắt các ngoại lệ quá rộng:catch(...)mà không cần thiết lập lại sẽ che giấu vấn đề thực sự
Điểm chính
  • Ba đảm bảo là: không ném (luôn thành công), mạnh mẽ (cam kết hoặc khôi phục) và cơ bản (trạng thái hợp lệ nhưng không xác định)
  • RAII là nền tảng - các hàm hủy chạy trong quá trình giải phóng, đảm bảo tài nguyên luôn được giải phóng
  • Sao chép và trao đổi mang lại sự đảm bảo mạnh mẽ: thực hiện tất cả công việc ném trước, sau đó làswap(lànoexcept)
  • Các hàm tạo di chuyểnnoexceptrất quan trọng - nếu không có chúng,std::vectorsẽ quay lại sao chép khi tái phân bổ
  • Mọi chức năng ít nhất phải hướng tới sự đảm bảo cơ bản; không ném nếu có thể

Câu đố — Kiểm tra kiến thức

(20 XP)

1. Đảm bảo ngoại lệ mạnh mẽ hứa hẹn điều gì?

2. Tại sao`std::vector`sao chép các phần tử thay vì di chuyển chúng khi hàm khởi tạo di chuyển không phải là`noexcept`?

3. Trong thành ngữ sao chép và hoán đổi, tại sao toán tử gán lại lấy tham số của nó theo giá trị?