为什么异常规范不能有用

Why exception specifications cannot be useful?

本文关键字:不能 有用 异常 范不能 为什么      更新时间:2023-10-16

我已经阅读了很多关于(不)在函数签名中使用throw(X)的论点,我认为它在ISO C++中指定的方式(并在当前编译器中实现)是相当无用的。但是为什么编译器不能简单地在编译时强制执行异常正确性呢?

如果我编写的函数/方法定义在其签名中包含throw(A,B,C),编译器在确定给定函数的实现是否异常正确时应该没有太多问题。这意味着函数体具有

  • 除了throw A; throw B; throw C;之外,没有其他throw;
  • 没有抛空签名限制小于throw (A,B,C)的函数/方法调用;

,至少在try{}catch()捕捉其他投掷类型之外。如果编译器在不满足这些要求时引发错误,则所有函数都应该是"安全的",并且不需要像unexpected()这样的运行时函数。所有这些都将在编译时得到保证。

void fooA() throw (A){
}
void fooAB() throw (A,B){
}
void fooABC() throw (A,B,C){
}

void bar() throw (A){
    throw A();   // ok
    throw B();   // Compiler error
    fooA();      // ok
    fooAB();     // compiler error
    fooABC();    // compiler error
    try{
       throw A();   // ok
       throw B();   // ok
       throw C();   // Compiler error
       fooA();   // ok
       fooAB();  // ok
       fooABC(); // compiler error
    } catch (B){}
}

这将要求所有非C++领域代码要么在缺省情况下throw()指定(extern "C"默认应该假定它),要么如果存在一些异常互操作性,那么适当的标头(至少对于C++)也应该throw指定。如果不这样做,可以与在不同的编译单元中使用具有不同函数/方法返回类型的标头进行比较。虽然它不会产生警告或错误,但它显然是错误的 - 并且由于抛出的异常是签名的一部分,它们也应该匹配。

如果我们强制执行这些约束,它将产生三种影响:

  • 它将删除所有那些隐式try{}catch块,否则运行时检查需要,从而提高异常处理性能。
  • "异常使我们的库太大,所以我们关闭它们"的论点将消失,因为大多数附加代码存在于每次函数调用时那些不必要的隐式抛出/捕获指令中。如果代码正确throw指定,则编译器不会添加其中的大部分代码。
  • 这会让大多数编程世界感到愤怒,因为似乎没有人喜欢过。异常。现在,由于这些实际上是可用的,我们需要学习如何使用它们。

如果我们对旧代码使用一些兼容性编译器标志,它不会破坏任何东西,但是由于新的异常代码会更快,因此不会使用它编写新代码。

总结一下我的问题:为什么ISO C++不要求这种执行?有什么强有力的理由不这样做吗?我一直认为异常只是另一个函数的返回值,但它是一个自动控制的函数,因此您可以避免编写诸如

std::pair<int, bool> str2int(std::string s);
int str2int(std::string s, bool* ok);

加上变量的额外自动销毁和通过堆栈上的多个函数传播,因此您不需要类似的代码

int doThis(){
    int err=0;
    [...]
    if ((err = doThat())){
        return err;
    }
    [...]
}

;如果你可以要求函数只return正确的类型,为什么你不能要求它throw它呢?

为什么异常说明符不能更好?为什么它们不像我从一开始就描述的那样制作?

PS 我知道异常和模板可能存在一些问题 - 根据这个问题的答案,也许我会问另一个问题 - 现在让我们忘记模板。

编辑(回应@NicolBolas):

编译器可以对异常类 X 进行什么样的优化,而不能对 Y 进行优化?

比较:

void fooA() throw (A){
}
void fooAB() throw (A,B){
}
void fooABC() throw (A,B,C){
}

void bar() throw (){
    try{
       fooA();
         // if (exception == A) goto A_catch
       fooAB();
         // if (exception == A) goto A_catch
         // if (exception == B) goto B_catch
       fooABC();
         // if (exception == A) goto A_catch
         // if (exception == B) goto B_catch
         // if (exception == C) goto C_catch
    }
    catch (A){  // :A_catch
      [...]
    }
    catch (B){  // :B_catch
      [...]
    }
    catch (C){  // :C_catch
      [...]
    }
}

和:

void fooA(){
}
void fooAB(){
}
void fooABC(){
}

void bar(){
    try{
       fooA();
         // if (exception == A) goto A_catch;
         // if (exception == B) goto B_catch;
         // if (exception == C) goto C_catch;
         // if (exception == other) return exception;
       fooAB();
         // if (exception == A) goto A_catch;
         // if (exception == B) goto B_catch;
         // if (exception == C) goto C_catch;
         // if (exception == other) return exception;
       fooABC();
         // if (exception == A) goto A_catch;
         // if (exception == B) goto B_catch;
         // if (exception == C) goto C_catch;
         // if (exception == other) return exception;
    }
    catch (A){  // :A_catch
      [...]
    }
    catch (B){  // :B_catch
      [...]
    }
    catch (C){  // :C_catch
      [...]
    }
}

在这里,我包含了一些伪代码,编译器不会生成程序集级别。如您所见,了解可以获得哪些异常可以减少代码量。如果我们在这里要销毁一些额外的变量,那么额外的代码会更长。

编译器验证的异常作为函数签名的一部分有两个(理论上的)优点:编译器优化和编译时错误检查。

就编译器而言,抛出异常类X的函数和类Y之间有什么区别?最终。。。什么都没有。编译器可以对异常类X进行什么样的优化,而它不能对Y进行优化?除非std::exception是特殊的(X是从中派生出来的,而Y不是),否则对编译器来说有什么关系?

最终,编译器在优化方面唯一关心的是函数是否会引发任何异常。这就是为什么 C++11 的标准委员会放弃了throw(...),转而支持 noexcept ,它指出该功能不会抛出任何东西。

至于编译时错误检查,Java清楚地显示了它是如何工作的。你正在编写一个函数,foo .你的设计有它抛出XY.其他代码段使用foo,它们抛出foo抛出的任何内容。但是异常规范并没有说"无论foo抛出什么"。它必须具体列出XY

现在你回去改变foo,让它不再抛出X,但现在它抛出Z.突然,整个项目停止编译。您现在必须转到每个抛出foo抛出的任何函数,只是为了更改其异常规范以匹配foo

最终,程序员只是举起手,说它抛出任何异常。当你放弃这样的功能时,事实上承认该功能弊大于利。

这并不是说它们没有用。只是它们的实际使用表明它们通常没有用。所以没有意义。

另外,请记住,C++的规范声明,没有规范意味着任何东西都会被抛出,而不是什么都没有(如在Java中)。使用该语言的最简单方法就是这样:不检查。所以会有很多人不想使用它。

许多人不想打扰的功能有什么好处,即使是那些这样做的人通常也会从中获得很多悲伤?

这意味着函数体具有

除了投掷 A 之外没有投掷; 投掷

B; 投掷 C;;没有函数/方法调用的抛出签名限制性低于抛出 (A,B,C);

不要忘记,代码可以在不同的时间在不同的机器上编译,并且只能在运行时通过动态库链接在一起。编译器可能具有被调用函数签名的本地版本,但它可能与运行时实际使用的版本不匹配。(我认为如果异常不完全匹配,可以修改链接器以禁止链接,但这可能会带来比它解决的更多的烦恼。

抛出规范在某些情况下可能很有用,特别是在嵌入式系统上,另一种选择是完全禁止异常。 除此之外,如果唯一允许抛出的函数是那些显式指定的函数,那么在调用无法抛出的函数时,可以消除异常处理开销。 请注意,即使在使用"元数据"进行异常处理的系统中,异常处理程序必须能够理解堆栈帧这一事实也排除了本来可能进行的优化。

抛出规范可能有用的另一种情况是,如果编译器允许catch语句指定它们应该只捕获不会通过任何非"预期"层冒泡的异常。 C++和借用它的语言中的异常处理概念的一个主要弱点是,由于直接调用的例程没有预料到的原因,没有很好的方法来区分在被调用例程中发生的异常(对于由此记录的条件)和嵌套子例程中发生的异常。 如果catch声明能够对前者采取行动而不抓住后者,那将是有益的。