More Effective C++----(14)审慎使用异常规格(exception specifications)

Item M14:审慎使用异常规格(exception specifications)

毫无疑问,异常规格是一个引人注目的特性。它使得代码更容易理解,因为它明确地描述了一个函数可以抛出什么样的异常。但是它不只是一个有趣的注释。编译器在编译时有时能够检测到异常规格的不一致。而且如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。异常规格既可以做为一个指导性文档同时也是异常使用的强制约束机制,它好像有着很诱人的外表。

不过在通常情况下,美貌只是一层皮,外表的美丽并不代表其内在的素质。函数unexpected缺省的行为是调用函数terminate,而terminate缺省的行为是调用函数abort,所以一个违反异常规格的程序其缺省的行为就是halt(停止运行)。在激活的栈中的局部变量没有被释放,因为abort在关闭程序时不进行这样的清除操作。对异常规格的触犯变成了一场并不应该发生的灾难。

不幸的是,我们很容易就能够编写出导致发生这种灾难的函数。编译器仅仅部分地检测异常的使用是否与异常规格保持一致。一个函数调用了另一个函数,并且后者可能抛出一个违反前者异常规格的异常,(A函数调用B函数,但因为B函数可能抛出一个不在A函数异常规格之内的异常,所以这个函数调用就违反了A函数的异常规格 译者注)编译器不对此种情况进行检测,并且语言标准也禁止编译器拒绝这种调用方式(尽管可以显示警告信息)。

例如函数f1没有声明异常规格,这样的函数就可以抛出任意种类的异常:

extern void f1(); // 可以抛出任意的异常

假设有一个函数f2通过它的异常规格来声明其只能抛出int类型的异常:

void f2() throw(int);

f2调用f1是非常合法的,即使f1可能抛出一个违反f2异常规格的异常:

void f2() throw(int)
{
  ...
  f1();                  // 即使f1可能抛出不是int类型的
                         //异常,这也是合法的。
  ...
}

当带有异常规格的新代码与没有异常规格的老代码整合在一起工作时,这种灵活性就显得很重要。

因为你的编译器允许你调用一个函数,其抛出的异常与发出调用的函数的异常规格不一致,并且这样的调用可能导致你的程序执行被终止,所以在编写软件时应该采取措施把这种不一致减小到最少。一种好方法是避免在带有类型参数的模板内使用异常规格。例如下面这种模板,它好像不能抛出任何异常:

// a poorly designed template wrt exception specifications
template<class T>
bool operator==(const T& lhs, const T& rhs) throw()
{
  return &lhs == &rhs;
}

这个模板为所有类型定义了一个操作符函数operator==。对于任意一对类型相同的对象,如果对象有一样的地址,该函数返回true,否则返回false。

这个模板包含的异常规格表示模板生成的函数不能抛出异常。但是事实可能不会这样,因为opertor&(地址操作符,参见Effective C++ 条款45)能被一些类型对象重载。如果被重载的话,当调用从operator==函数内部调用opertor&时,opertor&可能会抛出一个异常,这样就违反了我们的异常规格,使得程序控制跳转到unexpected。

上述的例子是一种更一般问题的特例,这个更一般问题也就是没有办法知道某种模板类型参数抛出什么样的异常。我们几乎不可能为一个模板提供一个有意义的异常规格。因为模板总是采用不同的方法使用类型参数。解决方法只能是模板和异常规格不要混合使用。

能够避免调用unexpected函数的第二个方法是如果在一个函数内调用其它没有异常规格的函数时应该去除这个函数的异常规格。这很容易理解,但是实际中容易被忽略。比如允许用户注册一个回调函数:

// 一个window系统回调函数指针
//当一个window系统事件发生时
typedef void (*CallBackPtr)(int eventXLocation,
                            int eventYLocation,
                            void *dataToPassBack);
//window系统类,含有回调函数指针,
//该回调函数能被window系统客户注册
class CallBack {
public:
  CallBack(CallBackPtr fPtr, void *dataToPassBack)
  : func(fPtr), data(dataToPassBack) {}
  void makeCallBack(int eventXLocation,
                    int eventYLocation) const throw();
private:
  CallBackPtr func;               // function to call when
                                  // callback is made
   void *data;                    // data to pass to callback
};                                // function
// 为了实现回调函数,我们调用注册函数,
//事件的作标与注册数据做为函数参数。
void CallBack::makeCallBack(int eventXLocation,
                            int eventYLocation) const throw()
{
  func(eventXLocation, eventYLocation, data);
}

这里在makeCallBack内调用func,要冒违反异常规格的风险,因为无法知道func会抛出什么类型的异常。

通过在程序在CallBackPtr typedef中采用更严格的异常规格来解决问题:

typedef void (*CallBackPtr)(int eventXLocation,
                            int eventYLocation,
                            void *dataToPassBack) throw();

这样定义typedef后,如果注册一个可能会抛出异常的callback函数将是非法的:

// 一个没有异常规格的回调函数
void callBackFcn1(int eventXLocation, int eventYLocation,
                  void *dataToPassBack);
void *callBackData;
...
CallBack c1(callBackFcn1, callBackData);
                               //错误!callBackFcn1可能
                               // 抛出异常
//带有异常规格的回调函数
void callBackFcn2(int eventXLocation,
                  int eventYLocation,
                  void *dataToPassBack) throw();
CallBack c2(callBackFcn2, callBackData);
                               // 正确,callBackFcn2
                               // 没有异常规格

传递函数指针时进行这种异常规格的检查,是语言的较新的特性,所以有可能你的编译器不支持这个特性。如果它们不支持,那就依靠你自己来确保不能犯这种错误。

避免调用unexpected的第三个方法是处理系统本身抛出的异常。这些异常中最常见的是bad_alloc,当内存分配失败时它被operator new 和operator new[]抛出(参见条款M8)。如果你在函数里使用new操作符(还参见条款M8),你必须为函数可能遇到bad_alloc异常作好准备。

现在常说预防胜于治疗(即:做任何事都要未雨绸缪 译者注),但是有时却是预防困难而治疗容易。也就是说有时直接处理unexpected异常比防止它们被抛出要简单。例如你正在编写一个软件,精确地使用了异常规格,但是你必须从没有使用异常规格的程序库中调用函数,要防止抛出unexpected异常是不现实的,因为这需要改变程序库中的代码。

虽然防止抛出unexpected异常是不现实的,但是C++允许你用其它不同的异常类型替换unexpected异常,你能够利用这个特性。例如你希望所有的unexpected异常都被替换为UnexpectedException对象。你能这样编写代码:

class UnexpectedException {};          // 所有的unexpected异常对象被
                                       //替换为这种类型对象
void convertUnexpected()               // 如果一个unexpected异常被
{                                      // 抛出,这个函数被调用
  throw UnexpectedException();
}

通过用convertUnexpected函数替换缺省的unexpected函数,来使上述代码开始运行。:

set_unexpected(convertUnexpected);

当你这么做了以后,一个unexpected异常将触发调用convertUnexpected函数。Unexpected异常被一种UnexpectedException新异常类型替换。如果被违反的异常规格包含UnexpectedException异常,那么异常传递将继续下去,好像异常规格总是得到满足。(如果异常规格没有包含UnexpectedException,terminate将被调用,就好像你没有替换unexpected一样)

另一种把unexpected异常转变成知名类型的方法是替换unexpected函数,让其重新抛出当前异常,这样异常将被替换为bad_exception。你可以这样编写:

void convertUnexpected()          // 如果一个unexpected异常被
{                                 //抛出,这个函数被调用
  throw;                          // 它只是重新抛出当前
}                                 // 异常
set_unexpected(convertUnexpected);
                                  // 安装 convertUnexpected
                                  // 做为unexpected
                                  // 的替代品

如果这么做,你应该在所有的异常规格里包含bad_exception(或它的基类,标准类exception)。你将不必再担心如果遇到unexpected异常会导致程序运行终止。任何不听话的异常都将被替换为bad_exception,这个异常代替原来的异常继续传递。

到现在你应该理解异常规格能导致大量的麻烦。编译器仅仅能部分地检测它们的使用是否一致,在模板中使用它们会有问题,一不注意它们就很容易被违反,并且在缺省的情况下它们被违反时会导致程序终止运行。异常规格还有一个缺点就是它们能导致unexpected被触发,即使一个high-level调用者准备处理被抛出的异常,比如下面这个几乎一字不差地来自从条款M11例子:

class Session {                  // for modeling online
public:                          // sessions
  ~Session();
  ...
private:
  static void logDestruction(Session *objAddr) throw();
};
Session::~Session()
{
  try {
    logDestruction(this);
  }
  catch (...) {  }
}

session的析构函数调用logDestruction记录有关session对象被释放的信息,它明确地要捕获从logDestruction抛出的所有异常。但是logDestruction的异常规格表示其不抛出任何异常。现在假设被logDestruction调用的函数抛出了一个异常,而logDestruction没有捕获。我们不会期望发生这样的事情,但正如我们所见,很容易就会写出违反异常规格的代码。当这个异常通过logDestruction传递出来,unexpected将被调用,缺省情况下将导致程序终止执行。这是一个正确的行为,但这是session析构函数的作者所希望的行为么?作者想处理所有可能的异常,所以好像不应该不给session析构函数里的catch块执行的机会就终止程序。如果logDestruction没有异常规格,这种事情就不会发生(一种防止的方法是如上所描述的那样替换unexpected)。

以全面的角度去看待异常规格是非常重要的。它们提供了优秀的文档来说明一个函数抛出异常的种类,并且在违反它的情况下,会有可怕的结果,程序被立即终止,在缺省时它们会这么做。同时编译器只会部分地检测它们的一致性,所以他们很容易被不经意地违反。而且他们会阻止high-level异常处理器来处理unexpected异常,即使这些异常处理器知道如何去做。

综上所述,异常规格是一个应被审慎使用的特性。在把它们加入到你的函数之前,应考虑它们所带来的行为是否就是你所希望的行为。

时间: 2024-07-29 18:36:48

More Effective C++----(14)审慎使用异常规格(exception specifications)的相关文章

Effective C++ Item 29 为”异常安全”而努力是值得的

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏.这样的函数区分为三种 可能的保证: 基本型-->发生异常,程序处于某个合法状态 强烈型-->发生异常,程序处于原先状态 不抛异常型-->承诺绝不抛出殿堂 示例: class PrettyMenu{ public: //... void changeBackground(std::istream &imgSr

Effective Item 14 - 关于泛型

自Java 1.5开始使用的泛型,给人比较简单的印象是..."尖括号里写了类型我就不用检查类型也不用强转了". 好的,那先从API的使用者的角度上想问题,泛型还有什么意义? 有句话叫:Discover errors as soon as possible after they are made, ideally at compile time. 泛型提供的正是这种能力. 比如有一个只允许加入String的集合,在没有声明类型参数的情况下,这种限制通常通过注视来保证. 接着在集合中add

More Effective C++ 条款14 明智运用exception specifications

1. Exception specifications作为函数声明的一部分,用于指出(并不能限制)函数可能会抛出的异常函数.C++规定,一个拥有exception specification的函数指针只能被赋予一个有着相同或更为局限的exception specification的函数地址,因而编译器要保证"在函数指针传递之际检验exception specifications".(但visual studio 2013不支持此项要求) 2. 当函数抛出exception specif

14.PowerShell--抛出异常,错误处理

PowerShell – 错误处理 1.  What-if 参数 (试运行,模拟操作) 简介:PowerShell 不会执行任何对系统有影响的操作,只会告诉你如果没有模拟运行,可能产生什么影响和后果. 实例: PS C:\>Stop-Process  -name calc -whatif What if: Performing theoperation "Stop-Process" on target "calc (119000)". 2.  -confirm

More Effective C++ Item14:明智运用exception specifications

使用exception specifications你必须非常仔细去确保,函数调用的子函数.注册的回调函数不会违背约定.而设计模板内部的异常更难确保. 设计回调机制的时候,如果调用方规定了不抛出异常,就必须确保注册进来的函数均不会抛出异常,书上给出了这样的做法: typedef void(*CallBackPtr)( int eventXLocation, int eventYLocation, void *dataToPassBack ) throw(); 并以CallBackPtr类型注册函

C# 知识回顾 - 你真的懂异常(Exception)吗?

你真的懂异常(Exception)吗? 目录 异常介绍 异常的特点 怎样使用异常 处理异常的 try-catch-finally 捕获异常的 Catch 块 释放资源的 Finally 块 一.异常介绍 我们平时在写程序时,无意中(或技术不够),而导致程序运行时出现意外(或异常),对于这个问题, C# 有专门的异常处理程序. 异常处理所涉及到的关键字有 try.catch 和 finally 等,用来处理失败的情况. CLR..NET 自身的类库.其它第三方库或者你写的程序代码都有可能会出现异常

Thymeleaf 异常:Exception processing template &quot;index&quot;: An error happened during template parsing (template: &quot;class path resource [templates/index.html]&quot;)

Spring Boot 项目,在 Spring Tool Suite 4, Version: 4.4.0.RELEASE 运行没有问题,将项目中的静态资源和页面复制到 IDEA 的项目中,除了 IDE 不同,其他基本相同. 运行 IDEA 中的项目,然后访问,出现异常: Exception processing template "index": An error happened during template parsing (template: "class path

启动tomcat后struts框架报异常严重: Exception starting filter struts2 Unable to load configuration.

启动tomcat后struts框架报异常严重: Exception starting filter struts2 Unable to load configuration. 出现此异常是因为,struts.xml定义的版本和 struts2-core-2.1.6.jar里面的struts-default.xml版本不一致!! struts-default.xml文件里面定义的<!DOCTYPE ...>如下: <!DOCTYPE struts PUBLIC "-//Apach

中断(interrupt)、异常(exception)、陷入(trap)

http://blog.chinaunix.net/cp.php?ac=blog 中断:是为了设备与CPU之间的通信.典型的有如服务请求,任务完成提醒等.比如我们熟知的时钟中断,硬盘读写服务请求中断.中断的发生与系统处在用户态还是在内核态无关,只决定于EFLAGS寄存器的一个标志位.我们熟悉的sti, cli两条指令就是用来设置这个标志位,然后决定是否允许中断.在单个CPU的系统中,这也是保护临界区的一种简便方法.中断是异步的,因为从逻辑上来说,中断的产生与当前正在执行的进程无关.事实上,中断是