- 使用异常处理错误使得代码更简单、更干净,并且更不可能错过错误。使用
errno
和if
语句使得错误处理和普通代码紧密缠绕,因此代码更加凌乱,也更难确保已经处理了所有的错误。 - 构造函数的工作是创建类的不变性(创建成员函数运行的环境),这经常需要获取如内存、锁、文件、套接字等资源,即 RAII(Resource Acquisition Is Initialization)。
- 报告一个构造函数检查到的错误需要抛异常实现。
- C++ 中,异常用于指示内部不能处理的错误,比如构造函数内部获取资源失败。
- 不要使用异常作为函数的返回值。
- C++ 使用异常来支持错误处理:
- 使用
throw
指示错误(函数不能处理错误,或者暴露错误的后置条件)。 - 在知道可以处理错误的时候使用
catch
指定错误处理行为(可以翻译成另一种类型并且重新抛出)。 - 不要使用
throw
指示调用函数的代码错误。而是使用assert
或其他机制,或者发送进程给调试器,或者使得进程崩溃并收集崩溃日志以便程序员调试。 - 当发现对组件不变式的意外违反时,不要使用
throw
,使用throw
或其他机制来终止程序。抛出异常不能解决内存崩溃甚至会导致后续使用数据的错误。
- 使用
- 异常是昂贵的:和没有错误处理相比,现代 C++ 实现已经将异常的负载降到 3% 左右。正常情况不抛异常,比使用返回值和检查代码运行更快。只有出现错误才会有负载。
- JSF++ 禁止异常:JSF++ 是硬实时和严格安全性的应用(飞机控制系统)。我们必须保证响应时间,所以我们不能使用异常,甚至禁止使用释放分配的存储。
- 使用 new 调用构造函数抛异常会导致内存泄漏:这是旧编译器的 bug,现在早已经解决了。
T *p= new T;//将被编译器转换给类似下面的代码
void allocate_and_construct()
{
// 第一步,分配原始内存,若失败则抛出bad_alloc异常
try
{
// 第二步,调用构造函数构造对象
new (p)T; // placement new: 只调用T的构造函数
}
catch(...)
{
delete p; // 释放第一步分配的内存
throw; // 重抛异常,通知应用程序
}
}
ofstream os("myfile");//需要打开一个文件
if(os.bad()) { /*打开失败需要处理错误*/ }
- 可以通过函数返回一个错误码或设置一个局部变量(如 errno)。
- 不使用全局变量:全局变量需要立即检查,因为其他函数可能会重置它;多线程也会有问题。
- 这就需要测试每个对象。当类由许多对象组成,尤其是这些子对象互相依赖时,会导致代码一团糟。
- 但是检查返回值要求智慧甚至不可能达到目的。比如下面的代码
- 对于 my_negate 函数,每一个 int 返回值都是正确的,但是当使用二进制补码表示的时候,是没有最大负数的,可参考C语言中INT_MIN的一些问题。这种情况下,就需要返回值对,分别表示错误码和运算结果。
double d = my_sqrt(-1);//错误返回 -1
if(d == -1) { /*处理错误*/ }
int x = my_negate(INT_MIN);//额。。。
- 条件语句更易犯错
- 延迟发布时间:白盒测试需要覆盖所有条件分支
- 增加开发花费:非必须的条件控制增加了发现 bug、解决 bug 和测试的复杂度
- 检测到错误的代码通常需要传递错误信息,这可能是多层函数调用,这种情况下每一层调用函数都需要添加判断代码和返回值;而异常可以更简洁、干净地传递错误信息到可以处理错误的调用者
- 使用异常
void f1()
{
try {
// ...
f2();
// ...
} catch (some_exception& e) {
// ...code that handles the error...
}
}
void f2() { ...; f3(); ...; }
// f3 到 f9 逐层调用,f9 调用 f10
void f10()
{
// ...
if ( /*...some error condition...*/ )
throw some_exception();
// ...
}
- 不使用异常
int f1()
{
// ...
int rc = f2();
if (rc == 0) {
// ...
} else {
// ...code that handles the error...
}
}
int f2()
{
// ...
int rc = f3();
if (rc != 0)
return rc;
// ...
return 0;
}
// f3 到 f9 都需要增加判断代码
int f10()
{
// ...
if (...some error condition...)
return some_nonzero_error_code;
// ...
return 0;
}
Number 类支持加减乘除 4 种基本运算,但是加会溢出,除会导致除 0 错误或向下溢出等等
- 使用异常
void f(Number x, Number y)
{
try {
// ...
Number sum = x + y;
Number diff = x - y;
Number prod = x * y;
Number quot = x / y;
// ...
}
catch (Number::Overflow& exception) {
// ...code that handles overflow...
}
catch (Number::Underflow& exception) {
// ...code that handles underflow...
}
catch (Number::DivideByZero& exception) {
// ...code that handles divide-by-zero...
}
}
- 不使用异常
int f(Number x, Number y)
{
// ...
Number::ReturnCode rc;
Number sum = x.add(y, rc);
if (rc == Number::Overflow) {
// ...code that handles overflow...
return -1;
} else if (rc == Number::Underflow) {
// ...code that handles underflow...
return -1;
} else if (rc == Number::DivideByZero) {
// ...code that handles divide-by-zero...
return -1;
}
Number diff = x.sub(y, rc);
if (rc == Number::Overflow) {
// ...code that handles overflow...
return -1;
} else if (rc == Number::Underflow) {
// ...code that handles underflow...
return -1;
} else if (rc == Number::DivideByZero) {
// ...code that handles divide-by-zero...
return -1;
}
Number prod = x.mul(y, rc);
if (rc == Number::Overflow) {
// ...code that handles overflow...
return -1;
} else if (rc == Number::Underflow) {
// ...code that handles underflow...
return -1;
} else if (rc == Number::DivideByZero) {
// ...code that handles divide-by-zero...
return -1;
}
Number quot = x.div(y, rc);
if (rc == Number::Overflow) {
// ...code that handles overflow...
return -1;
} else if (rc == Number::Underflow) {
// ...code that handles underflow...
return -1;
} else if (rc == Number::DivideByZero) {
// ...code that handles divide-by-zero...
return -1;
}
// ...
}
- 使用异常
void f() // Using exceptions
{
try {
GResult gg = g();
HResult hh = h();
IResult ii = i();
JResult jj = j();
// ...
}
catch (FooError& e) {
// ...code that handles "foo" errors...
}
catch (BarError& e) {
// ...code that handles "bar" errors...
}
}
- 不使用异常
int f() // Using return-codes
{
int rc; // "rc" stands for "return code"
GResult gg = g(rc);
if (rc == FooError) {
// ...code that handles "foo" errors...
} else if (rc == BarError) {
// ...code that handles "bar" errors...
} else if (rc != Success) {
return rc;
}
HResult hh = h(rc);
if (rc == FooError) {
// ...code that handles "foo" errors...
} else if (rc == BarError) {
// ...code that handles "bar" errors...
} else if (rc != Success) {
return rc;
}
IResult ii = i(rc);
if (rc == FooError) {
// ...code that handles "foo" errors...
} else if (rc == BarError) {
// ...code that handles "bar" errors...
} else if (rc != Success) {
return rc;
}
JResult jj = j(rc);
if (rc == FooError) {
// ...code that handles "foo" errors...
} else if (rc == BarError) {
// ...code that handles "bar" errors...
} else if (rc != Success) {
return rc;
}
// ...
return Success;
}
- 使用异常处理错误需要付出
- 异常处理要求原则和严谨:需要学习;
- 异常处理不是万能药:如果团队是草率没有纪律的,那么使用异常和返回值都会有问题
- 异常处理不是通用的:应当知道什么条件应该使用返回值,什么条件使用异常
- 异常处理会鞭策学习新技术
-
当不能正确初始化或构造一个对象时,应该在构造函数内部抛出异常
- 构造函数没有返回值,所以不能使用返回错误码的方式
- 最差的方式是使用一个内部状态码来判断是否构造成功,但是需要在每次调用构造函数的时候使用
if
检查状态码,或者在成员函数内部增加if
检查
-
构造函数抛异常也不会有内存泄漏
- 构造函数抛异常时,对象的析构函数不会运行。因为对象的生命周期是构造函数成功完成或返回,抛异常表示构造失败,生命周期没有开始。因此需要将 undone 的东西保存在对象的数据成员
- 比如使用智能指针保存分配的成员对象,而不是保存到原始的 Fred* 数据成员
// Fred.h #include <memory> class Fred { public: //typedef 简化了使用 Fred 对象的语法,可以使用Fred::Ptr 取代 std::unique_ptr<Fred> typedef std::unique_ptr<Fred> Ptr; // ... }; //调用者 cpp #include "Fred.h" void f(std::unique_ptr<Fred> p); // explicit but verbose void f(Fred::Ptr p); // simpler void g() { std::unique_ptr<Fred> p1( new Fred() ); // explicit but verbose Fred::Ptr p2( new Fred() ); // simpler // ... }
- 析构函数抛异常会导致异常点之后的代码不能指向,可能造成内存泄漏问题
- 可以在析构函数抛异常,但是该异常不能出析构函数,即需要在析构函数内部使用
catch
捕获异常。否则会破坏标准库和语言的规则。 - 处理方式是:
- 可以写信息到日志文件,终止进程。
- 提供一个普通函数执行可能抛异常的操作,给客户处理错误。
- C++ 规则是异常的 “栈展开(stack unwinding)” 进程中调用的析构函数不能抛异常:
- “stack unwinding”:当抛出一个异常时,栈是 “unwound” 的,因此在
throw
和catch
之间的栈帧会被弹出。 - 在 “stack unwinding” 过程中,这些栈帧中的所有局部变量会被析构。如果其中一个析构函数抛出异常,C++ 运行时系统将进入 “no-win” 状态:两个异常只能处理一个,忽视任何一个都会丢失信息。
- 此时 C++ 会调用
terminate()
终止进程。即在发生异常的情况下调用析构函数抛出异常会导致程序崩溃。因此避免的方法就是永远不要在析构函数抛异常。
- “stack unwinding”:当抛出一个异常时,栈是 “unwound” 的,因此在
- 抛出对象。如果可以,写子类继承自
std::exception
类,可以提供更多关于异常的信息
- 可以的话,捕获异常的引用:拷贝可能会有不同的行为;指针则不确定是否需要删除指向异常的指针
- 可用于实现简单的 “stack-trace”,即堆栈跟踪,在程序重要函数内部增加
catch
语句
class MyException {
public:
// ...
void addInfo(const std::string& info);
// ...
};
void f()
{
try {
// ...
}
catch (MyException& e) {
e.addInfo("f() failed");
throw;//再次抛出当前异常
}
}
- 也可用于 “exception dispatcher”,即异常分发
void handleException()
{
try {
throw;
}
catch (MyException& e) {
// ...code to handle MyException...
}
catch (YourException& e) {
// ...code to handle YourException...
}
}
void f()
{
try {
// ...something that might throw...
}
catch (...) {
handleException();
}
}
- 不是所有编译器支持异常捕获(exception-try-block),只有 GCC 和大多数新版本的 MSVC 支持。
- 初始化的异常不能被隐藏:构造函数内的异常处理部分必须抛出一个异常,或重新抛出捕获的异常。下面两个版本的代码是等价的
// Version 1
struct A
{
Buf b_;
A(int n)
try
: b_(n)
{
cout << "A initialized" << endl;
}
catch(BufError& )
{
cout << "BufError caught" << endl;
}
};
// Version 2
struct A
{
Buf b_;
A(int n)
try
: b_(n)
{
cout << "A initialized" << endl;
}
catch(BufError& be)
{
cout << "BufError caught" << endl;
throw;
}
};