0%

3 异常

C++增加了exceptions性质后,可能令人不舒服的改变了很多事情。原始指针的使用如今称为高风险行为,资源泄露的机会大增,撰写符合期望的constructors和destructors的难度也大增。程序员特别小心,放置程序在执行时突然中止。可执行文件和程序库变得更大、速度更慢。
为什么使用exceptions?答案很简单:exceptions无法被忽视。如果一个函数利用设定状态变量的方式或是利用返回错误码的方式发出一个异常,无法保证此函数的调用者会检查那个变量或检验那个错误码。于是程序不断执行,远离错误发生地点。但如果函数以抛出exception的方式发出异常信号,而该exception未被捕捉,程序的执行立刻中止。
C只有setjmplongjmp才能近似这样的行为。但longjmp在C++中有缺陷:当它调整栈(stack)的时候,无法调用局部(local)对象的destructors。C++很依赖destructors被调用。

条款9:利用destuctors避免资源泄露

  1. 有个例子,加入你现在要写一个软件。收养中心每天会产生一个文件,其中有它所安排的当天收养个案,你的工作是写程序读文件,然后为每个个案做适当处理:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // ALA: abstract base class
    class ALA
    {
    public:
    virtual void processAdoption() = 0;
    ...
    };
    class Puppy : public ALA
    {
    public:
    virtual void processAdoption();
    ...
    };
    class Kitten : public ALA
    {
    public:
    virtual void processAdoption();
    ...
    };
    ALA* readALA(istream &s);
    // 程序核心大致
    void processAdoptions(istream &dataSource)
    {
    // 如果还有数据
    while (dataSource)
    {
    // 取出下一只动物
    ALA *pa = readALA(dataSource);
    // 处理收养事宜
    pa->processAdoption();
    // 删除readALA返回的对象
    delete pa;
    }
    }
    如果pa->processAdoption抛出一个exception怎么办,很简单:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void processAdoptions(istream &dataSource)
    {
    while (dataSource)
    {
    ALA *pa = readALA(dataSource);
    try {
    pa->processAdoption();
    } catch (...) { // 捕捉
    delete pa; // delete,避免泄露
    throw; // 传播给调用端
    }
    delete pa;
    }
    }
    这样程序被try语句块和catch语句块搞得乱七八糟。解决方法是以一个类似指针的对象取代指针pa,这样当这个类指针对象被(自动)销毁,可以令其destructor调用delete。这就是smart pointers。C++标准库有,它看起来是这样子的(此书有点旧):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<class T>
    class auto_ptr
    {
    public:
    auto_ptr(T *p = 0) : ptr(p) {}
    ~auto_ptr() { delete ptr; }
    private:
    T *ptr;
    }
    auto_ptr不适合delete数组对象的指针。如果你希望有这样的auto_ptr用在数组上,要么自己写一个,要么最好使用一些容器,比如vector来取代。用auto_ptr对象取代原始指针后,processAdoptions看起来这样子:
    1
    2
    3
    4
    5
    6
    7
    8
    void processAdoptions(istream &dataSource)
    {
    while (dataSource)
    {
    auto_ptr<ALA> pa(readALA(dataSource));
    pa->processAdoption();
    }
    }
    它和原版本的差异只有两处。一是pa被声明为一个auto_ptr<ALA>对象;二是循环最后不再有delete语句。它是隐藏在auto_ptr背后的观念——以一个对象存放必须自动释放的资源,并依赖该对象的destructor释放。

条款10:在constructors内阻止资源泄露(resource leak)

  1. 对于尚未完全构造好的对象,C++拒绝调用其destructor。由于C++不自动清理构造期间抛出exceptions的对象,所以设法让constructor在那种情况下也能自我清理。这通常将可能的exceptions捕捉起来,执行清理工作,然后重新抛出exception。让它继续传播:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    class Image 
    {
    public:
    Image(const string &imageDataFileName);
    ...
    };

    class AudioClip
    {
    public:
    AudioClip(const string &audioDataFileName);
    ...
    };

    class BookEntry
    {
    public:
    ...
    ~BookEntry();
    BookEntry(const string&, const string&, const string&, const string&);
    void addPhoneNumber(const PhoneNumber &number);
    ...
    private:
    void cleanup();
    string theName;
    string theAddress;
    list<PhoneNumber> thePhones;
    Iimage * const theImage;
    AudioClip * const theAudioClip;
    Image* initImage(const string &imageFileName);
    AudioClip* initAudioClip(const string &audioClipFileName);
    };

    void BookEntry::cleanup()
    {
    delete theImage;
    delete the AudioClip;
    }

    /*
    BookEntry::BookEntry(const string &name,
    const string &address,
    const string &imageFileName,
    const string &audioClipFileName)
    : theName(name, theAddress(address),
    theImage(0), theAudioClip(0)
    {
    try {
    if (imageFileName != "")
    theImage = new Image(imageFileName);
    if (audioClipFileName != "")
    theAudioClip = new AudioClip(audioClipFileName);
    } catch (...) {
    cleanup();
    throw;
    }
    }
    */

    // 想要在exceptions传播到constructor外之前,无法将try和catch放到member initialization list中,就放到某些private member functions中。
    BookEntry::BookEntry(const string &name,
    const string &address,
    const string &imageFileName,
    const string &audioClipFileName)
    : theName(name, theAddress(address),
    theImage(initImage(imageFileName)), theAudioClip(initAudioClip(audioClipFileName))
    {}

    Image* BookEntry::initImage(const string &imageFileName)
    {
    if (imageFileName != "")
    return new Image(imageFileName);
    else return 0;
    }

    AudioClip* BookEntry::initAudioClip(const string &audioClipFileName)
    {
    try {
    if (audioClipFileName != "")
    return new AudioClip(audioClipFIleName);
    else return 0;
    } catch (...) {
    delete theImage;
    throw;
    }
    }

    BookEntry::~BookEntry()
    {
    cleanup();
    }
    更好的做法是如条款9一样,使用smart pointer来取代pointer class members,免除了exception出现时发生资源泄露。

条款11:禁止异常(exceptions)流出destructors之外

  1. 两种情况下destructor会被调用。第一种情况是当对象在正常状态下被销毁,也就是离开生存空间(scope)或是被明确删除时;第二种情况是当对象被exception处理机制——也就是exception传播过程中的stack-unwinding(栈展开)机制——销毁
  2. 当destructor被调用,可能有一个exception正在作用中,可惜的是无法在destructor区分这些状态(有了)。这样就要保守撰写destructors了。如果控制权基于exception的因素离开destructor,而此时另一个exception处于作用状态,C++会调用terminate函数,将程序结束掉。
  3. 有两个理由支持阻止exceptions传出destructor之外。第一,它可以避免terminate函数在exception传播过程的栈展开stack-unwinding)机制被调用;第二,它可以协助确保destructors完成其应该完成的所有事情

条款12:了解”抛出一个exception“与”传递一个参数“或”调用一个虚函数“之间的差异

  1. 从抛出端传递一个exception到catch字句,看起来和从函数调用端传递一个自变量到函数参数是一样的。但是有重大的不同。函数参数和exception的传递参数有3中:by value,by reference,by pointer。然而,当你调用一个函数,控制权最终会回到调用端(除非函数失败),但是当你抛出一个exception,控制权不再回到抛出端。而且被捕捉的exception是某个对象的副本,即使以by reference方式传参。
  2. 当对象被赋值当作一个exception,复制行为是由对象的copy constructor执行的。这个copy constructor相当于该对象的”静态类型“而非”动态类型”。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Widget { ... };
    class SpecialWidget : public Widget { ... };
    void passAndThrowWidget()
    {
    SpecialWidget localSpecialWidget;
    ...
    // rw表示一个SpecialWidget
    Widget& rw = localSpecialWidget;
    // 抛出一个类型为Widget的exception
    throw rw; // (条款25有返回动态类型的技术)
    // 抛出一个类型为SpecialWidget的exception;这是因为它并没有发生复制行为,只是再抛出。
    throw;
    }
    catch (Widget w) ... // by value
    catch (Widget &w) ... // by reference
    catch (const Widget &w) ... // by reference-to-const
    以上“参数传递”和“exception传播“之间的另一个区别。exception的复制动作,其结果是个临时对象。一个被抛出的对象可以简单的用by reference的方式捕捉,不需要以by reference-to-const的方式捕捉。调用过程将一个临时对象传递给non-const reference参数是不允许的,但对exceptions则是合法。
    值得一提,by value方式传递exception,得符出”被抛出物“两个副本的构造代价,其中一个构造动作用于产生exception临时对象,另一个构造动作作用于将临时对象赋值到w;而by reference得付出单一副本的构造代价,这个副本就是临时副本,by reference方式传递函数参数不会发生复制行为。
    还有,exceptions与catch子句匹配过程中,不会发生隐式转换的行为。它仅有两种转换可以发生。第一种是继承架构中的类转换inheritance-based conversions)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    graph BT
    logic_error-->exception
    runtime_error-->exception
    domain_error-->logic_error
    invalid_argument-->logic_error
    length_error-->logic_error
    out_of_range-->logic_error
    range_error-->runtime_error
    underflow_error-->runtime_error
    overflow_error-->runtime_error
    1
    2
    3
    4
    5
    6
    catch (runtime_error) ...
    catch (runtime_error&) ...
    catch (const runtime_error&) ...
    catch (runtime_error*) ...
    catch (const runtime_error*) ...
    catch (const void*) ...
    ”传递参数“和”传播exception“的最后不同是,catch子句总是依出现顺序做匹配尝试。将此行为和调用虚函数的行为比对。当调用虚函数时,被调用的函数是“调用者的动态类型”中的函数。在这方面,虚函数采用”best fit“(最佳吻合)策略exception遵循”first fit”(最先吻合)策略。所以针对derived class而设计的catch子句必须放到针对base class设计的catch子句之前。

条款13:以by reference方式捕捉exceptions

  1. throw by pointer应该是最有效率的一种做法,因为它不复制对象。但它并没有想象中的好,因为了让exception objects在控制权离开抛出指针的函数之后依然存在,就需要声明global或static对象,而程序很容易忘记。而且如果抛出的指针指向一个新的heap-based,负责catch的人不能区分是global即static还是heap中的exception object,于是可能产生内存泄露或者未定义的行为等潜在问题。
    此外,catch-by-pointer和语言本身建立起来的惯例有矛盾。4个标准的exception——bad-alloc、bad_cast、bad_typeid和bad_exception——统统都是对象,不是对象指针。说明只能使用by value或by reference。然而catch-by-value每当exception objects被抛出,就得复制两次。而且他会引起切割slicing)问题,因为derived class exception objects被捕捉并被视为base class exceptions:被切割过的对象其实就是缺少derived class data members。
    所以接下来就是catch-by-reference了。它不像catch-by-pointer发生对象删除问题,也不存在catch-by-value的切割问题,而且exception objects只会被复制一次。

条款14:明智运用exception specifications

  1. exception specifications让代码更容易被理解,因为它明确指出一个函数可以抛出什么样的exceptions。但它不只是一个注释而已。编译器有时候能够在编译期间侦测到与exception specifications不一致的行为。如果函数抛出了一个并未列入于其exception speicification的exception,这个错误会在运行期被检验出来。于是特殊函数unexpected会被自动调用。unexpected的默认行为是调用terminate,而terminate的默认行为是调用abort,所以程序如果违反exception speicification,默认结果就是程序被中止。
  2. 编译器只会对exception specifications做局部经检验,调用某个函数而该函数可能违反调用端函数本身的exception specification。这么做可能会导致程序被迫终止,为将这种不一致性降到最低,一个方法是避免将exception specification放在需要类型自变量的templates身上
    1
    2
    3
    4
    5
    6
    // 不良的temmplates design,它带有exception specification
    template<class T>
    bool operator==(const T &lhs, const T &rhs) throw()
    {
    return &lhs == &rhs;
    }
    它指明template产生出来的函数不会抛出任何exception,然而它调用的operator &可能会抛出一个异常。更一般化的问题是,没有任何方法可以知道一个template的类型参数可能抛出什么。
    避免踏上unexpected之路的第二个方法是:如果A函数调用的B函数没有exception specifications,那么A函数本身也不要设定exception specifications
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 标准委员会宣称typedef内不可出现exception specification, 可能需要该写宏
    typedef void (*CallBackPtr)(int eventXLocation, int eventYLocation, void *dataToPassBack) throw();

    class CallBack
    {
    public:
    CallBack(CallBackPtr fPtr, void *dataToPassBack) : func(fPtr), data(dataToPassBack) {}
    vvoid makeCallBack(int eventXLocation, int eentYLocaltion) const throw();s
    private
    CallBackPtr *func;
    void *fdata;
    }

    void CallBack::makeCallBack(int eventXLocation, int eventYLocatino) const throw()
    {
    fnuc(eventXLocation, eventYLocation, data);
    }
    第三个技术是:处理“系统”可能抛出的exceptions。直接处理非预期的exceptions比事先预防来的简单。但如果你写的软件大量运用exception specifications,但调用程序库提供的函数没有使用exception specifications,想要阻止之变得不切实际。这时候可以利用一个事实:以不同类型的exceptions取代非预期的exceptions。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 所有非预期的exception objects都被取代为此类的objects
    class UnexpectedException {};
    // 如果有非预期的exception被抛出,调用此函数
    void convertUnexpected()
    {
    throw UnexpectedException();
    }
    // 以convertUnexpected取代默认的unexpected
    std::set_unexpected(convertUnexpected);
    一旦完成这些部署,非预期的exception会导致该函数被调用。那么只要违反的exception specification内含有Unexpected Exception,exception的传播就会继续下去,而如果未含有,terminate会被调用,犹如未取代unexpected一样。
    将非预期的exceptions转换为一个已知类型的另一个做法是,依赖以下事实:如果非预期函数的替代者重新抛出当前(current)exception,该xception会被保准类型bad_exception取代而之。

条款15:了解异常处理(exception handing)的成本

  1. 为了能够在运行期处理exceptions,程序必须做大量簿记工作。在每个执行点,它们必须能够确认“如果发生exception,哪些对象需要析构”,它们必须在每个try语句块的进入点和离开点做记号,针对每个try语句块它们必须记录对应的catch子句及能够处理的exceptions类型。这些簿记工作必须符出代价,运行时期的比对工作(以确保符合exception specifications)不是免费的。