C++异常的23个问题

本篇博文翻译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》。

常见的反对使用异常的观点:

  • “但是异常是昂贵的(增加程序开销)!”,这不见得的是正确的,现代C++对使用异常的开销降低了几个百分点(3%),这是和不进行错误处理相比较得到的。(Modern C++ implementations reduce the overhead of using exceptions to a few percent (say, 3%) and that’s compared to no error handling.) 使用返回错误码并进行测试来处理错误也不是没有开销的,根据经验,如果不抛出异常时,异常的开销是很小的,在一些实现上(编译器)这种是没有开销的,所有的开销都是在抛出异常是产生的,也就是说如果程序没有出错,异常将比“错误码+测试”方式更高效,如果程序出错才会产生开销。
  • “但是在JSF++中,Stroustrup(C++之父)完全禁止使用异常!”,JSF++是用于开发硬实时和安全性至关重要的应用的(比如飞行控制程序),如果计算时间过长就会有人因此而死,出于这样的原因才禁止使用异常保证响应时间,甚至这种情况下手动释放内存也是被禁止的!Actually, the JSF++ recommendations for error handling simulate the use of exceptions in anticipation of the day where we have the tools to do things right, i.e. using exceptions.
  • “但是从构造函数中抛出异常会导致内存泄漏!”,胡说八道!这是一个编译器bug造成的无稽之谈,并且这个bug在十多年前就立刻被修复了。

二、该怎样使用异常?

可以参考《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++异常机制是为了支持错误处理而设计的

  • 仅适用throw报告错误信息(which means specifically that the function couldn’t do what it advertised, and establish its postconditions)
  • 只有在明确知道发生的错误可以处理时,才使用catch指定错误处理操作(也可能是将捕获的异常转换成其他类型再次抛出,比如捕获一个bad_alloc异常,重新抛出一个no_space_for_file_buffers异常)
  • 在使用函数时不要使用异常来表明编码错误(coding error),使用assert或者其他将进程发送到调试器的机制或者使进程崩溃,通过转储信息来调试
  • 当变量的值不是合理的,此时不要使用异常来处理,使用assert或者其他终止程序的机制,抛出异常并不能修复损坏的内存并且可能会进一步损坏其他重要数据

异常还有一些在其他语言中常用的用法,但是在C++中不是常用的并且编译器也没有很好的支持(编译器对异常的优化是假设异常用于错误处理)
特别强调下,不要使用异常控制执行流,不要简单的将异常作为从函数返回值的方法(类似于return),这样效率很低,并且会使习惯用异常作为错误处理的C++程序员感到困惑,类似的,也不要使用throw跳出循环

使用try/catch/throw为什么可以调高代码质量?

原因之一是消除if语句
try/catch/throw常用的替代方法是return一个返回码(也称为错误码),调用者通过条件语句(比如if语句)显式的测试返回码,例如,使用printf(), scanf()和malloc()时要测试返回码看函数是否成功调用
虽然有时候返回错误是最合适的错误处理技术,但是添加不必要的if语句会带来一些糟糕的副作用:

  • 降低代码质量:众所周知,条件语句出错的可能性是其他语句的十倍,所以在其他条件不变的情况下,如果能消除代码中的if语句,代码将更加健壮
  • 拖慢项目上线:条件语句的数量与白盒测试所需的测试用例数量息息相关,所以不必要的条件语句会增加测试时间,如果不测试每个分支的话,当用户/客户在使用时出现问题,那就糟糕了
  • 增加开发成本:不必要并且复杂的控制流会增加bug出现的概率,而发现bug,修复bug和测试大大增加了开发成本

因此,使用try/catch/throw进行错误处理要比使用返回错误的方式bug更少,开发成本更低,上线时间更快。当然如果你所在的组织没有使用try/catch/throw经验的话,你应该先在一些玩具项目中使用确保你知道你在做什么–就像你在携带一件武器上战场之前,应该习惯它的射程

你可能感兴趣的:(C/C++,编程语言,c++)