本篇博文翻译Standard C++中的一篇C++异常相关问题的文章
原文:https://isocpp.org/wiki/faq/exceptions,翻译内容如下:
使用异常有什么好处呢?回答一般为:“使用异常机制进行错误处理可以让代码更加简洁,更不容易遗漏错误情况”。之前用错误码errno和if语句处理错误有什么的问题呢?回答一般为:“使用错误码和if语句处理错误会使错误处理代码与正常的代码杂糅在一起,很难保证处理了所有的错误情况”。
首先一些情况不用异常处理是不行的,比如如果在构造函数中出现了错误如何报告错误呢?我们先来看如果没有异常,该如何处理这种情况?请记得构造函数是用来初始化/构造变量中的对象的,比如:
vector v(10000); // needs to allocate memory
ofstream os("myfile"); // needs to open a file
vector或者ofstream类的构造函数中出现错误时,会将一个变量的状态设置为bad,这样后续操作都会失败。因此创建对象之后对状态的判断就至关重要了,比如:对于ofstream,如果忘记检查打开文件的操作是否成功,则可能会导致要输出的内容丢失。因此要求代码写成如下形式:
vector v(10000); // needs to allocate memory
if(v.bad()){ /* handle error */} // vector doesn't actually have a bad(); it relies on exceptions
ofstream os("myfile"); // needs to open a file
if(os.bad()){ /* handle errro */}
这就意味着要额外测试每个对象,这对由多个对象组成的类来说非常麻烦,尤其是那些子对象相互依赖的情况。因此这种情况只能抛出异常。这是RAII(Resource Acquisition Is Initialization)技术的基础。更多信息可以参考《The C++ Programming Language》第8.3节、第14章和附录E或者论文《 Exception safety: Concepts and techniques》。
再来看如果没有异常,普通函数该如何报告错误?既可以通过返回一个错误码也可以通过设置一个全局变量(比如errno),值得注意的是除非函数调用之后立即检测设置的全局变量,否则这种方式有问题(因为其他函数可能会重置这个全局变量),如果多个线程都可能访问这全局变量,则不要采用这种报告错误的方式。返回错误码方式的问题在于选择错误码可能需要一些技巧,并且一些情况下是选取不到合适的返回码的,比如:
double d = my_sqrt(-1); // return -1 in case of error
if(d == -1){ /* handle error */}
int x = my_negate(INT_MIN); // Duh?
对于my_sqrt()可以返回-1报告错误,但是my_negate()却找不到一个合适的错误码,因为对于int类型除了-(2^31)其他值都是另一个int类型值的相反数,所以此时my_negate(INT_MIN)会出错,但是找不到一个合适的错误码。这种情况我们就需要返回值(不要忘记测试)。更多的例子和解释可以查看Stroustrup的《Beginning programming book》。
常见的反对使用异常的观点:
可以参考《The C++ Programming Language》第8.3节、第14章和附录E,附录主要介绍了在高要求的应用程序中编写异常安全代码的技术,它不是为新手编写的
在C++中,异常被用来报告一些没法在本地(当前代码)处理的错误,比如在构造函数中获取资源失败
例如:
class VectorInSpecialMemory {
int sz;
int* elem;
public:
VectorInSpecialMemory(int s)
: sz(s)
, elem(AllocateInSpecialMemory(s))
{
if (elem == nullptr)
throw std::bad_alloc();
}
...
};
不要将异常作为从函数中返回值的方法!正如一些使用者假设的(也是语言定义所鼓励的):异常处理代码是错误处理代码,并且不断优化的异常实现也验证了这种假设
RAII是使用带有析构函数的类对资源管理施加顺序的关键技术,例如:
void fct(string s)
{
File_handle f(s,"r"); // File_handle's constructor opens the file called "s"
// use f
} // here File_handle's destructor closes the file
如果fct()“use f”部分抛出了异常,析构函数仍然会被调用,文件也会正确的关闭,这与下面常用的不安全的写法形成了对比:
void old_fct(const char* s)
{
FILE* f = fopen(s,"r"); // open the file named "s"
// use f
fclose(f); // close the file
}
如果old_fct()“use f”部分抛出了异常或者简单的执行return语句,文件都不会关闭。在C语言中,longjmp是一个额外的风险
C++异常机制是为了支持错误处理而设计的
异常还有一些在其他语言中常用的用法,但是在C++中不是常用的并且编译器也没有很好的支持(编译器对异常的优化是假设异常用于错误处理)
特别强调下,不要使用异常控制执行流,不要简单的将异常作为从函数返回值的方法(类似于return),这样效率很低,并且会使习惯用异常作为错误处理的C++程序员感到困惑,类似的,也不要使用throw跳出循环
原因之一是消除if语句
try/catch/throw常用的替代方法是return一个返回码(也称为错误码),调用者通过条件语句(比如if语句)显式的测试返回码,例如,使用printf(), scanf()和malloc()时要测试返回码看函数是否成功调用
虽然有时候返回错误是最合适的错误处理技术,但是添加不必要的if语句会带来一些糟糕的副作用:
因此,使用try/catch/throw进行错误处理要比使用返回错误的方式bug更少,开发成本更低,上线时间更快。当然如果你所在的组织没有使用try/catch/throw经验的话,你应该先在一些玩具项目中使用确保你知道你在做什么–就像你在携带一件武器上战场之前,应该习惯它的射程