Quản lý đặc tả ngoại lệ trong ngôn ngữ lập trình C++

Khi phát triển phần mềm, việc gọi các hàm từ thư viện của bên thứ ba thường đặt ra một thách thức: Làm thế nào để biết chắc chắn một hàm có ném ra ngoại lệ (exception) hay không, và nếu có thì đó là loại ngoại lệ nào? Việc hiểu rõ hành vi này là tối quan trọng để người lập trình có thể thiết kế các khối try-catch phù hợp, tránh việc chương trình bị dừng đột ngột.

Trong trường hợp không có mã nguồn và tài liệu đi kèm không đồng nhất với phiên bản thư viện, C++ cung cấp một cơ chế cú pháp để tường minh hóa các ngoại lệ mà một hàm có thể ném ra, gọi là Đặc tả ngoại lệ (Exception Specification).

Cú pháp và ý nghĩa của đặc tả ngoại lệ

Đặc tả ngoại lệ được viết ngay sau danh sách tham số của hàm. Nó đóng vai trò như một bản cam kết giữa người viết hàm và người sử dụng hàm.

// Hàm có thể ném ra bất kỳ loại ngoại lệ nào
void performAction();

// Hàm chỉ được phép ném ra ngoại lệ kiểu int hoặc double
void calculateMetrics(int value) throw(int, double);

// Hàm cam kết không ném ra bất kỳ ngoại lệ nào
void logStatus() throw();

Việc sử dụng đặc tả ngoại lệ mang lại hai giá trị chính:

  • Đối với người gọi: Biết chính xác cần phải chuẩn bị logic xử lý cho những loại lỗi nào.
  • Đối với người bảo trì: Đảm bảo không vô tình thay đổi hành vi của hàm bằng cách ném ra các ngoại lệ nằm ngoài danh sách đã công bố.

Xử lý khi vi phạm đặc tả ngoại lệ

Một vấn đề phát sinh là: Điều gì sẽ xảy ra nếu một hàm ném ra một ngoại lệ không nằm trong danh sách throw của nó? Hãy xem xét ví dụ sau:

#include <iostream>

void executeTask() throw(int) 
{
    std::cout << "Dang thuc thi executeTask..." << std::endl;
    throw 'E'; // Vi phạm: ném ra kiểu char trong khi đặc tả chỉ cho phép int
}

int main() 
{
    try 
    {
        executeTask();
    } 
    catch(int) 
    {
        std::cout << "Bat duoc ngoai le kieu int" << std::endl;
    } 
    catch(char) 
    {
        std::cout << "Bat duoc ngoai le kieu char" << std::endl;
    }

    return 0;
}

Hành vi của đoạn mã trên phụ thuộc vào trình biên dịch:

  • Với các trình biên dịch tuân thủ tiêu chuẩn C++ (như GCC hoặc Clang trong môi trường Qt), khi xảy ra vi phạm đặc tả, hàm unexpected() toàn cục sẽ được gọi. Mặc định, unexpected() sẽ gọi terminate() để dừng chương trình, ngay cả khi bạn có khối catch(char) ở bên ngoài.
  • Với Microsoft Visual Studio (MSVC), trình biên dịch thường lờ đi đặc tả ngoại lệ và vẫn nhảy vào khối catch(char).

Tùy biến hàm unexpected()

C++ cho phép chúng ta thay đổi hành vi mặc định khi một hàm vi phạm đặc tả ngoại lệ bằng cách sử dụng set_unexpected(). Điều này giúp kiểm soát lỗi tốt hơn thay vì để chương trình sụp đổ ngay lập tức.

#include <iostream>
#include <exception>
#include <cstdlib>

void customUnexpectedHandler() 
{
    std::cout << "Loi: Ham vi pham dac ta ngoai le!" << std::endl;
    // Co the ket thuc chuong trinh hoac nem lai mot ngoai le hop le
    std::exit(1);
}

void riskyFunction() throw(int) 
{
    throw "String Exception"; // Khong hop le theo dac ta
}

int main() 
{
    std::set_unexpected(customUnexpectedHandler);

    try 
    {
        riskyFunction();
    } 
    catch(...) 
    {
        std::cout << "Xu ly ngoai le chung" << std::endl;
    }

    return 0;
}

Chuyển đổi ngoại lệ trong unexpected handler

Một kỹ thuật nâng cao là ném lại một ngoại lệ đúng kiểu từ trong hàm xử lý unexpected. Điều này cho phép chương trình tiếp tục chạy nếu ngoại lệ mới khớp với đặc tả ban đầu của hàm.

#include <iostream>
#include <exception>

void rethrowHandler() 
{
    std::cout << "Chuyen doi ngoai le khong hop le thanh kieu int..." << std::endl;
    throw 99; // Nem mot int, phu hop voi dac ta cua ham goi no
}

void validateData() throw(int) 
{
    throw 'X'; // Sai dac ta
}

int main() 
{
    std::set_unexpected(rethrowHandler);

    try 
    {
        validateData();
    } 
    catch(int e) 
    {
        std::cout << "Da bat duoc ngoai le int sau khi chuyen doi: " << e << std::endl;
    }

    return 0;
}

Trong ví dụ này, khi validateData ném ra kiểu char, hệ thống gọi rethrowHandler. Tại đây, chúng ta ném ra một giá trị kiểu int. Vì int nằm trong danh sách throw(int) ban đầu, chương trình sẽ tiếp tục thực thi và rơi vào khối catch(int) trong hàm main.

Lưu ý quan trọng: Do sự khác biệt về cách triển khai giữa các trình biên dịch (đặc biệt là MSVC), người lập trình nên kiểm tra kỹ mức độ hỗ trợ đặc tả ngoại lệ trên môi trường mục tiêu trước khi áp dụng rộng rãi trong dự án.

Thẻ: C++ Exception Handling Exception Specification set_unexpected error management

Đăng vào ngày 15 tháng 7 lúc 11:16