为什么异常规范无用?

时间:2012-07-05 22:29:43

标签: c++ templates exception exception-specification

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

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

    除了throw; 之外,
  • 没有throw A; throw B; throw C;
  • 没有比throw (A,B,C);
  • 更少限制的throw-signature的函数/方法调用

,至少在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 ++ at至少)应该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
      [...]
    }
}

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

3 个答案:

答案 0 :(得分:5)

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

在编译器方面,抛出异常类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中)。使用该语言最简单的方法就是这样:不检查。因此,会有很多人不想使用它。

许多人不想打扰的功能有什么用处,甚至那些做过的人通常也会对此感到悲伤?

答案 1 :(得分:1)

  

这意味着函数体已经

     除了投掷A之外没有投掷;扔B;扔C ;;
     没有抛出签名比throw(A,B,C)更少限制的函数/方法调用;

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

答案 2 :(得分:0)

在某些情况下,抛出规范可能很有用,特别是在嵌入式系统上,其中替代方案是完全禁止例外。除此之外,如果允许抛出的唯一函数是那些明确指定的函数,那么在调用不能抛出的函数时可以消除异常处理开销。请注意,即使在使用“元数据”进行异常处理的系统中,异常处理程序必须能够理解堆栈帧这一事实也会妨碍优化,否则可能会进行优化。

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