首先,开宗明义,如果你不知道到底该不该用异常的话,那答案就是该用。如果你需要避免使用异常,原因必须是你有明确的需要避免使用异常的理由。
如果像C一样通过返回错误码来判断是否出错,如果出错位置离处理错误的位置相差很远(多层嵌套调用),每一层函数调用里都得有判断错误码的代码,这就既对写代码的人提出了严格要求,也对读代码的人造成视觉上的干扰
对于一个矩阵类
class matrix {
…
private:
float* data_;
size_t nrows_;
size_t ncols_;
}
//构造函数
matrix::matrix(size_t nrows,
size_t ncols)
{
data_ = new float[nrows * ncols];
nrows_ = nrows;
ncols_ = ncols;
}
//析构
matrix::~matrix()
{
delete[] data_;
}
};
矩阵乘法
matrix operator*(const matrix& lhs,
const matrix& rhs)
{
if (lhs.ncols != rhs.nrows) {//左阵行数!=右阵列数 无法进行矩阵乘法抛出异常
throw std::runtime_error(
"matrix sizes mismatch");
}
matrix result(lhs.nrows, rhs.ncols);
// 进行矩阵乘法运算
.........
return result;
}
这段代码好像没有对异常进行处理啊,只是throw抛出一个异常。异常处理并不意味着要显式地写try和catch,异常安全的代码可以没有任何try catch。——异常安全指当异常发生时,既不会发生资源泄漏,系统也不会处于一个不一致的状态
矩阵乘法例子中
可能出现异常的地方
创建矩阵对象时,new内存分配。new出错,按照c++规则,会得到异常bad_alloc,对象的构造就失败了。这种情况下,在catch捕捉到这个异常之前,所有的栈上对象全部会被析构,资源全部被自动清理(堆上的东西都是由栈上的变量所引用的,栈上对象析构的过程,堆上相应的资源自然就被释放了—-如果栈上对象的类中析构函数 对其堆上资源进行释放则在栈上对象析构时堆上资源也会被正常释放—就是之前讲的RAII。而且被释放的对象的范围还被栈帧限定了。)
乘法左阵行数!=右阵列数 无法进行矩阵乘法抛出异常
在乘法中对result对象进行构造出现内存分配失败,像上面讲的那样,result对象根本没有构造出来,并且传入的lhs,rhs的析构函数会自动被调用两个传入的对象自动被析构
避免异常的风格指南
Google 的 C++ 风格指南是不使用异常,而使用错误码或断言。(但他们现在有点后悔)
美国国防部的联合攻击战斗机(JSF)项目的 C++ 编码规范就禁用异常,因为工具链不能保证抛出异常时的实时性能。不过在那种项目里,被禁用的 C++ 特性就多了,比如动态内存分配都不能使用。
一些游戏项目为了追求高性能,也禁用异常。这个实际上也有一定的历史原因,因为今天的主流 C++ 编译器,在异常关闭和开启时应该已经能够产生性能差不多的代码(在异常未抛出时)。代价是产生的二进制文件大小的增加,因为异常产生的位置决定了需要如何做栈展开,这些数据需要存储在表里。典型情况,使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升。
异常的问题
异常不是一个完美的特性,对它的批评主要有两条
1 异常违反了 “你不用就不需要付出代价”的c++原则,只要开启了异常即使不使用异常,你编译出的二进制代码通常也会膨胀
目前的主流异常实现中,都倾向于牺牲可执行文件大小、提高主流程的性能,只要程序不抛出异常,c++代码的性能比起完全不做错误检查的代码,都只有几个百分点的性能损失
2 异常比较隐蔽,不容易看出哪些地方会发生异常和发生什么异常
和java不同,c++在编译时不会对异常规约进行检查,从C++17开始,C++甚至完全禁止了以往的动态异常规约(你不能在函数声明里写可能会抛出某某异常),你唯一能声明的就是某函数不会抛出异常 noexcept、noexcept(true)或throw(c++11已经将thow抛弃 在c++11以前为函数加throw()表示保证函数内不会抛出任何异常 throw(int)会抛出int型异常 throw(…)会抛出某个不知道类型的异常)。 如果一个函数声明了不会抛出异常、但是在运行时抛出了异常,c++会调用std::terminate来终止程序。 无法声明会抛出什么类型的异常,编译器不会对代码中的异常检测代码进行检查,是c++异常的最大问题
不声明异常是有理由的,特别是在泛型编程的代码中,几乎无法预知会发生什么异常
1 要写异常安全的代码,尤其在模板里。 尽可能提供强异常安全保证(若函数抛出异常,则保证程序的状态会被回滚到该函数调用前的状态),保证任何第三方代码发生异常的情况下,不改变对象的内容,也不产生任何资源泄漏。
2 如果你的代码可能抛出异常,则在文档里明确声明可能发生的异常类型和发生条件。确保使用人在不检查你的代码的情况下 了解需要准备哪些异常处理
3 对于肯定不会抛出异常的代码,将其标记为noexcept,类的构造函数、析构函数、赋值函数等如果他们调用的代码都是noexcept的话,他们会自动成为noexcept。 所以像swap这样的成员函数尽可能标记成noexcept(swap在op= 以及构造函数中经常用到)
自动生成的特殊成员函数(默认构造,默认赋值)会是 noexcept
只有析构函数默认有noexcept 声明(前提是所有的基类和成员变量的析构函数都 noexcept)。构造函数函数如果不是 default 声明的话(编译器提供的默认构造),仍需手工标 noexcept。
不要在析构函数里抛异常——除非你是C++专家,知道所有的语法细节和特殊处理逻辑,知道你为什么要打破规则
使用异常的理由
标准库的错误处理方式就是异常,其中不仅包括运行时错误还有一些逻辑错误,都是通过异常来处理。
比如对于可以用[]下标访问元素的容器,还可以使用at成员函数,能够在下标不存在的时候抛出异常,作为一种额外的帮助调试的手段。
#include <iostream> // std::cout/endl
#include <stdexcept> // std::out_of_range
#include <vector> // std::vector
using namespace std;
vector<int> v{1, 2, 3};
v[0] // 1
v.at(0) //1
v[3] //越界 输出的是乱码 -1342175236
try {
v.at(3);//越界抛出out_of_range异常
}
catch (const out_of_range& e) {
cerr << e.what() << endl;
}
//_M_range_check: __n (which is 3) >= this->size() (which is 3)
c++标准容器在大部分情况下提供了强异常保证,即异常一旦发生现常会被恢复到调用产生异常的函数前的状态,容器的内容不会发生变化,也没有任何资源泄漏。就比如之前说的vector push_back,如果你传入一个右值,并且定义了移动构造。但是只要移动构造没有标记为noexcept,就不会调用移动构造而是调用拷贝构造。 因为如果在移动中抛出异常将会导致被移动的元素被破坏,不能使用只能析构,异常的安全性就不能保证了。
只要使用了标准容器,不管你用不用异常都得处理标准容器可能引发的异常—至少有bad_alloc(new 申请堆上内存失败),除非你明确知道你的目标运行环境不会产生这个异常。
对于代码中的逻辑错误,开发者可以选择不同的处理方式的:你可以使用异常,也可以使用 assert,在调试环境中报告错误并中断程序运行。由于测试通常不能覆盖所有的代码和分支,assert 在发布模式下一般被禁用,两者并不是完全的替代关系。在允许异常的情况下,使用异常可以获得在调试和发布模式下都良好、一致的效果。
标准 C++ 可能会产生哪些异常
https://time.geekbang.org/column/article/175579?cid=100040501
Boost.Asio 这个将是未来 C++ 网络标准库的基础。
不要把异常安全和noexcept混淆了
异常安全有四级:
不抛异常(noexcept)保证
强异常安全保证
基本安全异常保证
没有任何保证
你需要写出 try 和 catch 的地方很少。很多地方的异常处理,就是让程序优雅地通过异常退出当前函数。
就像我文中描述的“matrix c = a * b;”这句可能出异常的地方有好几处,目前的代码写法,也在某种意义上“处理”了异常——确保发生异常时程序行为的完全正常,即给出了异常安全保证。
又如异常安全的代码,常常是让会抛异常的操作最先做(如内存分配),然后再做其他不会抛异常的操作。这样的代码,一般不需要写 try… catch,也同样能在异常情况执行正确的流程。
外围(比如main里)当然是要写catch的。(我们一般也不会主动去调terminate;退出的话一般用exit。)