noexcept
声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。- 相对于不带
noexcept
声明的函数,带有noexcept
声明的函数有更多的机会得到优化noexcept
的性质对于移动操作,交换,内存释放函数,析构函数最有价值。- 大多数函数是异常中立的,不具备
noexcept
性质。
何谓异常|异常处理
异常
异常是指存在于程序运行时的反常行为,这些行为超出了函数正常功能的范畴。
C++
异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。 当程序的某部分检测到一个它无法处理的问题时,需要用到异常处理 。
异常处理
异常处理 机制 为程序中异常检测和异常处理这两部分的协作提供支持,允许程序中独立开发的部分能够在运行时,就出现的问题进行通信并作出相应的处理。异常使得我们能够将问题的检测与解决过程分离开来。程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。检测环节无须知道问题处理模块的所有细节,反之亦然。异常处理提供了一种系统的、健壮的方法来处理在本地检测到错误时无法恢复的错误。
注: 使用异常类和c++异常处理机制来捕获基本软件错误是对异常处理机制的误解。
认识异常的一种方式是将它看作在无法局部地执行有意义的动作时,就把控制交给调用者。
c++的异常处理包括:
thow
表达式、try
语句、一套异常类
为何要有异常处理
- 异常可能真的是无处不在,异常处理也确实五花八门,
c++
需要一个标准的框架来进行异常的处理。
随着程序越来越大,特别是当程序库被广泛使用时,处理错误(用更普遍的说法:“异常情景”)的标准将变得日益重要起来。Ada
、Algol 68
和Clu
都有各自的支持处置异常的标准方式。不幸的是,在c++
里还没有这种东西。在需要时,异常可以用函数指针、“异常对象”、“错误状态"以及c
标准库的signal
和longjmp
等机制来"冒充” 。一般来说,这是不能令人满意的,因此应该提供一种处理错误的标准框架。
- 当一个程序是由一些相互分离的模块组成时,特别是当这些模块来自某些独立开发的库时,错误处理的工作需要分成两个相互独立的部分:
- 一方报告出那些无法在局部解决的错误;
- 另一方处理那些在其他地方检查出的错误。
一个库的作者可以检查出运行时的错误,但一般说,他对于应该如何去做就没有什么主意了。库的使用者可能知道如何去处理某些错误,但却无法检查他们–要不然用户就会在自己的代码里处理这些错误,而不会把它们留给库。提供异常的概念就是为了有助于处理这类问题。
- C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以再适当的位置设计对不同类型异常的处理
- 异常是专门针对抽象编程中的一系列错误处理的,**C++中不能借助函数机制,**因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试。应该理解为另一种不同于函数机制的控制机构。
- **异常超脱于函数机制,**决定了其对函数的跨越式回跳。异常跨越函数。
异常处理的设计历程
初衷与思想
- 异常处理的设计初衷是:
- 并不是想简单地作为另一种返回机制,而是特别想作为一种支持构造容错系统的机制;
- 并不是想把每个函数转变为一个容错的实体,而是想作为一种机制,通过它能给子系统提供很大程度上的容错能力,即使其中各个函数在写法上并没有关心全局的错误处理策略。
- 并不是想将设计者们都约束到一个"正确的"错误处理概念上,而是希望语言更有表达能力。
- 设计思想:
- 允许从抛出点将任意数量的信息以类型安全的方式传递到异常处理器
- 对于不抛出异常的代码没有任何额外的(时间或空间)代价
- 保证所引发的每个异常都能被适当的处理器捕获
- 提供一种将异常结组的方式,使人不但可以写出捕获单个异常的处理器,还可以写出捕获一组异常的处理器
- 是一种默认方式就能对多线程系统正确工作的机制
- 是一种能够与其他语言合作的机制,特别是c语言工作
- 容易使用
- 容易实现
其中3和8只是接近实现。
结组
能够定义异常结组是很关键的一种能力 。例如,一个用户应该能捕获”所有的
I/O
异常“,而不必确切的知道其中到底包括那些异常。
结组设计的思路是: 在可能产生异常的代码中抛出对象 , 通过声明接受这个类型的对象的处理器去捕捉它。
如下:一个类型为Matherr
的处理器将能够捕获任何由Matherr
派生的类Overflow
的对象;一个MathFileError
类型的异常,既能被Matherr
处理,也能被FileError
处理。
class Matherr{};
class FileError{};
class Overflow : public Matherr{};
class Underflow: public Matherr{};
class Zerodivide: public Matherr{};
class MathFileError : public Matherr,public FileError{};
void g(){
try {fun;}
catch (Overflow ){
}
catch (Matherr){
}
}
资源管理
异常处理设计的核心点实际上是资源的管理。 特别是如果一个函数掌握着某项资源,如果发生了异常情况,应该如何帮助用户保证函数退出时能够正确地释放这项资源?一个优雅的解决方案是使用RAII技术(资源获取就是初始化),这种技术依赖于构造函数和析构函数的性质,以及它们与异常处理的相互关系。
RAII
保证资源能够用于任何会访问该对象的函数(资源可用性是一种类不变式,这会消除冗余的运行时测试)。它也保证对象在自己生存期结束时会以获取顺序的逆序释放它控制的所有资源。类似地,如果资源获取失败(构造函数以异常退出),那么已经构造完成的对象和基类子对象所获取的所有资源就会以初始化顺序的逆序释放。这有效地利用了语言特性(对象生存期、退出作用域、初始化顺序以及栈回溯)以消除内存泄漏并保证异常安全。根据RAII
对象的生存期在退出作用域时结束这一基本状况,此技术也被称为作用域界定的资源管理(Scope-Bound Resource Management,SBRM
)。
RAII
可以总结如下:
- 将每个资源封装入一个类,其中:
- 构造函数请求资源,并建立所有类不变式,或在它无法完成时抛出异常,
- 析构函数释放资源并且决不会抛出异常;
- 在使用资源时始终通过
RAII
类的满足以下要求的实例:- 自身拥有自动存储期或临时生存期,或
- 具有与自动或临时对象的生存期绑定的生存期
唤醒与终止
在异常处理机制设计期间,引起最大争议的是应该支持那种语义,是终止还是唤醒语义。但最终c++委员会采取了终止语义。
- 唤醒: 请求某个调用程序纠正问题,而后继续执行(使用函数调用,而非异常抛出来解决问题)
- 终止: 结束当前计算并返回某个调用程序。假设错误是致命性的,当异常发生后将无法返回原程序的正常运行部分,这时必须调用终止语义结束异常状态。无论程序的那个部分只要发生异常抛出,就表明程序运行进入了无法挽救的困境,应结束运行的非正常状态,而不应返回异常抛出之处。
对于唤醒方式,一个调用程序必须准备好,去帮助处理某段未知代码中出现的资源申请问题;对于终止方式,一个调用程序必须准备好去应付某个资源申请失败的情况。
在c++里,唤醒模式由函数调用机制支持,而终止模式由异常处理机制支持。请注意,采取终止策略时,终止的并不是整个程序,而只是其中某个个别的计算。
”终止“是一个传统的描述策略的术语,表示的是从”失败的“计算返回到与某个调用过程相关的某个错误处理器,而不是试图去修复那个坏状况,而后从检查出问题的哪一点继续下去。
”终止比唤醒更好“,这不是一种观点的问题,而是许多年的经验。唤醒是非常诱人的,但却是站不住脚的
迂回唤醒机制,如std::new_handler
。
new
处理函数是为分配函数在凡是内存分配尝试失败时调用的函数。其目的是三件事之一:
- 令更多内存可用
- 终止程序(例如通过调用
std::terminate
- 抛出
std::bad_alloc
或自std::bad_alloc
导出的类型的异常。
默认实现抛std::bad_alloc
。用户可以安装自己的new
处理函数,并可以提供异于默认者的行为。
若new
处理函数返回,则分配函数重复先前失败的分配,并若分配再次失败则调用new
处理函数。为终止循环,new
处理函数可调用std::set_new_handler(nullptr)
:若在失败的分配尝试后,分配函数发现std::get_new_handler
返回空指针值,则它会试图抛出std::bad_alloc
。
#include <cstdlib>
#include <iostream>
#include <new>
void no_memory() {
std::cout << "Failed to allocate memory!\n";
std::exit(1);
}
int main() {
std::set_new_handler(no_memory);
std::cout << "Attempting to allocate 1 GiB...\n";
int* p = new int[1024 * 1024 * 1024];
std::cout << "Ok\n";
delete p;
return 0;
}
非同步事件
c++
异常处理机制明显无法直接处理非同步事件,这种机制的设计只是为了处理同步异常。
- 中断异步 就是我可以不用立即处理,而是等执行完一条指令时候才可能处理**,异常同步** 是指出了异常必须立马处理。
- 为了做出可靠的系统,需要把非同步事件映射到某种形式的进程模型中。这种观点排除了直接使用异常去表达某些东西,如DEL案件,或者用异常去取代
unix
中的信号。- 低等事件,如算术溢出和除以零等不应由异常处理
多层传播
c++ 没有采用 在一个函数里发生的异常只能隐式地传播到它的直接调用处。
- 现存成百万的
c++
函数,期望去修改它们以便传播和处理异常是不合理的; - 把每个函数做成一个防火墙并不是一种好想法,最好的方法是让他们关注非局部的错误处理问题。
- 在混合编程环境中,不可能要求某个函数一定具有某种活动。
静态检查
由于允许异常的多层传播,
c++
就丧失了一方面的静态检查。你无法简单地看一个函数就确定它可能抛出什么异常。事实上它可能抛出任何异常,即使这个函数体中连一个thow语句也没有。因为被它调用的函数可能做这种抛出。
c++
提供了为描述一个函数可能抛出的异常所有的列表机制。 明确的声明函数可能抛出的异常,与在代码中直接进行与之等价的检查相比,其优点不仅在于节省了类型检查。最重要的有点是,函数声明属于用户可以看见的界面。而在另一方面函数定义并不是一般可见的。另一个优点是它使在编译时检查出许多未捕获异常的错误有了实际的可能性。
理想很丰满,异常刻画应该在编译时进行检查,但是这就要求每个函数都必须与这个模式合作,而这又是不可行的。进一步说,这种静态检查很容易变成许多重新编译的根源。 因此c++
决定支支持实时检查,将静态检查的问题留给另外的工具去做。使用动态检查时出现上述问题时使用结组去处理便可解决。关于静态检查这个在95年也支持了。
实现思想:
放置一个代码地址范围的表,将计算状态和与之相关的异常处理对应起来。对其中的每个范围,记录所有需要调用的析构函数和可能调用的异常处理器。当某个异常被抛出时,异常处理机构将程序计数器与范围表中的地址作比较。如果发现程序计数器位于表中某个范围里,就去执行有关动作;否则就解脱一层堆栈,使程序计数器退到调用程序中,再去查找范围表。
不变式
c
社区中的一部分人一直广泛地依靠assert()
宏,但是在运行中却没有好的办法报告出现违背断言的情况。异常提供了处理这个问题的一种方式,而模板提供了一种避免依赖宏的途径。
template<class T, class x>inline void Assert(T expr,X x){
if(!NODEBUG){
if(!expr) throw x;
}
}
如果expr
是假而且我们没有通过设置NDEBUG
关闭检查,他就会抛出异常x
。
选择?
优点
- 使用异常进行错误处理可以使代码更简单、更清晰,并且不太可能错过错误。 但是“好的老的
errno
和if
语句”又有什么错呢?基本的答案是: 使用它们,您的错误处理和普通代码是紧密交织在一起的。这样,您的代码会变得混乱,并且很难确保您已经处理了所有的错误。 - 异常允许应用高层决定如何处理在底层嵌套函数中「不可能发生」的失败(
failures
),不用管那些含糊且容易出错的错误代码(acgtyrant
注:error code
, 我猜是C
语言函数返回的非零int
值)。异常处理机制使程序更”脆弱“,异常处理模式对于错误的默认响应方式是终止程序,而传统则是接着做下去,以期得到更好的结果。这可能会导致更大的灾难性错误。 - 很多现代语言都用异常。引入异常使得
C++
与Python
,Java
以及其它类C++
的语言更一脉相承。 - 有些第三方 C++ 库依赖异常,禁用异常就不好用了。
- 异常是处理构造函数失败的唯一途径。 虽然可以用工厂函数(acgtyrant 注:
factory function
, 出自C++
的一种设计模式,即「简单工厂模式」)或Init()
方法代替异常, 但是前者要求在堆栈分配内存,后者会导致刚创建的实例处于 ”无效“ 状态。 - 在测试框架里很好用。
缺点
- 启用异常会增加二进制文件数据,延长编译时间(或许影响小),还可能加大地址空间的压力。
我们的系统非常小,以至于异常支持会占用我们大部分的 2K 内存(异常产生的代价是产生的二进制文件大小的增加,因为异常产生的位置决定了需要如何做栈展开(stack unwinding),这些数据需要存储在表里。典型情况,使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升)。 - 在现有函数中添加 throw 语句时,您必须检查所有调用点。要么让所有调用点统统具备最低限度的异常安全保证,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。举例,f() 调用 g(), g() 又调用 h(), 且 h 抛出的异常被 f 捕获。当心 g, 否则会没妥善清理好。
- 异常会彻底扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。您或许会加一大堆何时何处处理异常的规定来降低风险,然而开发者的记忆负担更重了。
- 异常安全需要RAII和不同的编码实践. 要轻松编写出正确的异常安全代码需要大量的支持机制. 更进一步地说, 为了避免读者理解整个调用表, 异常安全必须隔绝从持续状态写到 “提交” 状态的逻辑. 这一点有利有弊 (因为你也许不得不为了隔离提交而混淆代码). 如果允许使用异常, 我们就不得不时刻关注这样的弊端, 即使有时它们并不值得.
- 滥用异常 会变相鼓励开发者去捕捉不合时宜,或本来就已经没法恢复的「伪异常」。比如,用户的输入不符合格式要求时,也用不着抛异常。如此之类的伪异常列都列不完。
大神看法
Google c++
代码风格禁用异常
Google 现有的大多数 C++ 代码都没有异常处理, 引入带有异常处理的新代码相当困难。鉴于 Google 现有代码不接受异常, 在现有代码中使用异常比在新项目中使用的代价多少要大一些. 迁移过程比较慢, 也容易出错 。我们不相信异常的使用有效替代方案, 如错误代码, 断言等会造成严重负担。
Mozilla
的代码风格中关于错误处理也未使用异常
1.尽早并且经常检查错误
2. 使用优雅的宏
3. 发生错误时不要立即返回
Qt
也是禁用异常的
LLVM
禁用异常
In an effort to reduce code and executable size, LLVM does not use exceptions or RTTI (runtime type information, for example, dynamic_cast<>).
That said, LLVM does make extensive use of a hand-rolled form of RTTI that use templates like isa<>, cast<>, and dyn_cast<>. This form of RTTI is opt-in and can be added to any class.
- 美国国防部的联合攻击战斗机(JSF)项目的 C++ 编码规范就禁用异常,因为工具链不能保证抛出异常时的实时性能 。
- Bjarne Stroustrup、Herb Sutter、Scott Meyers 和 Andrei Alexandrescu。这些大神们都认为异常是比返回错误码更好的错误处理方式。
- 陈硕:整个 C++ exception 的行为在常见语言中是最奇葩的, 因为这个语言特性与 C++ 其他 feature(特别是确定性析构) 格格不入。在 C++ 中全面铺开使用异常会遇到其他语言中不存在的问题。
- 吴咏炜的观点:“陈硕当然是个技术大牛。不过,在编程语言这件事上,我更愿意信任 Bjarne Stroustrup、Herb Sutter、Scott Meyers 和 Andrei Alexandrescu。这些大神们都认为异常是比返回错误码更好的错误处理方式。 ”
- **此项应该是已解决:**多个核心上的多个线程应该能够并发地抛出异常,而不会相互干扰。但是不幸的是,在 libstdc + + 和/或 glibc 的当前实现中,栈展开过程采用了一个全局锁 (同时获取共享对象列表,或许还有其他东西) ,它序列化了这些并行抛出异常,可以显著降低程序的速度。
如何选择
若项目有特别严苛的实时性、空间之类的限制,不适用异常;反之,需要使用异常。
异常不能用于某些硬实时项目。
一些硬实时系统就是一个例子: 一个操作必须在一个固定的时间内完成,并有一个错误或正确的答案。在缺乏适当的时间估计工具的情况下,很难保证会出现异常。这样的系统(例如飞行控制软件)通常也禁止使用动态(堆)内存。如上节第五点。
try
语句块和异常处理
throw
如果程序发生异常情况,而在当前的上下文环境中获取不到异常处理的足够信息,我们可以创建一包含出错信息的对象并将该对象抛出当前上下文环境,将错误信息发送到更大的上下文环境中。这称为异常抛出。
throw
是一个C++
关键字,与其后的操作数构成了throw
语句,语法上类似于return
语句。throw
语句必须被包含在try
块之中;可以是被包含在调用栈的外层函数的try
中。
执行throw
语句时,其操作数的结果作为对象被复制构造为一个新的对象 ,放在内存的特殊位置(既不是堆也不是栈,Windows
上是放在“线程信息块TIB
”中)。这个新的对象由本级的try
所对应的catch
语句逐个做类型匹配;如果匹配不成功,则与本函数的外层catch
语句依次做类型匹配;如果在本函数内不能与catch
语句匹配成功,则递归回退到调用栈的上一层函数内从函数调用点开始继续与catch
语句匹配。重复这一过程直到与某个catch
语句匹配成功或者直到主函数main()
都不能处理该异常。
因此,throw
语句抛出的异常对象不同于一般的局部对象。一般的局部对象会在其作用域结束时被析构。而throw
语句抛出的异常对象驻留在所有可能被激活的catch
语句都能访问到的内存空间中。
throw
语句抛出的异常对象在匹配成功的catch
语句的结束处被析构(即使该catch
语句使用的是非“引用”的传值参数类型)。
由于throw
语句都进行了一次副本拷贝,因此异常对象应该是可以copy
构造的。但对于Microsoft Visual C++
编译器,异常对象的复制构造函数即使私有的情形,异常对象仍然可以被throw
语句正常抛出;但在catch
语句的参数是传值时,在catch
语句处编译报错:
cannot be caught as the destructor and/or copy constructor are inaccessible”。
抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型。
异常必须显式地抛出,才能被检测和捕获到;如果没有显式的抛出,即使有异常也检测不到。
throw
表达式
对错误条件发信号,并执行错误处理代码。
-
throw 表达式
首先,从 表达式 复制初始化异常对象:- 这可能会调用右值表达式的移动构造函数。即使复制初始化选择了移动构造函数,从左值复制初始化仍必须为良式,且析构函数必须可访问 (
C++11
起) - 这也可能会通过如 return 语句中一般的两步重载决议调用左值表达式的移动构造函数,如果它们指名局部变量或是函数或 catch 子句的形参,且它的作用域不会超出最内层的外围
try
块(如果存在) (C++17
起) - 复制/移动 (
C++11
起)可能会被复制消除处理
然后转移控制给拥有匹配类型,所在的复合语句或成员初始化器列表是最近进入,且未由此执行线程退出的异常处理块。
- 这可能会调用右值表达式的移动构造函数。即使复制初始化选择了移动构造函数,从左值复制初始化仍必须为良式,且析构函数必须可访问 (
-
thow
:重抛当前处理的异常。中止当前catch
块的执行并将控制转移到下一个匹配的异常处理块(但不是到同一个try
块的下个catch
子句:它所在的复合语句被认为已经‘退出’),并重用既存的异常对象:不会生成新对象。只能在异常处理过程中使用这种形式(其他情况中使用时会调用std::terminate
)。对于构造函数,关联到函数try
块 的catch
子句必须通过重抛出退出。
try {
// 一些危险操作
} catch (const std::bad_alloc&) {
std::cerr << "内存溢出" << std::endl;
} catch (...) {
std::cerr << "意想不到的异常" << std::endl;
// 希望调用者知道如何处理这个异常
throw;
}
动态异常说明(c++17后被移除)
throw
关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification
)
列出函数可能直接或间接抛出的异常。
语法 | 说明 |
---|---|
throw(类型标识列表(可选)) | 显示的动态异常说明 |
这种说明只能在作为类型为函数类型、函数指针类型、函数引用类型、成员函数指针类型的函数、变量、非静态数据成员的声明符的,顶层函数声明符上和形参的声明符或返回类型的声明符上出现。
void f() throw(int); // OK:函数声明
void (*pf)() throw (int); // OK:函数指针声明
void g(void pfa() throw(int)); // OK:函数指针形参声明
typedef int (*pf)() throw(int); // 错误:typedef 声明
//这个函数可能传播std:: runtime_error,
//但并不是说,一个std:: logic_erro
void risky() throw(std::runtime_error);
// 这个函数不能传播任何异常
void safe() throw();
- 动态异常说明不会被认为是函数类型的一部分。
- 如果函数抛出了没有列于其异常说明的类型的异常,那么调用函数
std::unexpected
。默认的该函数会调用std::terminate
,但它可以(通过std::set_unexpected
)被替换成可能调用std::terminate
或抛出异常的用户提供的函数。如果异常说明接受从std::unexpected
抛出的异常,那么栈回溯照常持续。如果它不被接受,但异常说明允许std::bad_exception
,那么抛出std::bad_exception
。否则,调用std::terminate
。
解释第二点: 当违反异常规范时会发生什么?
异常规范的思想是执行一个运行时检查,以确保只从函数中发出特定类型的异常(或者根本不发出任何异常)。例如,下面函数的异常规范保证 f ()
只会发出 A
或 B
类型的异常:
int f() throw( A, B );
如果函数抛出了没有列于其异常说明的类型的异常,那么调用函数 std::unexpected
,例如:
int f() throw( A, B ){
throw C(); // 将会调用unexpected()
}
您可以使用标准的set_unexpected()
函数为意外异常情况注册您自己的处理程序。替换处理程序必须不接受任何参数,并且必须具有 void
返回类型。例如:
void MyUnexpectedHandler() { /*...*/ }
std::set_unexpected( &MyUnexpectedHandler );
剩下的问题是,您的意想不到的处理程序能做什么?它不能做的一件事就是通过一个通常的函数return
返回。它可能会做两件事:
- 它可以决定将异常转换为异常规范所允许的内容,方法是抛出自己的异常,这些异常满足导致它被调用的异常规范列表,然后从中断的地方继续堆栈退出。
- 它可以调用
terminate()
。(terminate()
函数也可以被替换,但必须始终结束程序。)
#include <cstdlib>
#include <exception>
#include <iostream>
class X {};
class Y {};
class Z : public X {};
class W {};
void f() throw(X, Y) {
int n = 0;
if (n) throw X(); // OK
if (n) throw Z(); // OK
throw W(); // 将调用 std::unexpected()
}
int main() {
std::set_unexpected([] {
std::cout << "预料外的异常!" << std::endl; // 需要清除缓冲区
std::abort();
});
f();
}
编译输出
<source>:11:10: warning: dynamic exception specifications are deprecated in C++11 [-Wdeprecated]
11 | void f() throw(X, Y) {
| ^~~~~
<source>: In function 'int main()':
<source>:19:24: warning: 'void (* std::set_unexpected(unexpected_handler))()' is deprecated [-Wdeprecated-declarations]
19 | std::set_unexpected([] {
| ~~~~~~~~~~~~~~~~~~~^~~~~
20 | std::cout << "预料外的异常!" << std::endl; // 需要清除缓冲区
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21 | std::abort();
| ~~~~~~~~~~~~~
22 | });
| ~~
In file included from <source>:2:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/exception:91:22: note: declared here
91 | unexpected_handler set_unexpected(unexpected_handler) _GLIBCXX_USE_NOEXCEPT;
|
输出如下:
预料外的异常!
如何抛出C++
异常
throw 表达式
#include <stdexcept>
int compare( int a, int b ) { //仅用作抛出异常示例展示,实际代码中不应对这种进行负值限制做异常处理
if ( a < 0 || b < 0 ) {throw std::invalid_argument( "接收到负值" );}
}
标准库附带了一个很好的 内置异常对象 , 您可以抛出这些对象。 请记住,**您应该始终 按值抛出 并按引用捕获 **:
try {compare( -1, 3 );}
catch( const std::invalid_argument& e ) {
// 使用异常做一些事情...
}
每次尝试后可以有多个catch()
语句,因此您可以根据需要分别处理不同的异常类型。
您还可以重新抛出异常:
catch( const std::invalid_argument& e ) {
// 做些一些处理
// 如果需要的话,可以让调用堆栈的高层来处理
throw;
}
throw
用作异常规范
异常规范背后的思想很容易理解: 在 C++
程序中,除非另有说明,否则任何函数都可能发出任何类型的异常。考虑一个名为 Func()
的函数:
int Func(); // 可以抛出任何异常
默认情况下,在 C++
中,Func()
确实可以抛出任何东西,正如注释所说。现在,我们通常知道一个函数可能抛出什么类型的东西,然后我们当然有理由向编译器和人类程序员提供一些信息来限制函数可能抛出的异常。例如:
int Func1() throw(int); //只能抛出int类型异常,若抛出其他类型异常,try将无法捕获,只能终止程序
int Gunc() throw(); // 将不会抛出任何异常,若抛出异常,try将无法捕获,只能终止程序
int Hunc() throw(A,B); // 只能抛出A或者B异常
人们可能会自然而然地认为,对函数可能抛出的内容进行声明是件好事,因为信息越多越好。但这却不一定是正确的,因为魔鬼往往产生于细节: 虽然动机是高尚的,但是在 C++
中指定的异常规范的方式并不总是有用的,而且往往是完全有害的。
异常规范与函数定义和函数声明
异常规范不参与函数的类型。动态异常说明不会被认为是函数类型的一部分。
首先考虑一个例子,当异常规范没有参与到函数的类型中:
#include <exception>
#include <iostream>
class A{};
class B {};
void f() throw(A,B) {}
typedef void (*PF)throw(A,B){}; //语法错误
int main() {
PF pf = f;
}
typedef
的throw-specification
是非法的,C + + 不允许您编写这样的代码,因此异常规范不允许参与函数的类型 … … 至少在typedef
的上下文中不允许。但在其他情况下,异常规范确实参与了函数的类型,例如,如果您编写了相同的函数声明,但没有 typedef
,这是ok的
#include <exception>
#include <iostream>
class A{};
class B {};
void f() throw(A,B){}
void (*pf)() throw(A,B);
int main() {
pf = f;
}
顺便说一句, C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。只要目标的异常规范不比源的异常规范更具限制性,就可以对函数的指针进行这种赋值:
#include <exception>
#include <iostream>
class A{};
class B {};
class C {};
void f() throw(A, B) {}
void (*pf)() throw(A, B);
void (*pf1)() throw(A, B, C);
int main() {
pf = f;
pf1 = f;
}
虚函数中的异常规范
当您尝试重写虚函数时,异常规范也会参与到该虚函数的类型中,C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。
#include <exception>
#include <iostream>
class A {};
class B {};
class C {};
class FF {virtual void f() throw(A, B); }; // same ES
class FF1 : FF {void f(); }; // 错误, 异常说明符很重要
int main() { return 0; }
正确写法如下:
#include <exception>
#include <iostream>
class A {};
class B {};
class C {};
class FF {virtual void f() throw(A, B);};
class FF1:public FF {void f() throw(A, B);};
int main() { return 0; }
因此,当今 C++
中存在的异常规范的第一个问题是,它们实际上是一个“影子类型系统”,按照与类型系统其他部分不同的规则运行。
错误的认知
许多人认为异常规范所做的事情:
- 保证函数只会抛出列出的异常(可能没有)。
- 基于仅抛出列出的异常(可能没有)的知识启用编译器优化。
异常规范实际会做的事情:
- 在运行时强制函数只抛出列出的异常(可能没有)。
- 启用或防止编译器优化,必须检查列出的异常是否确实被抛出。
#include <exception>
#include <iostream>
#include <string>
using namespace std;
class A {};
class B {};
class C {};
int Junc() { throw 100;}
int Hunc() throw(A, B, int) { return Junc(); }
int main() { Hunc(); return 0; }
我们看下汇编代码,这段汇编代码同样有助于我们理解栈回溯。
Junc():
push rbp
mov rbp, rsp
mov edi, 4
call __cxa_allocate_exception
mov DWORD PTR [rax], 100
mov edx, 0
mov esi, OFFSET FLAT:_ZTIi
mov rdi, rax
call __cxa_throw
Hunc():
push rbp
mov rbp, rsp
call Junc()
jmp .L7
cmp rdx, -1
je .L5
mov rdi, rax
call _Unwind_Resume
.L5:
mov rdi, rax
call __cxa_call_unexpected
.L7:
pop rbp
ret
main:
push rbp
mov rbp, rsp
call Hunc()
mov eax, 0
pop rbp
ret
将throw 100;
修改为return100;
后的汇编代码如下,编译器做了优化:
Junc():
push rbp
mov rbp, rsp
mov eax, 100
pop rbp
ret
Hunc():
push rbp
mov rbp, rsp
call Junc()
pop rbp
ret
main:
push rbp
mov rbp, rsp
call Hunc()
mov eax, 0
pop rbp
ret
但从功能上讲,编译器必须生成如下代码,并且在运行时它的成本通常与您自己手写一样昂贵(尽管由于编译器为您生成它而减少了输入):
int Hunc()
try {
return Junc();
}
catch( A ){
throw;
}
catch( B ){
throw;
}
catch( ... ){
std::unexpected();
}
这里我们可以更清楚地看到,为什么不让编译器通过假设只抛出某些异常来进行优化,而是恰恰相反: 编译器必须在运行时做更多的工作来强制只抛出那些异常。
如果上述比较晦涩,且看下述示例:
#include <exception>
#include <iostream>
#include <string>
using namespace std;
void func() throw(char *, exception) {
throw 10;
cout << "[1]这个声明将不会被执行。" << endl;
}
int main() {
try {
func();
} catch (int) {
cout << "异常类型:int" << endl;
}
return 0;
}
在GCC
下,这段代码运行到第 7 行时程序会崩溃(terminate called after throwing an instance of 'int'
)。虽然func()
函数中发生了异常,但是由于throw
限制了函数只能抛出 char*、exception
类型的异常,所以 try-catch
将捕获不到异常,只能交给系统处理,终止程序。
调试过程如下:
$g++ -g -o testException testException
$gdb testException -q
Reading symbols from testException...
(gdb) l
1 #include <exception>
2 #include <iostream>
3 #include <string>
4 using namespace std;
5
(gdb)
7 throw 10;
8 cout << "[1]这个声明将不会被执行。" << endl;
9 }
10
(gdb) b 7
Breakpoint 1 at 0x1271: file testException.cpp, line 7.
(gdb) r
Starting program: /mnt/d/Qt_Project/testException
Breakpoint 1, func () at testException.cpp:7
7 throw 10;
(gdb) n
6 void func() throw(char *, exception) {
(gdb) n
terminate called after throwing an instance of 'int'
Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) n
Program terminated with signal SIGABRT, Aborted.
The program no longer exists.
略势在此
除了显示的生成try/catch
块的开销(对于高效的编译器来说可能很小)之外,异常规范通常还有至少两种其他方式会影响运行时性能。首先,一些编译器会自动拒绝内联具有异常规范的函数,就像它们可以应用其他启发式方法一样,比如拒绝内联具有超过一定数量的嵌套语句或包含任何类型的循环构造的函数。其次,一些编译器根本不能很好地优化与异常相关的知识,并且会添加上面显示的try/catch
块,即使可以证明函数体不能抛出。
除了运行时性能之外,异常规范还会增加耦合性,因此可能会浪费编码的时间。例如,从基类虚函数的异常规范中删除一个类型是一种快速而简单的方法,可以在一个巨大的 foop 中破坏大量派生类(如果您正在寻找一种方法)。
结论:不要使用异常规范,c++11都弃用了,你还犹豫啥
Moral #1: Never write an exception specification.
Moral #2: Except possibly an empty one, but if I were you I’d avoid even that.
系统完成,测试结束,甚至包括用户的最后更改。用户用咆哮和嘲讽的方式惊呼,“这只是我们要求的,但不是我们想要的!"。
try
语句块
try
块
将一或多个异常处理块(catch
子句)与复合语句关联
语法:
- 声明一个具名形参的 catch 子句
try { /* */ } catch (const std::exception& e) { /* */ }
- 声明一个无名形参的 catch 子句
try { /* */ } catch (const std::exception&) { /* */ }
- catch-all 处理块,可被任何异常激活
全捕获(catch-all)子句catch (...)
匹配任何类型的异常。如果存在,那么它必须是 处理块序列 中的最后一个catch
子句。全捕获块可以用来确保不可能有异常从提供不抛出异常保证的函数中不被捕获而逃逸。
try { /* */ } catch (...) { /* */ }
示例1
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vect;
vect.push_back(0);
vect.push_back(1);
// 访问第三个元素,也就是不存在
try {
vect.at(2);
} catch (exception& exc) {
cout << "异常情况发生了: " << exc.what()<< endl;
}
return 0;
}
输出结果:
异常情况发生了: vector::_M_range_check: __n (which is 2) >= this->size() (which is 2)
示例2:
#include <iostream>
using namespace std;
double zeroDiv(int a, int b) {
if (b == 0) {
throw "除0 操作!";
}
return (a / b);
}
int main() {
int i = 17;
int j = 0;
double k = 0;
try {
k = zeroDiv(i, j);
cout << k << endl;
} catch (const char* msg) {
cerr << msg << endl;
}
return 0;
}
输出结果:
除0 操作!
- 如果检测完所有
catch
子句后仍然没有匹配,那么就会如throw
表达式中所述,到外围的try
块继续异常的传播。如果没有剩下的外围try
块,那么就会执行std::terminate
(此情况下,由实现定义是否完全进行栈回溯:抛出未捕获的异常可以导致程序终止而不调用任何析构函数)。 - 当进入一个
catch
子句时,如果它的形参是异常类型的基类,那么它会从异常对象的基类子对象进行复制初始化。否则它会从异常对象复制初始化(这个复制遵循复制消除规则)。
函数 try 块
建立围绕整个函数体的异常处理块。
函数 try
块是一种 函数体 的替代语法形式,它是函数定义的一部分。 try 构造函数初始化器(可选) 复合语句 处理块序列
函数 try
块的主要目的是应对从构造函数中的成员初始化器列表抛出的异常,进行记录并重抛,修改异常对象并重抛,抛出一个不同的异常,或终止程序。析构函数或常规函数很少用到它们。
#include <iostream>
#include <string>
struct S {
std::string m;
S(const std::string& str, int idx) try : m(str, idx) {
std::cout << "S(" << str << ", " << idx << ") 构造完成,m = " << m << '\n';
} catch (const std::exception& e) {
std::cout << "S(" << str << ", " << idx << ") 失败:" << e.what() << '\n';
} // 此处有隐含的 "throw;"
};
int main() {
S s1{"ABC", 1}; // 不抛出(索引在范围内)
try {
S s2{"ABC", 4}; // 抛出(越界)
} catch (std::exception& e) {
std::cout << "S s2... 抛出了一个异常:" << e.what() << '\n';
}
}
内置的异常对象std::exception
在头文件 <exception>
中 定义
class exception;
提供一致的接口,以通过throw
表达式处理错误。
标准库所生成的所有异常继承自 std::exception
- logic_error 它报告程序内部错误逻辑所导致的可避免错误,如违背逻辑前提条件或类不变量。
- invalid_argument 报告因参数值未被接受而引发的错误。
- domain_error 输入在操作有定义的定义域外的情形。
- length_error 报告试图超出一些对象的实现定义长度极限所导致的错误。
- out_of_range 报告访问试图受定义范围外的元素所带来的错误。
- future_error(C++11) 为处理异步执行和共享状态( std::future 、 std::promise 等)的线程库中的函数在失败时抛出
- bad_optional_access(C++17) 在访问不含值的 optional 对象时所抛出的异常对象类型。
- runtime_error 报告源于程序作用域外,且不能轻易预测到的错误。
- range_error 能用于报告值域错误(即计算结果不能以目标类型表示的情形)。
- overflow_error 能用于报告算术上溢错误(即计算结果对目标类型过大的情形)。
- underflow_error 可用于报告算术下溢错误(即计算结果是非正规浮点值的情形)。
- regex_error(C++11) 以报告正则表达式库中的错误。
- system_error(C++11) 是多种库函数(通常是与 OS 设施交接的函数,例如 std::thread 的构造函数)在拥有关联于该异常的 std::error_code 时抛出的异常类型,同时可能报告该 std::error_code 。
- ios_base::failure(C++11) 定义输入/输出库中的函数在失败时抛出的异常对象。
- filesystem::filesystem_error(C++17) 定义文件系统库中函数的抛出版重载所抛出的异常对象。
- nonexistent_local_time(C++20) 报告试图转换不存在的 std::chrono::local_time 为 std::chrono::sys_time 而不指定 std::chrono::choose (如 choose::earliest 或 choose::latest )
- ambiguous_local_time(C++20) 报告试图转换有歧义的 std::chrono::local_time 为 std::chrono::sys_time 而不指定 std::chrono::choose (如 choose::earliest 或 choose::latest )
- tx_exception(TM TS) 定义能用于取消并回滚关键词 atomic_cancel 所初始化的原子事务的异常类型。
- format_error(C++20) 定义抛出以报告格式化库中错误的异常对象类型。
- bad_typeid 此类型的异常在应用 typeid 运算符到多态类型的空指针值时抛出。
- bad_cast 在 dynamic_cast 对引用类型运行时检查失败(例如因为类型并非以继承关联)时,还有若请求的刻面不存在于本地环境时从 std::use_facet 抛出此类型异常。
- bad_any_cast(C++17) 在失败时以值返回形式抛出的对象的类型。
- bad_weak_ptr(C++11) std::bad_weak_ptr 是 std::shared_ptr 以 std::weak_ptr 为参数的构造函数,在 std::weak_ptr 指代已被删除的对象时,作为异常抛出的对象类型。
- bad_function_call(C++11) std::bad_function_call 是若函数包装器无目标,则 std::function::operator() 将抛出的异常类型。
- bad_alloc std::bad_alloc 是分配函数作为异常抛出的对象类型,以报告存储分配失败。
- bad_array_new_length(C++11) 是new 表达式作为异常抛出以报告非法数组长度的对象类型
- bad_exception std::bad_exception 是 C++ 运行时在某些情形下抛出的异常类型
- ios_base::failure(C++11 前) 定义输入/输出库中的函数在失败时抛出的异常对象。
- bad_variant_access(C++17) std::bad_variant_access 是在某些情形中抛出的异常类型
用户自定义的异常类
可以使用c++ std::exception
类构造可作为异常抛出的对象。<exception>
头文件包含该类的定义。由类提供的函数what()
是虚成员函数。
这个方法返回一个以空结尾的char *
字符序列。为了获得异常描述,我们可以在派生类中重写它。
#include <exception>
#include <iostream>
using namespace std;
class MyException : public exception {
virtual const char* what() const throw() { return "MyException 异常发生"; }
} newexc;
int main() {
try {
throw newexc;
} catch (exception& exc) {
cout << exc.what() << '\n';
}
return 0;
}
输出:
MyException 异常发生
堆栈展开
栈展开(unwinding)是指当前的try...catch...
块匹配成功或者匹配不成功异常对象后,从try
块内异常对象的抛出位置,到try
块的开始处的所有已经执行了各自构造函数的局部变量,按照构造生成顺序的逆序,依次被析构。如果当前函数内对抛出的异常对象匹配不成功,则从最外层的try
语句到当前函数体的起始位置处的局部变量也依次被逆序析构,实现栈展开,然后再回退到调用栈的上一层函数内从函数调用点开始继续处理该异常。
catch
语句如果匹配异常对象成功,在完成了对catch
语句的参数的初始化(对传值参数完成了参数对象的copy
构造)之后,对同层级的try
块执行栈展开。
由于线程执行时,被调用的函数的参数、返回地址、局部变量等都是依函数调用次序保存在函数调用栈(即线程运行时栈)上。当前被调用函数的参数、局部变量名字可以覆盖掉早前调用函数的同名变量,看起来就是只有当前函数内的名字可以访问,早前调用的函数内部的名字都不可访问,就像磁带被“卷起”。异常处理时按照函数调用顺序的逆序析构,依次析构各个被调函数的局部变量,就类似把已经卷起的“磁带”再展开,抹去上面记录的数据,故此“栈展开”得名。
#include <iostream>
#include <string>
using namespace std;
class MyException {};
class Dummy {
public:
Dummy(string s) : MyName(s) { PrintMsg("Created Dummy:"); }
Dummy(const Dummy& other) : MyName(other.MyName) {
PrintMsg("Copy created Dummy:");
}
~Dummy() { PrintMsg("Destroyed Dummy:"); }
void PrintMsg(string s) { cout << s << MyName << endl; }
string MyName;
int level;
};
void C(Dummy d, int i) {
cout << "Entering FunctionC" << endl;
d.MyName = " C";
throw MyException();
cout << "Exiting FunctionC" << endl;
}
void B(Dummy d, int i) {
cout << "Entering FunctionB" << endl;
d.MyName = "B";
C(d, i + 1);
cout << "Exiting FunctionB" << endl;
}
void A(Dummy d, int i) {
cout << "Entering FunctionA" << endl;
d.MyName = " A";
// Dummy* pd = new Dummy("new Dummy"); //Not exception safe!!!
B(d, i + 1);
// delete pd;
cout << "Exiting FunctionA" << endl;
}
int main() {
cout << "Entering main" << endl;
try {
Dummy d(" M");
A(d, 1);
} catch (MyException& e) {
cout << "Caught an exception of type: " << typeid(e).name() << endl;
}
cout << "Exiting main." << endl;
char c;
cin >> c;
}
运行结果
Entering main
Created Dummy: M
Copy created Dummy: M
Entering FunctionA
Copy created Dummy: A
Entering FunctionB
Copy created Dummy:B
Entering FunctionC
Destroyed Dummy: C
Destroyed Dummy:B
Destroyed Dummy: A
Destroyed Dummy: M
Caught an exception of type: 11MyException
Exiting main.
Scott Meyers : The difference between unwinding the call stack and possibly unwinding it has a surprisingly large impact on code generation. In a noexcept function, optimizers need not keep the runtime stack in an unwindable state if an exception would propagate out of the function, nor must they ensure that objects in a noexcept function are destroyed in the inverse order of construction should an exception leave the function. The result is more opportunities for optimization, not only within the body of a noexcept function, but also at sites where the function is called. Such flexibility is present only for noexcept functions. Functions with “throw()” exception specifications lack it, as do functions with no exception specification at all.
展开调用堆栈和可能展开调用堆栈之间的区别对代码生成有很大的影响。在noexcept
函数中,如果异常从函数传播出去,优化器不需要将运行时堆栈保持在不可缠绕状态,也不需要确保当异常离开函数时,noexcept
函数中的对象按照构造的逆顺序销毁。结果是有更多的优化机会,不仅在noexcept
函数体中,而且在调用该函数的地方。这种灵活性只适用于函数。带有throw()
异常说明的函数缺乏它,就像根本没有异常说明的函数一样。
noexcept
noexcept
关键字有两个目的(就像大多数语言一样):它是编译器的一条指令,而且对阅读代码的人也有帮助。但人类需要知道它有两种不同的含义:
- 此函数永远不会引发异常。它不使用
new
,不调用可能使用new
的库函数,不执行任何可能溢出或下溢的算法,也不以任何其他方式引发任何类型的异常。 - 此函数永远不会抛出可以捕获和恢复的异常。如果它所做的任何事情导致抛出任何类型的异常,那么展开堆栈并执行捕获过程是没有意义的,因为事情处于如此糟糕的状态,恢复是不可能的。就终止。
如下例所示,如果在.cpp
文件中实现其中一个函数,而不是在类声明中内联实现,则关键字是签名的一部分,必须在两个位置都包含它。
#include <iostream>
#include <string>
using namespace std;
class Something {
private:
int x;
public:
Something(int xx) noexcept : x(xx) {}
int getX() noexcept;
void reset() noexcept { x = 0; }
};
int Something::getX() noexcept { return x; }
int main() {}
若在实现中这么写int Something::getX() { return x; }
便会有如下错误:
<source>:14:5: error: declaration of 'int Something::getX()' has a different exception specifier
14 | int Something::getX() { return x; }
| ^~~~~~~~~
<source>:11:9: note: from previous declaration 'int Something::getX() noexcept'
11 | int getX() noexcept;
| ^~~~
如果能够将一个函数标记为noexcept
,那么将以两种完全不同的方式使应用程序更快。
- 首先,编译器不必进行一定量的设置——基本上就是支持堆栈展开的基础设施,以及在进出函数的过程中进行拆除——如果没有异常从它向上传播的话。(您可能会争辩说,这不适用于像我的示例中那样的内联函数,但是大多数函数实际上比演示代码要长得多。)函数调用的次数越多,这一点就越重要。
- 其次,标准库也不例外,当进行一些常见操作(比如增大
vector
)时,它可以用它来决定复制(通常速度较慢)和移动(数量级较快)之间的选择。(为什么?如果push _ back
代码将10个“旧”元素移动到新的、更大的向量中,当其中一个移动抛出一个异常时,你不能只是传播异常,然后继续假装push _ back
从未发生过,因为“旧”向量充满了从无法恢复的元素中移动的元素。因此,如果move
你的vector
元素会抛出异常时,push_back不
会使用move
操作,它会使用较慢的复制) )这同样适用于您的swap
函数——事实上,其他指导原则建议显式地编写您自己的swap并对其进行标记。
int Something::getX() noexcept
{
if (x == 3)
throw std::exception("I refuse to return 3");
return x;
}
运行结果如下:
如果您构造一个值为3的Something
,然后调用getX
,这样可以通过编译,但在程序执行过程中,程序会调用terminate()
以确保遵守不在运行时抛出异常的承诺。没有堆栈展开,也没有机会在调用代码中使用catch
块来处理这种情况。 这可能就是你想要的。但如果不是,那么您不应该将getX
标记为noexcept
。
#include <exception>
#include <iostream>
using namespace std;
class MyException {
public:
MyException(const char *message) : message_(message) {
cout << "MyException ..." << endl;
}
MyException(const MyException &other) : message_(other.message_) {
cout << "Copy MyException ..." << endl;
}
virtual ~MyException() { cout << "~MyException ..." << endl; }
const char *what() const { return message_.c_str(); }
private:
string message_;
};
class MyExceptionD : public MyException {
public:
MyExceptionD(const char *message) : MyException(message) {
cout << "MyExceptionD ..." << endl;
}
MyExceptionD(const MyExceptionD &other) : MyException(other) {
cout << "Copy MyExceptionD ..." << endl;
}
~MyExceptionD() { cout << "~MyExceptionD ..." << endl; }
};
void fun(int n) throw(int, MyException, MyExceptionD) {
if (n == 1) {
throw 1;
} else if (n == 2) {
throw MyException("test Exception");
} else if (n == 3) {
throw MyExceptionD("test ExceptionD");
}
}
void fun2() throw() {}
int main(void) {
try {
fun(2);
} catch (int n) {
cout << "catch int ..." << endl;
cout << "n=" << n << endl;
} catch (MyExceptionD &e) {
cout << "catch MyExceptionD ..." << endl;
cout << e.what() << endl;
} catch (MyException &e) {
cout << "catch MyException ..." << endl;
cout << e.what() << endl;
}
return 0;
}
运行结果:
MyException ...
catch MyException ...
test Exception
~MyException ...
noexcept
是throw()
的改进版本,后者在 C++11
中弃用。与 C++17
前的throw()
不同,noexcept
不会调用 std::unexpected
,并且可能或可能不进行栈回溯,这可能允许编译器实现没有throw()
的运行时开销的 noexcept
。从 C++17
起,throw()
被重定义为严格等价于 noexcept(true)
。
其他
零开销原则
它指出:
- 你不用为你不使用的东西付费。
- 您使用的内容与您可以合理手写的内容一样有效。
一般来说,这意味着不应向C++
添加任何会在时间或空间上施加任何开销的功能,而不是程序员在不使用该功能的情况下引入的开销。
该语言中唯一不遵循零开销原则的两个特性是 运行时类型识别 和 异常 ,这也是为什么大多数编译器都包含一个开关来关闭它们的原因。
理解堆栈跟踪
堆栈跟踪是一个异常列表(或者你可以说一个“Cause by”的列表) ,从最表面的异常(例如服务层异常)到最深的异常(例如数据库异常)。正如我们之所以称之为“堆栈”是因为堆栈是最后一个出现的(FILO) ,最深的异常发生在最初,然后一连串的异常产生了一系列的后果,表面异常是最后一个时刻发生的,但我们会首先看到它。
简单来说,堆栈跟踪是应用程序在抛出异常时所处的方法调用列表。
-
关键1: 这里需要理解的一个棘手而重要的事情是:最深层次的原因可能不是“根本原因”,因为如果你写了一些“糟糕的代码”,它可能会导致一些比其更深的异常。例如,错误的
sql
查询可能会导致底层的SQLServerException
连接重置,而不是简单的语法错误,后者可能只是在堆栈的中间。所以找到中间的根本原因是你的工作。 -
关键 2: 另一个棘手但重要的事情是在每个“Cause by”块内,第一行是最深的层,并且发生在该块的第一位。例如,
Exception in thread "main" java.lang.NullPointerException
at com.example.myproject.Book.getTitle(Book.java:16)
at com.example.myproject.Author.getBookTitles(Author.java:25)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Book.java:16
被Bootstrap.java:14
调用的Auther.java:25
调用,Book.java:16
是根本原因。
更详细的知识点请移步什么是堆栈跟踪,我如何使用它来调试我的应用程序错误?或者直接参考原文What is a stack trace, and how can I use it to debug my application errors?
异常实践忠告
- 在设计的前期开发出一种错误处理策略;
- 用异常做错误处理,异常的
throw
应该没有函数调用那么频繁,C + +
实现倾向于基于异常很少的假设进行优化; - 当更局部的控制机构足以应付时,不要使用异常;
- 采用
RAII
技术去管理资源,以防止内存泄漏; - 并不是每个程序都要求具有异常时的安全性;
- 采用
RAII
技术和异常处理器去维持不变式 - 尽量减少显式
try/catch
的使用,用RAII
技术,而不是显式地处理器代码; - 并不是每个函数都需要处理每个可能的错误,也就是说不要试图捕获每个函数种的每个异常;
- 在构造函数里通过抛出异常指明出现失败,也就是说抛出异常以表明函数无法执行其分配的任务,以使错误处理系统化、健壮化和非重复性。
- 在从赋值种抛出异常之前,使操作对象处于合法状态;
- 避免从析构函数里抛出异常‘
- 让
main()
捕捉并报告所有的异常 - 使正常处理代码和错误处理代码相互分离;
- 在构造函数里抛出异常之前,应保证释放在此构造函数里申请的所有的资源;
- 使资源管理更具有层次性;
- 对于主要界面使用异常描述;
- 当心通过
new
分配的内存在发生异常时没有释放,并由此而导致存储的流失; - 如果一函数可能抛出某个异常,就应假定它一定会抛出异常
- 不要假定所有的异常都是由
exception
类派生出来的; - 库不应该单方面终止程序。相反,应该抛出异常,让调用者去做决定;
- 库不应生成面向最终用户的错误信息。相反,它应该抛出异常,让调用者去做决定;
- 不可能抛出异常时,使用noexcept修饰函数
- 让构造函数建立一个不变量,如果不能,则抛出异常
- 占有资源时,不要抛出异常
- 用户定义的类型可以更好地将有关错误的信息传递给处理程序(而非内置类型)
- 通过值捕获可以适用于小值类型,如枚举值。其他使用const 引用捕获
- 析构函数、释放、交换和异常类型的复制/移动构造一定不能失败
- 如果没有合适的资源句柄可用,则使用
final _ action
对象表示清理 - 如果不能抛出异常,则模拟 RAII 进行资源管理
- 如果不能抛出异常,请考虑快速失败
- 如果不能引发异常,请系统地使用错误代码
- 避免基于全局状态的错误处理(例如
errno
) - 正确地排列你的捕获条款
参考
[1] c++ 中的关键字noexcept
[2] C++11 带来的新特性 (3)—— 关键字noexcept
[3] noexcept specifier
[4] C++ Core Guidelines: The noexcept Specifier and Operator
[5] Exceptions and Error Handling
[6] Google C++ Style Guide:6.7. 异常
[7] Exceptions and Error Handling
[8] Exception Safety: Concepts and Techniques
[9] C++ 异常处理
[10] Error and Exception Handling
[11] C++ Core Guidelines
[12] C++ Core Guidelines
[13] When and How to Use Exceptions
[14] Exception Safety in STLport
[15] 异常
[16] try 块
[17] Exception Handling in C++
[18] noexcept, stack unwinding and performance
[19] Technical Report on C++ Performance
[20] Make Your Code Faster with noexcept
[21] 对使用 C++ 异常处理应具有怎样的态度
[22] Does stack unwinding really require locks?
[23] Mozilla Coding Style Guide
[24] Qt Coding Conventions
[25] Concurrently throwing exceptions is not scalable
[26] JSF air vehicle C++ coding standards
[27] 零开销原则
[28] C++ 异常和替代方案 - Bjarne Stroustrup
[29] C++ throw(抛出异常)详解
[30] How to throw a C++ exception
[31] https://stackoverflow.com/questions/77005/how-to-automatically-generate-a-stacktrace-when-my-program-crashes
[32] A Pragmatic Look at Exception Specifications
[33] 函数 try 块
[34] try 块