Skip to content

Latest commit

 

History

History
475 lines (399 loc) · 15.6 KB

exception.md

File metadata and controls

475 lines (399 loc) · 15.6 KB

异常

为什么使用异常

  • 使用异常处理错误使得代码更简单、更干净,并且更不可能错过错误。使用 errnoif 语句使得错误处理和普通代码紧密缠绕,因此代码更加凌乱,也更难确保已经处理了所有的错误。
  • 构造函数的工作是创建类的不变性(创建成员函数运行的环境),这经常需要获取如内存、锁、文件、套接字等资源,即 RAII(Resource Acquisition Is Initialization)。
  • 报告一个构造函数检查到的错误需要抛异常实现。

C++ 使用异常

  • 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);//额。。。

使用 try/catch/throw 而不是条件判断和返回错误码来改善软件质量

  • 条件语句更易犯错
  • 延迟发布时间:白盒测试需要覆盖所有条件分支
  • 增加开发花费:非必须的条件控制增加了发现 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” 的,因此在 throwcatch 之间的栈帧会被弹出。
    • 在 “stack unwinding” 过程中,这些栈帧中的所有局部变量会被析构。如果其中一个析构函数抛出异常,C++ 运行时系统将进入 “no-win” 状态:两个异常只能处理一个,忽视任何一个都会丢失信息。
    • 此时 C++ 会调用 terminate() 终止进程。即在发生异常的情况下调用析构函数抛出异常会导致程序崩溃。因此避免的方法就是永远不要在析构函数抛异常。

抛出什么异常

  • 抛出对象。如果可以,写子类继承自 std::exception 类,可以提供更多关于异常的信息

捕获什么异常

  • 可以的话,捕获异常的引用:拷贝可能会有不同的行为;指针则不确定是否需要删除指向异常的指针

throw 再次抛异常

  • 可用于实现简单的 “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;
    }
};

参考