您应该在什么时候创建自己的异常类型

When should you create your own exception type?

本文关键字:自己的 异常 类型 创建 什么时候      更新时间:2023-10-16

我正在进行一个项目,我们正在将旧的C代码重组为新的C++,为了处理错误,我们使用了异常。

我们正在为不同的模块创建不同的异常类型。

我认为这不值得,但我没有任何有效的论据来证明我的观点。因此,如果我们编写了标准库,您将看到vector_exception、list_exception等

在思考的时候,我偶然发现了这个问题:

什么时候应该创建自己的异常类型,什么时候应该坚持std库中已经创建的异常?

此外,如果采用上述方法,我们在不久的将来可能会面临什么问题?

在以下情况下创建自己的异常类型:

  1. 您可能希望在处理时区分它们。如果它们是不同的类型,则可以选择编写不同的catch子句。它们仍然应该有一个共同的基础,这样你就可以在适当的时候进行共同的处理
  2. 您希望添加一些可以在处理程序中实际使用的特定结构化数据

拥有单独的listvector异常似乎是不值得的,除非它们有一些明显的列表式或矢量化的东西。根据哪种类型的容器出现错误,您真的会有不同的捕获处理吗?

相反,对于可能在运行时恢复的事情,与回滚但可以重试的事情,以及肯定致命或指示错误的事情,使用单独的异常类型是有意义的。

在任何地方使用相同的异常都很容易。尤其是在试图捕捉异常时。不幸的是,它为口袋妖怪异常处理打开了大门。它带来了捕捉到你意想不到的异常的风险。

为所有不同的模块使用专用异常增加了几个优点:

  • 每个案例的自定义消息,使其对报告更有用
  • Catch只能捕获预期的异常,更容易在意外情况下崩溃(是的,这比不正确的处理更有优势)
  • 在崩溃时,IDE可以显示异常类型,甚至有助于调试

我认为除了其他答案中的原因外,还有代码可读性,因为程序员花了很多时间来支持它

void MyClass::function() noexcept(false) {
// ...
if (errorCondition) {
throw std::exception("Error: empty frame");
}
}
void MyClass::function() noexcept(false) {
// ...
if (errorCondition) {
throw EmptyFrame();
}
}

在第二种情况下,我认为它更可读,并且用户的消息(用what()函数打印)隐藏在这个自定义异常类中。

我看到了两个可能的原因。

1) 如果您想在异常类中存储一些关于异常的自定义信息,那么您需要一个具有额外数据成员的异常。通常,您将从std异常类之一继承。

2) 如果要区分异常。例如,假设您有两种不同的invalid argument情况,并且在catch块中,您希望能够区分这两种情况。您可以创建std::invalid_argument的两个子类

我看到的STL的每个元素触发不同内存异常的问题是,STL的某些部分是在其他部分上构建的,因此队列必须捕获和转换列表异常,或者队列的客户端必须知道如何处理列表异常。

我可以看到将异常从不同模块中分离出来的一些逻辑,但我也可以看到拥有项目范围的异常基类的逻辑。我还可以看到拥有异常类型类的逻辑。

邪恶的联盟将建立一个例外层次:

std::exception
EXProjectBase
EXModuleBase
EXModule1
EXModule2
EXClassBase
EXClassMemory
EXClassBadArg
EXClassDisconnect
EXClassTooSoon
EXClassTooLate

然后坚持每个实际激发的异常都是从模块和分类中派生的。然后,你可以捕捉任何对捕捉者有意义的东西,比如断开连接,而不是必须分别捕捉HighLevelDisconnect和LowLevelDiscovery。

另一方面,建议HighLevel接口应该完全处理LowLevel故障也是完全公平的,并且它们永远不应该被HighLevel API客户端看到。在这里,模块捕捉将是一个有用的最后一搏功能。

对于try...catch部分,我想问,"这段代码知道如何从错误中恢复吗?

使用try...catch的代码预计可能会发生异常。您编写try...catch而不是简单地让异常通过的原因是:

  1. 添加一些代码以确保父函数异常的安全。很多工作都应该在析构函数中使用RAII悄悄完成,但有时(例如移动操作)需要更多的工作来回滚部分完成的工作,这取决于您想要的异常安全级别
  2. 发出或添加一些调试信息并重新引发异常
  3. 尝试从错误中恢复

对于情况1和2,您可能不需要担心用户异常类型,它们看起来像这个

try {
....
} catch (const std::exception & e) {
// print or tidy up or whatever
throw;
} catch (...) {
// print or tidy up or whatever
// also complain that std::exception should have been thrown
throw;
}

根据我的经验,这可能会在99%的现实世界中发生。

有时您会希望从错误中恢复。也许父函数知道库在某些情况下会失败,这可以动态修复,或者有一种使用不同机制重新尝试作业的策略。

有时catch块会被专门编写,因为较低级别的代码被设计为抛出异常,作为其正常流程的一部分。(我经常在读取不受信任的外部数据时这样做,在这些数据中,许多事情都可能出现错误。)

在这种罕见的情况下,用户定义的类型是有意义的。