为什么“动态异常”保证会导致额外开销?

5

在C++11中,这已经被弃用:

void foo() throw();

并被替换为

void foo() noexcept;

这篇文章中解释了其中一个原因(还有其他原因,归结为同一件事情),即:

C++异常规范在运行时而不是编译时进行检查,因此它们不能向程序员提供所有异常都已处理的保证。

虽然我觉得这很有道理,但我不明白为什么首先要动态检查throw(),或者为什么noexcept除了调用std::terminate而不是正常的堆栈展开之外没有提供异常保证(在我看来这并不是一个可靠的保证)。
难道不能在编译期间检查是否抛出异常,如果发生异常则编译失败吗?就我所见,基本上有三种情况:
void foo() noexcept
{
    // 1. Trivial case
    throw myexcept();

    // 2. Try-catch case
    //    Necessary to check whether myexcept is derived
    //    from exception
    try 
    { 
        throw myexcept(); 
    } 
    catch(exception const & e)
    {}

    // 3. Nested function call
    //    Recursion necessary
    bar();
}

使用C++模板时,每个类型都需要实例化,编译应用程序本来就需要很长时间 - 所以为什么不将noexcept更改为强制编译器在编译时检查是否抛出异常呢?
我唯一看到的困难在于一个函数可能会根据运行时状态抛出或不抛出异常 - 但是在我看来,该函数不应该被允许调用自身noexcept
我是否遗漏了什么,或者意图是不增加编译时间,或者对编译器开发人员要求较少?

13
编译器无法知道你在函数中调用的库函数是否会引发异常。 - lapk
4
throw() 是编译器对你的保证。noexcept 是你对编译器的保证。 - Kerrek SB
@PetrBudnik:当然有可能,任何未标记为“noexcept”的函数都可能会抛出异常。 - Matthieu M.
6
@MatthieuM。即使不涉及动态加载,您是否建议C++11编译器拒绝编译void foo() noexcept( true ) { bar(); },其中bar()来自于在引入noexcept关键字之前创建的库? - lapk
@KerrekSB:你的意思是编译器会通过调用terminate()来保证即使抛出异常也能“狠狠地打击”你(并且违反所有RAII期望,不进行堆栈展开)吗? - Ad N
@AdN:我认为noexcept背后的想法是,在某些平台上,如果一个作用域块创建了一个具有析构函数的变量,并且在该变量处于活动状态时调用可能引发异常的方法,则即使函数正常返回(没有抛出异常),确保析构函数将被调用所需的代码也可能引入重大开销。只有编译器知道不会抛出异常,才能避免开销,而通常编译器知道的唯一方法就是由程序员来说明。 - supercat
1个回答

1
我认为这很大程度上要归因于在定义异常规范时编译器的开发者远远落后于技术潮流。实现C++98足够复杂,以至于只有一个编译器声称实现了所有功能,其他编译器都至少遗漏了一个包含在标准中的重要功能,大多数还公开承认他们遗漏的远不止这些。
你还需要记住,动态异常规范也比throw()复杂得多。它允许程序员指定可以抛出的任意一组类型。更糟的是,指定函数可以抛出foo意味着它也可以抛出派生自foo的任何东西。
静态强制执行异常规范是可行的,但这显然会增加相当多的额外工作量,而且没有人真正确定它提供了什么(如果有)好处。在此情况下,我认为大多数人认为静态强制执行是一种可以稍后要求的东西,如果有足够的使用来证明其合理性,则需要进行工作。从运行时强制执行到编译时强制执行的更改不需要修改现有代码,只需修改现有实现即可。
另一个观点是我不确定是否曾经真正有强烈的异常规范支持。我认为基本思想有普遍共识,但当你深入了解时,可能对细节的关注就少了。

底线:只强制动态执行很容易,而将静态执行留待以后(如果有必要)。事实证明,在任何情况下,静态执行都不会带来太多积极作用,因此强制执行可能也不会产生太大的成效。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接