异常的辨识

  典型情况下,在一个程序里可能存在多种不同的运行时错误,我们可以将这些错误映射到一些具有不同名字的异常。我喜欢定义一些除了服务于异常处理之外没有其他用途的类,这使人不容易将它们的用途弄错。特别地,我绝不去用内部类型(例如,int)作为异常。因为,在一个大程序里,我没有有效的方法去确定int异常的无关使用,这样,我也就无法保证那些另有所图的使用不会与我的使用相互干扰。

  我们的计算器(6.1节)必须处理两类运行时的错误:语法错误和企图除以0的错误。检查到企图除以0错误的代码不需要给处理器传递任何信息,因此,除以0的问题可以用一个简单的空类型表示:

struct Zero_divide { };

而在另一方面,处理器非常希望能得到一个有关出现了什么语法错误的指示。在这里我们就传递一个字符串

    struct Syntax_error {
        const char* p;
        Syntax_error(const char* q) { p = q; }
    };

为了记述上的方便,我给这个struct加进了一个构造函数(2.5.2节、10.2.3节)。

  分析器的用户可以通过在try块之后附加两个处理器的方式,完成对这两种异常的辨识,在需要时控制就会进入适当的处理器。如果我们从一个处理器的“末端掉出去”,执行就将从整个的处理器列表之后继续下去:

    try {
        // ...
        expr(false);
        // 当且仅当expr()没有导致任何异常,我们将到达这里
        // ...
    }
    catch (Syntax_error) {
        // 处理语法错误
    }
    catch (Zero_divide) {
        // 处理用零除的错误
    }
    // 如果expr()没有发生任何异常,或者是出现Syntax_error
    // 或Zero_divide异常并被捕捉到(而且其处理器不return,
    // 不抛出异常,也不以其他方式改变控制流),我们就能到达这里

处理器的表看起来就像一个开关语句,但是这里不需要break。处理器列表在语法与case列表不同,部分的原因也就在于此,另一个原因是指明每个处理器都是一个作用域(4.9.4节)。

  一个函数不必捕捉所有可能的异常。例如,前面的try块就没有打算去捕捉可能由分析器的输入操作产生的异常。这些异常将简单地“穿过”这里,继续去查找某个带有合适处理器的调用者。

  从语言的观点看,被考虑那个异常正好在其处理器的入口进行处理,所以,在执行处理器期间所抛出的异常就必须由这个try块的调用者去处理。举个例子,下面的代码不会导致无穷循环:

    class Input_overflow { /* ... */ };

    void f()
    {
        try {
            // ...
        }
        catch (Input_overflow) {
            // ...
            throw Input_overflow();
        }
    }

异常处理器也可以嵌套。例如,

    class XXII { /* ... */ };

    void f()
    {
        // ...
        try {
            // ...
        }
        catch (XXII) {
            try {
                // 某些复杂的东西
            }
            catch (XXII) {
                // 复杂处理器代码失败
            }
        }
        // ...
    }

当然,在人们写出的代码中很少会看到这种嵌套,这更多的是表明了某种糟糕风格。

🔚