更多 C++ 惯用法/多态异常
- 以多态的方式创建异常对象
- 将模块与它可能抛出的异常的具体细节解耦
依赖倒置原则 (DIP),一个流行的面向对象软件设计指南指出,高级模块不应该直接依赖于低级模块。相反,两者都应该依赖于共同的抽象(以定义良好的接口的形式捕获)。例如,类型为 Person 的对象(例如 John)不应创建和使用类型为 HondaCivic 的对象,而是 John 应该简单地承诺 Car 接口,它是 HondaCivic 的抽象基类。这允许 John 在将来轻松升级到 Corvette,而无需对 Person 类进行任何更改。John 可以使用 依赖注入 技术用汽车的具体实例(任何汽车)进行“配置”。使用 DIP 导致灵活且可扩展的模块,这些模块易于进行单元测试。DIP 简化了单元测试,因为可以使用依赖注入轻松将真实对象替换为模拟对象。
但是,在某些情况下会违反 DIP:(1)在使用单例模式时,以及(2)在抛出异常时!单例模式破坏了 DIP,因为它在访问静态 instance() 函数时强制使用具体类名。应该在调用函数或构造函数时将单例作为参数传递。在处理 C++ 中的异常时,也会出现类似的情况。C++ 中的 throw 子句需要一个具体类型名(类)来引发异常。例如,
throw MyConcreteException("Big Bang!");
任何抛出类似异常的模块都会立即导致违反 DIP。自然,这样的模块更难进行单元测试,因为无法轻松地将真实异常对象替换为模拟异常对象。像下面这样的解决方案惨败了,因为 C++ 中的 throw 使用静态类型并且对多态性一无所知。
struct ExceptionBase { };
struct ExceptionDerived : ExceptionBase { };
void foo(ExceptionBase& e)
{
throw e; // Uses static type of e while rasing an exception.
}
int main (void)
{
ExceptionDerived e;
try {
foo(e);
}
catch (ExceptionDerived& e) {
// Exception raised in foo does not match this catch.
}
catch (...) {
// Exception raised in foo is caught here.
}
}
多态异常惯用法解决了这个问题。
多态异常惯用法简单地使用虚拟函数 raise() 将引发异常的任务委托给派生类。
struct ExceptionBase
{
virtual void raise() { throw *this; }
virtual ~ExceptionBase() {}
};
struct ExceptionDerived : ExceptionBase
{
virtual void raise() { throw *this; }
};
void foo(ExceptionBase& e)
{
e.raise(); // Uses dynamic type of e while raising an exception.
}
int main (void)
{
ExceptionDerived e;
try {
foo(e);
}
catch (ExceptionDerived& e) {
// Exception raised in foo now matches this catch.
}
catch (...) {
// not here anymore!
}
}
throw 语句已移至虚拟函数中。在函数 foo 中调用的 raise 函数是多态的,它根据作为参数传递的内容(依赖注入)在 ExceptionBase 或 ExceptionDerived 类中选择实现。*this 的类型显然在编译时已知,这会导致引发多态异常。这种惯用法的结构与 虚拟构造函数 惯用法非常相似。
传播多态异常
通常,异常会在多个 catch 语句中进行处理,以便在程序/库的不同层中对它进行不同的处理。在这种情况下,较早的 catch 块需要重新抛出异常,以便任何外部 catch 块(如果有)都可以采取必要的措施。当涉及多态异常时,内部 catch 块可能会在将异常传递给堆栈中的 catch 块之前修改异常对象。在这种情况下,必须注意确保传播原始异常对象。考虑下面看似无害的程序,它没有做到这一点。
try {
foo(e); // throws an instance of ExceptionDerived as before.
}
catch (ExceptionBase& e) // Note the base class. Exception is caught polymorphically.
{
// Handle the exception. May modify the original exception object.
throw e; // Warning! Object slicing is underway.
}
throw e 语句不会抛出原始异常对象。相反,它会抛出原始对象的切片副本(仅 ExceptionBase 部分),因为它会考虑它前面的表达式的静态类型。原始异常对象被默默地丢失,并且被转换为基类型异常对象。堆栈中的 catch 块无法访问此 catch 块拥有的相同信息。有两种方法可以解决这个问题。
- 只需使用 throw;(后面没有任何表达式)。它将重新抛出原始异常对象。
- 再次使用多态异常惯用法。它将抛出原始异常对象的副本,因为 raise() 虚拟函数使用 throw *this。
在实践中应该强烈推荐使用 throw;,因为根据实现,它可能会在异常未处理并且程序转储核心时保留原始抛出位置,从而简化问题的死后分析。
try {
foo(e); // throws an instance of ExceptionDerived as before.
}
catch (ExceptionBase& e) // Note the base class. Exception is caught polymorphically.
{
// Handle the exception. May modify the original exception object.
// Use only one of the following two.
throw; // Option 1: Original derived exception is thrown.
e.raise(); // Option 2: A copy of the original derived exception object is thrown.
}