Chuyển tới nội dung chính

Exception Safety: Đảm bảo an toàn khi có ngoại lệ

Trong C++, khi chương trình gặp lỗi và ném ngoại lệ (throw), nếu không xử lý đúng cách, chương trình có thể rơi vào trạng thái không xác định, làm rò rỉ tài nguyên (memory leak), hoặc làm sai logic. Vì vậy, ta cần viết code có đảm bảo an toàn ngoại lệ (exception safety).

Ba mức độ an toàn ngoại lệ

1. Basic Exception Safety – An toàn cơ bản

  • Khi có ngoại lệ xảy ra, không có tài nguyên nào bị rò rỉ.
  • Đối tượng vẫn còn "hợp lệ", nhưng trạng thái bên trong có thể thay đổi (không còn đúng như trước).
  • Ví dụ:
    void push(int value) {
    data.push_back(value); // nếu throw, data vẫn tồn tại, chỉ là có thể chưa push được
    }

2. Strong Exception Safety – An toàn mạnh

  • Nếu có lỗi xảy ra, đối tượng không bị thay đổi gì hết.
  • Toàn bộ thao tác được xem như "giao dịch" (transaction): thành công thì thay đổi, thất bại thì như chưa từng làm gì.
  • Cách đạt được thường là: copy dữ liệu tạm ra, rồi nếu thành công thì mới cập nhật.
  • Ví dụ:
    void update(std::vector<int>& data, int value) {
    auto temp = data; // sao chép ra bản sao
    temp.push_back(value); // thao tác trên bản sao
    data = std::move(temp); // thành công mới gán lại
    }

3. No-throw Guarantee – Cam kết không ném ngoại lệ

  • Hàm không bao giờ ném ngoại lệ, bất kể điều gì xảy ra.
  • Đây là mức độ an toàn cao nhất và rất quan trọng trong destructor, vì ~Destructor không được phép ném ngoại lệ (nếu có, chương trình có thể terminate() ngay).
  • Ví dụ:
    ~MyFile() noexcept {
    if (file) std::fclose(file); // đảm bảo không throw
    }

Ví dụ minh họa trong constructor, destructor, assignment

Constructor – cần đảm bảo strong safety

class Resource {
std::vector<int> data;
public:
Resource() {
// Nếu push_back ném ngoại lệ, không sao vì object chưa được tạo hoàn toàn
data.push_back(1);
}
};

Destructor – nên đảm bảo no-throw

class MyClass {
FILE* file;
public:
~MyClass() noexcept {
if (file) std::fclose(file); // không ném lỗi trong destructor
}
};

Assignment – cần đảm bảo strong safety

MyClass& operator=(const MyClass& other) {
if (this != &other) {
MyClass temp(other); // nếu thất bại, this không thay đổi
swap(temp); // strong guarantee
}
return *this;
}

RAII và chiến lược tránh rò rỉ tài nguyên

RAII (Resource Acquisition Is Initialization) là cách lập trình giúp tự động quản lý tài nguyên như bộ nhớ, file, mutex, socket... bằng cách bọc nó trong một object.

  • Constructor: lấy tài nguyên.
  • Destructor: giải phóng tài nguyên.

Ví dụ RAII với std::unique_ptr:

void process() {
std::unique_ptr<int[]> arr(new int[100]);
// nếu có lỗi xảy ra ở đây, arr sẽ tự hủy
}

Kết luận

  • Code tốt không chỉ đúng khi mọi thứ suôn sẻ, mà còn cần an toàn khi lỗi xảy ra.
  • Hãy đảm bảo:
    • Destructor không ném lỗi.
    • Dùng RAII để tự động dọn dẹp tài nguyên.
    • Với assignment hoặc các thao tác cập nhật: nếu không chắc, hãy sao chép trước rồi gán sau.

Bạn có thể bắt đầu đơn giản bằng cách đặt mục tiêu: ít nhất nên đạt "basic safety", còn nếu được thì "strong". Khi cần viết code thực sự an toàn và tối ưu – hãy dùng RAIInoexcept đúng chỗ.