为什么异常规范不好?


50

大约10年前回到学校时,他们正在教您使用异常说明符。由于我的背景是其中的一位Torvaldish C程序员,他们顽固地避免使用C ++(除非被迫这么做),所以我只会偶尔散布C ++,并且当我这样做时,我仍然使用异常说明符,因为这就是我所教的。

但是,大多数C ++程序员似乎并不喜欢异常说明符。我已经阅读了各种C ++专家的辩论和论点,例如这些。据我了解,它可以归结为三点:

  1. 异常说明符使用与其余语言不一致的类型系统(“影子类型系统”)。
  2. 如果您的带有异常说明符的函数抛出了除您指定的内容以外的其他任何内容,则该程序将以不良的意外方式终止。
  3. 即将在即将发布的C ++标准中删除异常说明符。

我在这里错过什么还是所有这些原因吗?

我个人的意见:

关于1):那又如何。就语法而言,C ++可能是有史以来最不一致的编程语言。我们有宏,goto /标签,未定义/未指定/实现定义的行为的部落(ho?),定义欠佳的整数类型,所有隐式类型升级规则,特殊情况下的关键字,例如friend,auto ,注册,显式...等等。有人可能会写几本关于C / C ++怪异故事的厚书。那么,人们为什么要对这种特殊的矛盾做出反应呢?与语言的许多其他更危险的特征相比,这是一个较小的缺陷?

关于2):这不是我的责任吗?我可以用C ++编写致命错误的方法有很多,为什么这种特殊情况会更糟?除了编写throw(int)然后抛出Crash_t之外,我还可以声明我的函数返回一个指向int的指针,然后进行一个狂野的,显式的类型转换,然后返回一个指向Crash_t的指针。C / C ++的精神始终是将大部分责任留给程序员。

那优势呢?最明显的是,如果您的函数尝试显式抛出除指定类型之外的任何类型,则编译器将给您一个错误。我认为有关此标准很明确。仅当您的函数调用其他函数而又抛出错误的类型时,才会发生错误。

来自确定性的嵌入式C程序世界,我当然希望更确切地知道函数会给我带来什么。如果有某种语言支持该功能,为什么不使用它呢?替代方案似乎是:

void func() throw(Egg_t);

void func(); // This function throws an Egg_t

我认为在第二种情况下,调用者有很大的机会忽略/忘记实现try-catch,而在第一种情况下则更少。

据我了解,如果这两种形式之一决定突然引发另一种异常,则程序将崩溃。在第一种情况下,因为不允许它抛出另一个异常,在第二种情况下,因为没有人期望它抛出SpanishInquisition_t,因此该表达式不会在应有的位置被捕获。

如果是后者,在程序的最高级别进行一些最后的捕获似乎并没有比程序崩溃更好的方法:“嘿,程序中的某处引发了一个奇怪的,未处理的异常”。一旦远离异常引发的地方,您将无法恢复程序,唯一可以做的就是退出程序。

从用户的角度来看,如果他们从操作系统中收到一个错误消息框,说“程序终止。地址为0x12345的Blablabla”,或者从您的程序中获得一个错误消息框,指出了“未处理的异常:myclass。 func.something”。该错误仍然存​​在。


随着即将到来的C ++标准,我将别无选择,只能放弃异常说明符。但是,我宁愿听到一些可靠的论据,说明它们为什么不好,而不是“圣洁已经说过了,事实就是如此”。也许对他们的争论比我列出的要多,或者对他们的争论比我所意识到的还要多?


30
我很想把它否定为“伪装成问题”。您问了有关E.spec的三个有效点,但是为什么您要用C ++这么烦人又像C一样漫不经心地打扰我们呢?
马丁·巴

10
@Martin:因为我想指出我既有偏见,也没有及时了解所有细节,而且我认为这种语言幼稚和/或尚未毁灭。而且C C ++已经是难以置信的缺陷语言,因此某种缺陷或多或少实际上并不重要。在我将其编辑下来之前,该帖子实际上要糟糕得多:)反对异常说明符的争论也很主观,因此很难在不引起主观的情况下讨论它们。

SpanishInquisition_t!搞笑!使用堆栈框架指针引发异常给我个人留下了深刻的印象,并且看起来它可以使代码更简洁。但是,我从未真正编写过带有异常的代码。称我为老式,但返回值对我来说很好用。
沙巴兹(Shahbaz)

@Shahbaz正如人们可以在两行之间读到的一样,我也很老套,但我从未真正质疑过异常是好是坏。

2
过去时态的罚球,不。这是一个很强的动词。
TRiG 2014年

Answers:


48

异常规范是不好的,因为它们的执行力很弱,因此实际上执行不了什么,它们也很糟糕,因为它们强制运行时检查意外的异常,以便可以终止(),而不是调用UB。 ,这会浪费大量性能。

因此,总而言之,异常规范在语言中的执行程度不足以使代码实际上变得更安全,因此按规定实施它们会浪费大量性能。


2
但是运行时检查不是异常规范本身的问题,而是标准中指出抛出错误类型应该终止的任何部分。简单地删除导致这种动态检查的要求,然后让所有内容由编译器进行静态评估,是否更明智?听起来RTTI是这里的真正罪魁祸首,或者...?

7
@Lundin:不可能静态确定所有可能从C ++函数抛出的异常,因为该语言允许在运行时对控制流进行任意和不确定的更改(例如,通过函数指针,虚方法和类似)。像Java一样,实现静态编译时异常检查将需要对该语言进行根本性更改,并禁用许多C兼容性。

3
@OrbWeaver:我认为静态检查很有可能-异常规范将成为函数规范的一部分(如返回值),并且函数指针,虚拟替代等必须具有兼容的规范。主要反对意见是它是一项全有或全无的功能-具有静态异常规范的函数无法调用旧版函数。(调用C函数会很好,因为它们不能抛出任何东西)。
Mike Seymour,

2
@Lundin:异常规范尚未删除,仅已弃用。使用它们的旧版代码仍然有效。
Mike Seymour

1
@Lundin:具有异常规范的旧版C ++完全兼容。如果以前的代码有效,它们将不会失败,也不会意外运行。
DeadMG

18

没有人使用它们的原因之一是因为您的主要假设是错误的:

“最明显的优势是,如果您的函数试图显式抛出除指定类型之外的任何类型,则编译器将给您一个错误。”

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

该程序的编译没有错误或警告

此外,即使将捕获异常,程序仍会终止。


编辑:要回答您问题的一个要点,即使您不这样做,catch(...)(或更好的是catch(std :: exeption&)或其他基类,然后 catch(...))仍然有用确切知道出了什么问题。

考虑到您的用户点击了“保存”菜单按钮。菜单按钮的处理程序邀请应用程序保存。由于种种原因,该操作可能会失败:该文件位于网络资源上消失了,有一个只读文件,无法保存,等等。它只关心成功还是失败。如果成功,一切都很好。如果失败,它可以告诉用户。异常的确切性质无关紧要。此外,如果您编写正确的,异常安全的代码,则意味着任何此类错误都可以在您的系统中传播而不会降低它-即使对于不关心该错误的代码也是如此。这使扩展系统变得更加容易。例如,您现在通过数据库连接进行保存。


4
@Lundin编译时没有警告。不,你不能。不可靠。您可以通过函数指针进行调用,也可以是虚拟成员函数,并且派生类将引发派生_异常(基类可能不知道)。
卡兹龙

倒霉。Embarcadero / Borland为此发出警告:“抛出异常违反了异常说明符”。显然,GCC不会。他们没有理由不能实施警告。同样,静态分析工具可以轻松找到此错误。

14
@伦丁:请不要开始争论和重复虚假信息。您的问题已经有些大话了,声称虚假的东西无济于事。静态分析工具无法找到这种情况;这样做将涉及解决暂停问题。而且,它需要分析整个程序,并且经常没有整个源。
David Thornley,

1
@Lundin,同一静态分析工具可以告诉您哪些抛出的异常离开了堆栈,因此在这种情况下,使用异常规范仍无济于事,除非潜在的假阴性会导致程序崩溃,而您可能在其中处理了错误情况。更好的方式(例如宣布失败并继续运行:请参见我的答案的第二部分作为示例)。
哈兹巨龙

1
@Lundin一个好问题。答案是,通常来说,您不知道所调用的函数是否会抛出异常,因此可以肯定的假设是它们都会抛出异常。我在stackoverflow.com/questions/4025667/…中回答了一个问题。事件处理程序尤其形成自然的模块边界。允许异常逃逸到通用窗口/事件系统(例如)中将受到惩罚。如果程序要在那里生存,则处理程序应捕获所有错误。
卡兹龙

17

对安德斯·海斯伯格的采访非常有名。在其中,他解释了为什么C#设计团队首先丢弃已检查的异常。简而言之,有两个主要原因:可版本性和可伸缩性。

我知道OP专注于C ++,而Hejlsberg正在讨论C#,但是Hejlsberg提出的观点也完全适用于C ++。


5
“ Hejlsberg提出的观点也完全适用于C ++” ..错误!用Java实现的检查异常会强制调用者处理它们(和/或调用者的调用者等)。C ++中的异常规范(虽然很弱并且没有用)仅适用于单个“函数调用边界”,不像检查的异常那样“传播”调用堆栈。
马丁·巴

3
@Martin:我同意C ++中异常规范的行为。但是,我引用“ Hejlsberg提出的观点也完全适用于C ++”这一短语是指Hejlsberg提出的观点,而不是C ++规范。换句话说,我在这里谈论的是程序的可版本性和可伸缩性,而不是异常传播。
CesarGon 2011年

3
我很早就不同意Hejlsberg的观点:“我对检查异常的关注是他们给程序员戴上了手铐。您会看到程序员选择了具有所有throws子句的新API,然后看到他们的代码变得多么复杂,并且意识到被检查的异常对他们没有任何帮助。这些独裁的API设计人员告诉您如何进行异常处理。” ---是否检查了异常,您仍然必须处理正在调用的API引发的异常。受检查的异常仅使其明确。
quant_dev

5
@quant_dev:不,您不必处理正在调用的API引发的异常。完全有效的选择是处理异常,并让异常使调用堆栈冒泡,以供您的呼叫者处理。
CesarGon 2011年

4
@CesarGon不处理异常也可以选择如何处理它们。
quant_dev
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.