C++编程规范 错误处理与异常

第68条 广泛地使用断言记录内部假设和不变式
使用断言吧!广泛地使用 assert 或者等价物记录模块内部(也就是说,调用代码和被调用代码由同一个人或者小组维护)的各种假设,这些假设是必须成立的,否则就说明存在编程错误(例如,函数的调用代码检查到函数的后条件不成立)。(另见 C70)当然,要确保断言不会产生任何副作用。
详细:
1、按照信息论的原理,一个事件中所含的信息量与该事件发生的概率是成反比的。因此,如果 assert 触发的可能性越低,它触发时所提供的信息量就越大。
2、要避免使用 assert(false),应该使用 assert(!"informational message")。还可以考虑在更复杂的断言中加入 && "informational message",尤其是这样可以取代注释。
3、不要使用断言报告运行时错误(见 C70, C72)。例如,不要用 assert 确保 malloc 成功执行、窗口成功创建或者线程成功启动。但是,可以用 assert 确保 API 函数的执行符合文档的记载。
4、用抛出异常代替断言是不可取的,虽然标准库的 std::logic_error 异常类最初就是为此目的而设计的。
5、我们知道有些错误是可能发生的(见 C69, C75)。对于其他不应该发生的错误,如果发生了,就是程序员的过错,此时就该使用 assert 了。
6、对基本假设应该使用 assert。对类型系统无法强制实施的重言式(逻辑学上重言式指的是由简单的叙述句所组成的肯定为真的陈述。),应该使用 assert。

第69条 建立合理的错误处理策略,并严格遵守
应该在设计早期开发实际、一致、合理的错误处理策略,并予以严格遵守。许许多多的项目对这一点的考虑(或者错误估计)都相当草率,应该对此有意识地规定,并认真应用。策略必须包含以下内容:
鉴别:那些情况属于错误。
严重程度:每个错误的严重性或紧急性。
检查:哪些代码负责检查错误。
传递:用什么机制在模块中报告和传递错误通知。
处理:哪些代码负责处理错误。
报告:怎样将错误记入日志,或者通知用户。
只在模块边界处改变错误处理机制。
详细:
1、普适地:错误鉴别:对每个实体(例如,每个函数、每个类、每个模块),记录该实体内部和外部的不变式。
2、对每个函数:错误鉴别:记录它的前后条件、它负有维护责任的不变式以及它所支持的错误安全性保证(见 C70, C71)。
3、对每个错误(见 C70):错误的严重程度和分类、错误检查(见 C70)、错误处理(见 C74)、错误报告。
4、对每个模块:错误的传递:对于每个模块(请注意:是每个模块,而不是每个错误),标明使用什么编程机制传递错误(例如 C++ 异常、COM 异常、CORBA 异常、返回值)。
5、我们要强调的是,错误处理机制应该只在模块边界改变(见 C62, C63)。每个模块都应该在其内部一致地使用一种错误处理策略和机制(例如,用 C++ 编写的模块应该在内部使用异常;见 C72),并在接口中一致地使用一种(可能是不同于内部所用的另一种)错误处理策略和机制(例如,模块可以能会提供普通的 C 语言的 API,从而允许用各种编程语言编写调用代码,或者也可能会提供能够支持 COM 异常和 COM 封装)。
6、如果模块内外所使用的策略不同,则所有模块入口函数都要直接负责由内到外的策略转换。特别要注意,按定义回调函数和线程主线函数是(或者可能是)位于模块边界(见 C62)。

第70条 区别错误与非错误
违反约定就是错误:函数是一个工作单元。因此,失败应该视为错误,或根据其对函数的影响而定。在函数 f 中,当且仅当失败违反了 f 的一个前条件,或者阻碍了 f 满足其调用代码的任何前条件、实现 f 自己的任何后条件或者重新建立 f 有责任维持的不变式时,失败才是一个错误。
这里我们特别排除了内部的程序设计错误(即调用代码和被调用代码都由同一个人或者同一个团队负责,比如位于一个模块中),这种错误一般可以使用断言来解决(见 C68)。
详细:
1、根据对函数的影响,而清楚地区别错误和非错误是非常关键的,对定义安全保证(见 C71)而言尤其如此。
2、错误就是阻止函数成功操作的任何失败。有三种类型:违反或者无法满足前条件(precondition)、无法满足后条件(postcondition)、无法重新建立不变式(invariant)。任何其他情况都不是错误,因此不应该报告为错误。
3、可能产生错误的代码应该负责检查和报告错误。
4、如果函数处于模块内部(即只能从模块内部调用),则任何前条件的违反按定义都是模块的编程错误,此时可以通过断言来报告(见 C68)。这就是所谓的防御性编程(defensive programming)。
5、当且仅当有正当理由让所有调用代码在调用函数 f 之前检查和验证条件的有效性时,这个条件才应该是函数 f 的前条件。

第71条 设计和编写错误安全代码
承诺,但是不惩罚:在所有函数中,都应该提供最强的安全保证,而且不应该惩罚不需要这种保证的调用代码。至少要提供基本保证。
确保出现错误时程序会处于有效状态。这是所谓的基本保证(basic guarantee)。要小心会破坏不变式的错误(包括但是不限于泄漏),它们肯定都是 bug。
应该进一步保证最终状态要么是最初状态(如果有错误,则回滚操作),要么是所希望的目标状态(如果没有错误,则提交操作)。这就是所谓的强保证(strong guarantee)。
应该进一步保证操作永远不会失败。虽然这对于大多数函数来说是不可能的,但是对于析构函数和释放函数这样的函数来说则是必须的。这就是所谓的不会失败保证(no-fail guarantee)。
详细:
1、一般而言,每个函数都应该尽可能地提供最强保证,而且不能无必要地惩罚不需要此保证的调用代码。只要可能,它应该进一步提供足够的功能以允许需要更强保证的调用代码获得这种保证。
2、理想情况下,我们编写的是总能成功的函数,所有可以提供不会失败保证。
3、大多数函数都会失败。当有可能出现错误时,最安全的方法就是确保函数支持事务性的行为:要么就完全成功,将程序从初始的有效状态转到期望的目标有效状态,要么就失败,让程序保持调用前的状态不变,即任何对象的可见状态在调用失败之前与调用失败之后都是一样的,在调用失败之前调用代码能够执行的任何操作,在调用失败之后仍然能够保持原有语义执行。这就是强保证。
4、如果提供强保证很困难或者过于昂贵,那就提供基本保证:要么函数完全成功,达到期望的目标状态,要么无法完全成功,让程序处于一个仍然有效(保持函数知道而且负责保持的不变式)但是不可预测(可能是初始状态,也可能不是,可能能满足所有后条件,也可能只能满足其中一些,还有可能完全不能满足;但是要注意的是必须重新建立所有的不变式)的状态。应用程序的设计必须为正确地处理该状态做好准备。
5、不存在更低的层次。如果无法满足最低的基本保证,那么无疑是一个程序 bug。
6、“错误不安全”和“糟糕设计”是形影不离的:如果一段代码连基本保证都很难满足,那么这几乎就是说明设计很糟糕。例如,如果函数要承担多个互不相关的职责,那就很难做到错误安全的(见 C5)。

第72条 优先使用异常报告错误
出现问题时,就使用异常:应该使用异常而不是错误码来报告错误。但不能使用异常时,对于错误以及不是错误的情况,可以使用状态码(比如返回码,errno)(见 C62)来报告异常。当不可能从错误中恢复或者不需要恢复时,可以使用其他方法,比如正常终止或者非正常终止。
详细:
1、在 C++ 中,和通过错误码报告相比,通过异常报告错误有许多明显的优势,这些优势都能够使代码更加健壮:异常不能不加修改地忽略;异常是自动传播的;有了异常处理,就不必在控制流的主线中加入错误处理和恢复了;对于从构造函数和操作符报告错误来说,异常处理要优于其他方案。
2、异常处理潜在的主要缺点在于,它要求程序员必须熟悉一些会反复遇到的惯用法,这些惯用法来源于异常的特殊控制流。
3、一个常见的编程惯用法就是先执行所有可能安全地发出异常的工作,然后,只在知道真正的工作成功后,才使用提供不会失败保证(见 C51)的操作提交和修改程序状态。
4、性能通常不是异常处理的缺点。首先,请注意应该总是打开编译器的异常处理,即使默认是关闭的,否则就无法获得 C++ 语言操作(比如 operator new)和标准库操作(比如 STL 容器插入)的标准行为和错误报告。
5、错误码使用过度的一个标志就是,应用程序需要不断地检查各种琐细的为真条件,或者(更糟糕地)不检查应该检查的错误码。
6、异常使用过度的一个标志就是,应用程序代码频繁地抛出和捕获异常,以致于 try 代码块成功与失败的次数是同一数量级的。这样的 catch 代码块要么没有处理真正的错误(违反了前条件、后条件和不变式的错误),要么说明程序存在严重问题。
7、构造函数失败时,应该抛出异常;成功的树递归查找,不应该将查找结果作为异常抛出。
8、在极其罕见的情况下,如果能够肯定以下两点为真,就可以考虑使用错误码:异常的优点不适用;抛出异常与使用错误码的实测性能差异比较明显。应该在别无选择的情况下才采用,在进行决策之前,要详细分析如何从构造函数和操作符中报告错误,分析这种机制将如何在所用的编译器上运作。如果在认真深入的分析之后,仍然认为确实不得不关闭异常处理,那么也不要在整个项目范围内这样做;只在尽可能少的模块中这样做,尝试尽量将这种时间敏感的关键操作集中在一个模块中,会有所帮助。

第73条 通过值抛出,通过引用捕获
学会正确捕获(catch):通过值(而非指针)抛出异常,通过引用(通常是 const 的引用)捕获异常。这是与异常语义配合最佳的组合。当重新抛出相同的异常时,应该优先使用 throw; ,避免使用 throw e; 。
详细:
1、如果抛出指针,就需要处理内存管理问题。如果觉得确实必须抛出指针,那么可以考虑抛出一个类似值的智能指针,比如用 shared_ptr<T> 代替普通的 T* 。
2、通过值抛出可以说是“集人间宠爱于一身”,因为这时编译器本身将负责管理异常对象的内存这一复杂过程。我们所需要操心的就是保证为异常类实现不抛出的复制构造函数(见 C32)。
3、通过值捕获普通值将在捕获处引起切片问题(见 C54),这会粗暴地去除通常至关重要的异常对象的多态性。通过引用捕获则能够保持异常对象的多态性。
4、在重新抛出异常 e 时,应该只写成 throw; 而不是 throw e; ,因为第一种形式总是能够保持重新抛出对象的多态性。

第74条 正确地报告、处理和转换错误
什么时候说什么话:在检查出并确认是错误时报告错误。在能够正确处理错误的最近一层处理或者转换每个错误。
详细:
1、只要函数检查出一个它自己无法解决而且会使函数无法继续执行的错误,就应该报告错误(比如编写 throw)(见 C70)。
2、我们需要具备的处理错误的知识包括在错误策略中定义的保证错误不跨越块边界(如在 main 和线程 mainline 中,见 C62)和吸收析构函数和释放操作中出现的错误。只有具备了这些知识,才能够正确地处理错误(比如编写 catch,不再重新抛出同样的或者另一个异常,或者发送另一种错误码)。
3、在以下情况下转换错误:要添加高层的语义;要改变错误处理机制。

第75条 避免使用异常规范
对异常规范说不:不要在函数中编写异常规范,除非不得已而为之(因为其他无法修改的代码已经使用了异常规范,见本条款例外情况)。
详细:
1、简而言之,不要沾惹异常规范。即使是专家级程序员也不应该。
2、异常规范的主要问题在于,它们只不过“有些像”类型系统的一部分,它们的行为与大多数人所想象的都不同,而且它们实际所做的几乎总是与我们想要的不符。
3、一种很常见但不正确的看法是,异常规范能够静态地保证函数只抛出规范所列的异常(可能为空),从而使编译器能够基于这种信息实施优化。
4、然后最糟糕的,还在于异常规范是一种并不灵活的工具:如果违反,默认情况下会马上终止程序。当然,可以注册一个 unexpected_handler,但它极有可能帮不上什么忙,因为只有一个全局处理函数,而它避免立即调用 terminate 的唯一方法是,重新抛出一个异常规范所允许的异常。
5、一般而言,无法为函数模板编写出有用的异常规范,因为一般无法说清它们所操作的类型可能抛出哪些异常。
6、为强制实施几乎完全无用的异常规范(因为一旦违反,结果将是无可挽回的)而付出性能开销,是不成熟劣化(见 C9)的极佳例子。
7、如果不得不改写已经使用了异常规范的基类虚拟函数(比如,std::exception::what),而且又不能修改基类删去异常规范(或者说服基类的维护者删去),那么就只能在改写函数中写一个兼容的异常规范,同时应该尽量使其限制不少于基类版本,这样可以最大限度地减少违反异常规范的频率。

class Base
{								// 在别人编写的一个类中
	virtual f() throw(X, Y, Z);	// 作者使用了异常规范
};								// 如果无法让他删去的话……

class MyDerived : public Base
{								// ……那么在自己的基类改写类中
	virtual f() throw(X, Y, Z);	// 必须有一个兼容的
};								// (完全一样更好)异常规范

返回 目录

返回《C++ 编程规范及惯用法》

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值