C++ `set_terminate` / `set_unexpected`:自定义异常处理终止行为

好的,各位观众老爷,欢迎来到今天的C++异常处理特别节目!今天我们要聊的是C++里两个有点神秘,但关键时刻能救你一命的函数:std::set_terminatestd::set_unexpected。 它们就像异常处理世界的“最后防线”,当你的程序遇到一些你根本没预料到的异常,或者直接崩溃边缘的时候,它们能让你优雅地(或者不那么优雅地)掌控局面。

第一幕:异常风云

首先,咱们来回顾一下C++的异常机制。简单来说,就是try-catch结构:

#include <iostream>
#include <stdexcept>

int main() {
  try {
    // 可能抛出异常的代码
    int x = 10;
    int y = 0;
    if (y == 0) {
      throw std::runtime_error("除数为0!"); // 抛出一个异常
    }
    int result = x / y;
    std::cout << "结果:" << result << std::endl;
  } catch (const std::runtime_error& e) {
    // 捕获并处理runtime_error类型的异常
    std::cerr << "运行时错误:" << e.what() << std::endl;
  } catch (...) {
    // 捕获所有其他类型的异常 (catch-all)
    std::cerr << "未知异常!" << std::endl;
  }

  std::cout << "程序继续执行..." << std::endl; // 如果异常被捕获,程序会到这里

  return 0;
}

这段代码展示了基本的try-catch。 如果try块里的代码抛出了一个std::runtime_error 类型的异常,那么第一个catch块就会接手处理。 如果抛出的是其他类型的异常,catch(...) 就会捕获它。

这就像一个异常拦截网,把那些不速之客(异常)拦下来,然后根据类型进行处理。 但是,各位有没有想过一个问题:如果这个拦截网失效了呢? 比如,程序抛出的异常,没有任何catch块能处理它,或者更糟糕,异常处理过程中又抛出了另一个异常,那会发生什么?

第二幕:Terminate 的降临

答案是:程序会调用 std::terminate() 函数。 std::terminate() 函数的默认行为是调用 std::abort(),直接粗暴地结束程序。 这就像你的程序告诉你:“我搞不定了,撂挑子不干了!” 这通常不是我们想要的,因为直接崩溃可能会导致数据丢失、资源泄漏,甚至留下一些未完成的操作,让你的程序处于一种不稳定的状态。

好消息是,我们可以使用 std::set_terminate() 函数来改变这个行为。 std::set_terminate() 允许你注册一个自定义的terminate处理函数,当 std::terminate() 被调用时,你的函数会被执行。

#include <iostream>
#include <exception>
#include <cstdlib> // For std::abort()

// 自定义的 terminate 处理函数
void myTerminateHandler() {
  std::cerr << "程序即将终止! 正在进行一些清理工作..." << std::endl;

  // 可以在这里做一些清理工作,比如释放资源、记录日志等
  // ...

  std::cerr << "清理完成,程序准备退出。" << std::endl;

  // 通常情况下,你仍然需要调用 abort() 或 exit() 来真正结束程序
  std::abort(); //或者 std::exit(1);
}

int main() {
  // 注册自定义的 terminate 处理函数
  std::set_terminate(myTerminateHandler);

  try {
    // 故意抛出一个未被捕获的异常
    throw 123; // 抛出一个 int 类型的异常,没有 catch 块处理它
  } catch (const std::exception& e) {
    std::cerr << "捕获到异常:" << e.what() << std::endl;
  }

  std::cout << "程序继续执行..." << std::endl; // 永远不会执行到这里

  return 0;
}

在这个例子中,我们定义了一个名为 myTerminateHandler() 的函数,它会在程序终止前执行一些清理工作,比如打印一些错误信息。然后,我们使用 std::set_terminate(myTerminateHandler) 将这个函数注册为terminate处理函数。

现在,当程序抛出一个未被捕获的 int 类型的异常时,程序不会直接崩溃,而是会先调用 myTerminateHandler(),执行清理工作,然后再调用 std::abort() 结束程序。

Terminate 的使用场景

  • 资源清理: 在terminate处理函数中,你可以释放一些关键的资源,比如关闭文件、释放内存等,避免资源泄漏。
  • 日志记录: 你可以在terminate处理函数中记录一些错误信息,方便你调试程序。
  • 发送警报: 如果你的程序运行在服务器上,你可以在terminate处理函数中发送警报,通知管理员程序崩溃了。

第三幕:Unexpected 的逆袭

接下来,我们来聊聊 std::set_unexpected()。 这个函数和 std::set_terminate() 有点类似,但它的应用场景更加特殊:它主要用于处理异常说明 (exception specification) 违反的情况。

什么是异常说明呢? 简单来说,就是你可以在函数声明时指定这个函数可能抛出的异常类型。例如:

void foo() throw (std::runtime_error); // foo 函数可能会抛出 std::runtime_error 类型的异常
void bar() throw (); // bar 函数保证不会抛出任何异常

throw (std::runtime_error) 表示 foo 函数可能会抛出 std::runtime_error 类型的异常。 throw () 表示 bar 函数保证不会抛出任何异常。

如果一个函数违反了它的异常说明,比如一个声明为 throw () 的函数实际上抛出了一个异常,那么 std::unexpected() 函数就会被调用。 std::unexpected() 的默认行为是调用 std::terminate(),也就是直接结束程序。

同样,我们可以使用 std::set_unexpected() 来改变 std::unexpected() 的行为。 std::set_unexpected() 允许你注册一个自定义的 unexpected 处理函数,当 std::unexpected() 被调用时,你的函数会被执行。

#include <iostream>
#include <exception>
#include <cstdlib> // For std::abort()

// 自定义的 unexpected 处理函数
void myUnexpectedHandler() {
  std::cerr << "违反了异常说明!正在尝试处理..." << std::endl;

  // 在这里可以尝试将异常转换为符合异常说明的异常
  // 或者直接抛出一个 std::bad_exception 类型的异常
  throw std::bad_exception();
}

void foo() throw () { // 承诺不抛出任何异常
  std::cout << "foo 函数开始执行..." << std::endl;
  // 故意抛出一个异常,违反异常说明
  throw 123; // 抛出一个 int 类型的异常,违反了 throw () 的承诺
  std::cout << "foo 函数执行结束。" << std::endl; // 不会执行到这里
}

int main() {
  // 注册自定义的 unexpected 处理函数
  std::set_unexpected(myUnexpectedHandler);
  std::set_terminate(myTerminateHandler); // 同时设置terminate handler

  try {
    foo();
  } catch (const std::bad_exception& e) {
    std::cerr << "捕获到 std::bad_exception 异常:" << e.what() << std::endl;
  } catch (...) {
    std::cerr << "捕获到未知异常!" << std::endl;
  }

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

  return 0;
}

void myTerminateHandler() {
    std::cerr << "Terminate handler called!" << std::endl;
    std::abort();
}

在这个例子中,我们定义了一个名为 myUnexpectedHandler() 的函数,它会在违反异常说明时尝试进行处理。 我们使用 std::set_unexpected(myUnexpectedHandler) 将这个函数注册为 unexpected 处理函数。

foo 函数声明为 throw (),承诺不抛出任何异常,但实际上它抛出了一个 int 类型的异常。 这违反了异常说明,所以 std::unexpected() 会被调用,进而调用我们的 myUnexpectedHandler()

myUnexpectedHandler() 中,我们抛出了一个 std::bad_exception 类型的异常。 这个异常符合异常说明,所以程序可以继续执行,并在 main 函数中被 catch (const std::bad_exception& e) 捕获。

Unexpected 的使用场景

  • 兼容性: 在一些老代码中,异常说明可能被滥用,或者不准确。 使用 std::set_unexpected() 可以让你在不修改老代码的情况下,处理违反异常说明的情况。
  • 安全性: 如果你的程序对异常的安全性要求很高,你可以使用 std::set_unexpected() 来确保所有的异常都符合预期。

第四幕:Terminate vs Unexpected:傻傻分不清楚?

很多同学可能会问:std::set_terminate()std::set_unexpected() 有什么区别呢? 它们都用于处理异常,但应用场景不同:

特性 std::set_terminate() std::set_unexpected()
触发条件 1. 异常没有被任何 catch 块捕获。2. 异常处理过程中又抛出了另一个异常。 函数违反了它的异常说明。
默认行为 调用 std::abort() 直接结束程序。 调用 std::terminate(),进而调用 std::abort()
主要用途 处理程序无法恢复的异常情况,进行最后的清理工作。 处理违反异常说明的情况,尝试进行转换或抛出 std::bad_exception
关注点 程序的整体稳定性,避免资源泄漏。 异常的安全性,确保异常符合预期。

简单来说,std::set_terminate() 是最后的防线,当异常处理完全失效时才会触发。 std::set_unexpected() 则是在异常处理的早期阶段,当函数违反异常说明时触发。

第五幕:实战演练

让我们来看一个更复杂的例子,结合 std::set_terminate()std::set_unexpected() 来处理各种异常情况:

#include <iostream>
#include <exception>
#include <cstdlib> // For std::abort()

// 自定义的 terminate 处理函数
void myTerminateHandler() {
  std::cerr << "程序即将终止!正在进行一些关键清理工作..." << std::endl;
  // 记录关键日志
  std::cerr << "严重错误:程序遇到了无法处理的异常,即将终止。" << std::endl;
  std::abort();
}

// 自定义的 unexpected 处理函数
void myUnexpectedHandler() {
  std::cerr << "违反了异常说明!正在尝试处理..." << std::endl;
  try {
    throw std::bad_exception(); // 将异常转换为 std::bad_exception
  } catch (const std::bad_exception& e) {
    std::cerr << "成功转换为 std::bad_exception。" << std::endl;
    throw; // 重新抛出异常,让 catch 块处理
  } catch (...) {
    std::cerr << "转换失败,抛出原始异常。" << std::endl;
    throw; // 重新抛出原始异常
  }
}

class MyException : public std::exception {
public:
  const char* what() const noexcept override {
    return "自定义异常!";
  }
};

void foo() throw (MyException) { // 声明可能抛出 MyException 类型的异常
  std::cout << "foo 函数开始执行..." << std::endl;
  throw MyException(); // 抛出一个 MyException 类型的异常,符合异常说明
  std::cout << "foo 函数执行结束。" << std::endl; // 不会执行到这里
}

void bar() throw () { // 承诺不抛出任何异常
  std::cout << "bar 函数开始执行..." << std::endl;
  throw 123; // 抛出一个 int 类型的异常,违反了 throw () 的承诺
  std::cout << "bar 函数执行结束。" << std::endl; // 不会执行到这里
}

int main() {
  // 注册自定义的 terminate 和 unexpected 处理函数
  std::set_terminate(myTerminateHandler);
  std::set_unexpected(myUnexpectedHandler);

  try {
    foo();
  } catch (const MyException& e) {
    std::cerr << "捕获到 MyException 异常:" << e.what() << std::endl;
  } catch (const std::bad_exception& e) {
    std::cerr << "捕获到 std::bad_exception 异常:" << e.what() << std::endl;
  } catch (...) {
    std::cerr << "捕获到未知异常!" << std::endl;
  }

  std::cout << "foo 函数处理完成。" << std::endl;

  try {
    bar();
  } catch (const std::bad_exception& e) {
    std::cerr << "捕获到 std::bad_exception 异常:" << e.what() << std::endl;
  } catch (...) {
    std::cerr << "捕获到未知异常!" << std::endl;
  }

  std::cout << "bar 函数处理完成。" << std::endl;

  return 0;
}

在这个例子中,我们定义了一个自定义的异常类型 MyException,并创建了两个函数 foobarfoo 函数抛出一个符合异常说明的 MyException 类型的异常,bar 函数抛出一个违反异常说明的 int 类型的异常。

main 函数中,我们分别调用 foobar,并使用 try-catch 块来处理可能抛出的异常。 当 bar 函数抛出 int 类型的异常时,std::unexpected() 会被调用,进而调用我们的 myUnexpectedHandler()。 在 myUnexpectedHandler() 中,我们将 int 类型的异常转换为 std::bad_exception 类型的异常,并重新抛出。 这个 std::bad_exception 类型的异常最终会被 main 函数中的 catch (const std::bad_exception& e) 捕获。

第六幕:注意事项

  • 线程安全: std::set_terminate()std::set_unexpected() 设置的处理函数是全局的,所以需要注意线程安全问题。 如果你的程序是多线程的,你需要使用锁来保护这些函数的调用。
  • 只设置一次: std::set_terminate()std::set_unexpected() 只能设置一次处理函数。 如果多次调用这些函数,只有最后一次设置的处理函数会生效。
  • 不要抛出异常: 在 terminate 和 unexpected 处理函数中,尽量不要抛出异常。 如果抛出异常,程序会直接调用 std::terminate(),进入无限循环。
  • 谨慎使用: std::set_terminate()std::set_unexpected() 应该谨慎使用。 通常情况下,你应该尽可能地使用 try-catch 块来处理异常,而不是依赖这些最后的防线。

总结

std::set_terminate()std::set_unexpected() 是C++异常处理机制中非常重要的组成部分。 它们可以让你在程序遇到无法处理的异常或者违反异常说明的情况下,进行一些清理工作,或者尝试进行恢复。 但是,它们也应该谨慎使用,因为它们可能会掩盖一些潜在的问题。

希望今天的节目能帮助大家更好地理解C++的异常处理机制,并在实际开发中更加游刃有余。 感谢大家的收看,我们下期再见!

发表回复

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