C++ `std::terminate` 与 `std::uncaught_exceptions`:理解异常处理的边界

好的,各位观众老爷们,欢迎来到今天的“异常处理大冒险”讲座!今天我们要聊聊C++异常处理中两个比较“边缘”的角色:std::terminatestd::uncaught_exceptions。别害怕,这俩哥们儿虽然听起来有点吓人,但理解了它们的脾气,就能更好地掌控你的程序,避免它突然暴毙。

第一幕:std::terminate——程序终结者

std::terminate,顾名思义,就是“终结”的意思。它是个狠角色,一旦被调用,你的程序基本上就宣告完蛋了,不死也得脱层皮。

它什么时候会出场呢?

简单来说,当C++的异常处理机制无法继续处理异常时,std::terminate就会被调用。这通常发生在以下几种情况:

  1. 未捕获的异常逃逸了线程边界: 线程里抛出了异常,但是没有被try...catch块捕获,最终逃逸出了线程函数,这会导致 std::terminate 被调用。

    #include <iostream>
    #include <thread>
    #include <stdexcept>
    
    void thread_func() {
        throw std::runtime_error("Thread exception!");
    }
    
    int main() {
        try {
            std::thread t(thread_func);
            t.join();
        } catch (const std::exception& e) {
            std::cerr << "Caught exception in main: " << e.what() << std::endl;
        }
    
        std::cout << "程序继续执行..." << std::endl; // 这行代码不会被执行
        return 0;
    }

    在这个例子中,thread_func抛出了异常,但是main函数中的try...catch块无法捕获它,因为它是在另一个线程中抛出的。 这会直接调用 std::terminate 结束程序,甚至连 cout 都没机会执行。

  2. 异常处理期间又抛出了异常:catch块中处理异常时,如果又抛出了新的异常,且没有被进一步捕获,std::terminate 也会被调用。这就像是火上浇油,异常处理机制直接崩溃。

    #include <iostream>
    #include <stdexcept>
    
    int main() {
        try {
            throw std::runtime_error("Initial exception!");
        } catch (const std::exception& e) {
            std::cerr << "Caught exception: " << e.what() << std::endl;
            throw std::logic_error("Exception during exception handling!"); // 再次抛出异常
        }
    
        std::cout << "程序继续执行..." << std::endl; // 这行代码不会被执行
        return 0;
    }

    这里,第一个异常被捕获了,但是在catch块中又抛出了一个新的异常。由于没有进一步的catch块来处理这个新的异常,程序会调用 std::terminate 并终止。

  3. 析构函数抛出异常: 在C++中,析构函数的设计原则是不应该抛出异常。如果析构函数抛出了异常,且该异常没有在析构函数内部被捕获,那么程序会调用std::terminate。这是因为在异常处理的过程中,析构函数可能会被调用,如果在析构函数中又抛出异常,会导致程序的状态变得非常不稳定。

    #include <iostream>
    #include <stdexcept>
    
    class MyClass {
    public:
        ~MyClass() {
            std::cerr << "Destructor called." << std::endl;
            throw std::runtime_error("Exception in destructor!");
        }
    };
    
    int main() {
        try {
            MyClass obj;
            throw std::runtime_error("Initial exception!");
        } catch (const std::exception& e) {
            std::cerr << "Caught exception: " << e.what() << std::endl;
        }
    
        std::cout << "程序继续执行..." << std::endl; // 这行代码不会被执行
        return 0;
    }

    在这个例子中,main函数抛出一个异常,在异常处理的过程中,MyClass的对象obj的析构函数会被调用。由于析构函数也抛出了一个异常,程序会调用 std::terminate 并终止。

  4. noexcept函数抛出异常: 如果一个函数被标记为noexcept,这意味着它承诺不会抛出任何异常。如果在noexcept函数中抛出了异常,程序会立即调用std::terminate。这是因为noexcept函数被设计用来保证某些操作的原子性和安全性,抛出异常会破坏这些保证。

    #include <iostream>
    #include <stdexcept>
    
    void noexcept_func() noexcept {
        std::cerr << "noexcept_func called." << std::endl;
        throw std::runtime_error("Exception in noexcept function!");
    }
    
    int main() {
        try {
            noexcept_func();
        } catch (const std::exception& e) {
            std::cerr << "Caught exception: " << e.what() << std::endl; // 这行代码不会被执行
        }
    
        std::cout << "程序继续执行..." << std::endl; // 这行代码不会被执行
        return 0;
    }

    这里,noexcept_func被声明为noexcept,但是它却抛出了一个异常。这会导致程序立即调用 std::terminate 并终止,catch块中的代码不会被执行。

std::terminate 默认行为

默认情况下,std::terminate 会调用 std::abort,后者会粗暴地终止程序,通常会产生一个core dump。

如何自定义 std::terminate 的行为?

虽然 std::terminate 很粗暴,但我们还是可以稍微驯服它,让它在临死前做一些有用的事情,例如记录日志、清理资源等。我们可以使用 std::set_terminate 函数来设置自己的terminate handler。

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

void my_terminate_handler() {
    std::cerr << "自定义的 terminate handler 被调用了!" << std::endl;
    // 在这里可以进行一些清理工作,例如记录日志
    std::abort(); // 最后还是得终止程序
}

int main() {
    std::set_terminate(my_terminate_handler);

    try {
        throw std::runtime_error("抛出一个异常,触发 terminate");
    } catch (...) {
        std::cerr << "异常被捕获了,但是我们故意不处理它" << std::endl;
        throw; // 重新抛出异常,导致 terminate 被调用
    }

    return 0; // 这行代码不会被执行
}

在这个例子中,我们设置了一个自定义的terminate handler my_terminate_handler。当程序因为未捕获的异常而调用 std::terminate 时,my_terminate_handler 会被调用,它会打印一条消息到标准错误输出,并最终调用 std::abort 终止程序。

第二幕:std::uncaught_exceptions——异常计数器

std::uncaught_exceptions 是一个函数,它返回当前未捕获的异常的数量。 听起来有点抽象,但它在某些特定的场景下非常有用。

它有什么用?

主要用于判断当前是否处于异常处理的过程中。 特别是当你在析构函数中需要执行一些清理工作时,但又不想在异常处理过程中抛出新的异常(因为这会导致 std::terminate 被调用),std::uncaught_exceptions 就能派上用场。

怎么用呢?

#include <iostream>
#include <stdexcept>
#include <exception>

class Resource {
public:
    Resource() {
        std::cout << "Resource acquired." << std::endl;
        resource_ = new int[100]; // 分配一些内存
    }

    ~Resource() {
        std::cout << "Resource destructor called." << std::endl;
        if (resource_ != nullptr) {
            if (std::uncaught_exceptions() == 0) {
                // 只有在没有未捕获的异常时才清理资源
                std::cout << "Releasing resource..." << std::endl;
                delete[] resource_;
                resource_ = nullptr;
            } else {
                std::cerr << "Skipping resource release due to active exception." << std::endl;
            }
        }
    }

private:
    int* resource_ = nullptr;
};

int main() {
    try {
        Resource res;
        throw std::runtime_error("Something went wrong!");
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    std::cout << "程序继续执行..." << std::endl;
    return 0;
}

在这个例子中,Resource 类在构造函数中分配了一些内存,并在析构函数中释放这些内存。 但是,如果在构造函数之后,析构函数之前抛出了异常,那么在析构函数中释放内存可能会导致问题。 为了避免这种情况,我们使用 std::uncaught_exceptions() 来检查当前是否有未捕获的异常。 如果 std::uncaught_exceptions() 返回 0,表示没有未捕获的异常,我们可以安全地释放资源。 如果 std::uncaught_exceptions() 返回一个大于 0 的值,表示当前有未捕获的异常,我们应该避免释放资源,以防止程序崩溃。

为什么要这样做?

因为在异常处理过程中,如果析构函数再次抛出异常,会导致程序调用 std::terminate。 使用 std::uncaught_exceptions 可以避免在异常处理过程中抛出新的异常,从而保证程序的稳定性。 虽然这个例子比较简单,但在更复杂的场景下,例如在处理文件、网络连接等资源时,std::uncaught_exceptions 就显得非常重要。

std::uncaught_exceptions 的返回值

返回值 含义
0 没有未捕获的异常。这意味着程序当前不在处理异常的过程中。
> 0 存在未捕获的异常。返回值表示当前未捕获的异常的数量。这意味着程序当前正在处理异常的过程中,或者有异常正在等待被处理。

std::uncaught_exceptionsstd::current_exception 的区别

std::uncaught_exceptions 返回的是未捕获的异常的数量,而 std::current_exception 返回的是当前正在处理的异常的 std::exception_ptrstd::current_exception 只能在 catch 块中使用,而 std::uncaught_exceptions 可以在任何地方使用。 std::uncaught_exceptions 主要用于判断是否处于异常处理的过程中,而 std::current_exception 主要用于获取当前正在处理的异常的信息。

第三幕:实战演练——一个更复杂的例子

让我们来看一个更复杂的例子,它结合了 std::terminatestd::uncaught_exceptions,展示了它们在实际开发中的应用。

#include <iostream>
#include <stdexcept>
#include <exception>
#include <fstream>

class Logger {
public:
    Logger(const std::string& filename) : filename_(filename) {
        file_.open(filename_, std::ios::app);
        if (!file_.is_open()) {
            throw std::runtime_error("Failed to open log file.");
        }
    }

    ~Logger() {
        if (file_.is_open()) {
            if (std::uncaught_exceptions() == 0) {
                file_ << "Logger closed normally." << std::endl;
                file_.close();
            } else {
                file_ << "Logger closed due to uncaught exception." << std::endl;
                file_.close();
            }
        }
    }

    void log(const std::string& message) {
        file_ << message << std::endl;
    }

private:
    std::string filename_;
    std::ofstream file_;
};

void my_terminate_handler() {
    std::cerr << "Custom terminate handler called!" << std::endl;
    // 尝试记录错误信息到日志文件
    try {
        Logger logger("error.log"); // 这里可能会抛出异常
        logger.log("Program terminated unexpectedly due to uncaught exception.");
    } catch (const std::exception& e) {
        std::cerr << "Failed to log error message: " << e.what() << std::endl;
    }
    std::abort();
}

int main() {
    std::set_terminate(my_terminate_handler);

    try {
        Logger logger("app.log");
        logger.log("Program started.");

        // 模拟一些可能抛出异常的操作
        int* arr = new int[10];
        delete[] arr;
        arr[10] = 42; // 越界访问,会导致程序崩溃,抛出异常

        logger.log("Program finished successfully.");
    } catch (const std::exception& e) {
        std::cerr << "Caught exception in main: " << e.what() << std::endl;
    }

    std::cout << "程序继续执行..." << std::endl; // 这行代码通常不会被执行
    return 0;
}

在这个例子中,我们有一个 Logger 类,用于将日志信息写入文件。 Logger 的析构函数使用 std::uncaught_exceptions() 来判断程序是否因为未捕获的异常而终止。 如果是,它会记录一条特殊的消息到日志文件中。 我们还设置了一个自定义的 terminate handler my_terminate_handler,它会在程序因为未捕获的异常而终止时被调用。 my_terminate_handler 尝试将错误信息记录到另一个日志文件 "error.log" 中。 注意,在 my_terminate_handler 中,我们使用了 try...catch 块来捕获可能在日志记录过程中抛出的异常,以防止 std::terminate 被再次调用。

总结

  • std::terminate 是程序最后的救命稻草(或者说是绞刑架),它会在异常处理机制无法继续处理异常时被调用。
  • std::uncaught_exceptions 用于判断当前是否处于异常处理的过程中,特别是在析构函数中,可以避免在异常处理过程中抛出新的异常。
  • 合理使用 std::terminatestd::uncaught_exceptions 可以提高程序的健壮性和可靠性,避免程序意外崩溃。

一些建议

  • 尽量避免在析构函数中抛出异常。如果必须抛出异常,请确保在析构函数内部捕获它。
  • 使用 noexcept 关键字来标记不会抛出异常的函数。
  • 编写健壮的异常处理代码,确保所有可能抛出异常的代码都被 try...catch 块包围。
  • 在自定义的 terminate handler 中,尽量避免执行复杂的操作,以防止程序崩溃。

希望今天的讲座能帮助大家更好地理解 std::terminatestd::uncaught_exceptions,并在实际开发中灵活运用它们。 记住,异常处理是C++编程中非常重要的一部分,掌握它能让你的程序更加健壮和可靠。 感谢大家的观看,我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注