1. Vấn đề nó giải quyết
Một hàm mở tài nguyên rồi ném exception có thể bỏ qua câu lệnh dọn ở cuối hàm. RAII gắn trách nhiệm giải phóng với destructor, nên tài nguyên được dọn khi đối tượng quản lý nó rời phạm vi trong quá trình xử lý exception.
2. Kiến thức cần có
Biết constructor, destructor và phạm vi biến cục bộ.
Biết
throw,try,catchvà cách exception đi từ hàm được gọi tới nơi bắt lỗi.
3. Ý tưởng cốt lõi
RAII là Resource Acquisition Is Initialization: đặt quyền quản lý tài nguyên (ownership) vào một đối tượng. std::unique_ptr quản lý bộ nhớ, std::lock_guard quản lý một lần giữ khóa, còn stream tệp quản lý tệp đang mở.
Khi tìm được nơi xử lý exception, quá trình tháo các phạm vi gọi hàm (stack unwinding) hủy các đối tượng tự động đã dựng xong theo thứ tự ngược. Nếu constructor ném lỗi, destructor của chính đối tượng chưa dựng xong không chạy; các thành viên và lớp cơ sở đã dựng xong vẫn được hủy. Quy tắc hủy khi xử lý exception.
4. Cú pháp tối thiểu
auto owner = std::make_unique<Resource>();
operation_that_may_throw();Cần <memory> cùng định nghĩa Resource và hàm thao tác. Khi exception thoát phạm vi này để tới catch, owner hủy Resource mà không cần viết delete trong từng nhánh lỗi.
5. Cách nó hoạt động
Nhận tài nguyên vào đối tượng quản lý ngay sau khi tạo thành công; tránh khoảng trống mà chỉ con trỏ thô biết tài nguyên nằm đâu.
Thực hiện công việc. Khi rời phạm vi bình thường hoặc tháo ngăn xếp vì exception, destructor giải phóng tài nguyên.
Tách việc dọn tài nguyên khỏi việc bảo toàn trạng thái dữ liệu. Nếu đã tăng một bộ đếm rồi bước sau ném lỗi, RAII không tự giảm bộ đếm đó.
Chọn mức bảo đảm: cơ bản là giữ bất biến và không rò rỉ; mạnh là nếu thất bại thì trạng thái quan sát được không đổi. Muốn mức mạnh, thường cần chuẩn bị kết quả riêng rồi cập nhật bằng bước không ném lỗi.
6. Lỗi thường gặp
Cho rằng RAII tự hoàn tác mọi thay đổi dữ liệu.
Giữ tài nguyên mới cấp phát trong con trỏ thô qua một lời gọi có thể ném lỗi.
Để exception thoát destructor trong lúc đang xử lý exception khác; chương trình sẽ gọi
std::terminate.Trông chờ cleanup khi
abort,std::terminatehoặc tiến trình bị dừng cưỡng bức. Đây không phải đường thoát phạm vi thông thường.
7. Khi nào nên dùng
Dùng RAII cho tài nguyên cần ghép thao tác nhận/trả: bộ nhớ, khóa, tệp và socket. Ưu tiên kiểu quản lý có sẵn; nếu viết kiểu mới, xác định rõ quyền sở hữu và destructor không ném lỗi ra ngoài.
8. Ví dụ đơn giản
Phần chính trong mã mẫu C++20:
void work() {
const auto resource = std::make_unique<Resource>();
throw std::runtime_error{"failure"};
}Resource có destructor in released. main bắt exception rồi in caught, nên thứ tự là released trước caught. Dòng in trong destructor chỉ minh họa thứ tự; destructor thực tế vẫn phải giữ cam kết không ném lỗi.
Mã mẫu hoàn chỉnh
Tệp mã nguồn
dailycppinterview/170_raii-and-exception-safety/main.cpp
// Real-World C++ Interviews Q170: How can RAII (Resource Acquisition Is Initialization) help
// with exception safety?
// Key: RAII stores resource ownership in an object's state: construction establishes ownership
// and the destructor releases it. Because destructors of fully constructed automatic objects
// run during normal return and stack unwinding, files, locks, memory, and other resources are
// released without duplicated cleanup paths. RAII prevents leaks but does not by itself
// guarantee transactional state; strong exception guarantees may also require
// commit-after-success designs such as copy-and-swap.
#include <iostream>
#include <memory>
#include <stdexcept>
struct Resource {
~Resource() { std::cout << "released" << std::endl; }
};
void work() {
const auto resource = std::make_unique<Resource>();
throw std::runtime_error{"failure"};
}
int main() {
try {
work();
} catch (const std::exception&) {
std::cout << "caught" << std::endl;
}
}
9. Điều cần nhớ
RAII giải quyết việc ai dọn tài nguyên và khi nào dọn. Bảo đảm thao tác thất bại mà dữ liệu không đổi cần thêm thiết kế cập nhật phù hợp.
10. Câu hỏi tự kiểm tra
Đề đầy đủ: RAII giúp bảo đảm exception safety như thế nào?
Vì sao released xuất hiện trước caught trong mã mẫu? Nếu hàm đã sửa một bộ đếm trước throw, RAII có tự khôi phục nó không? Điều gì được hủy khi constructor thất bại?