为什么C++0x的`noexcept`是动态检查的?

28

我对C++0x FCD中的noexcept背后的原理很好奇。虽然throw(X)已经被废弃,但noexcept似乎可以实现相同的功能。为什么noexcept没有在编译时进行检查呢?如果这些函数能够在try块内只调用可抛出异常的函数,那么静态检查它们似乎会更好。


9
另外,第1107页有一个很有趣的注脚。 - rlbond
2
哈哈!不错的发现。这就像在N3035中将“ill-formed program”重新定义为“一个不符合规范的想要成为C++程序”的定义一样好。 - James McNellis
检查这些静态的问题在于1)不能真正稳健地完成,2)即使如此,使用起来仍然非常痛苦 - 见Java等效的问题。没有人真正希望在C ++中使用静态检查的异常指定符。问题更多的是异常指定符是否应该存在 - jalf
9
有趣的脚注存在的问题在于它很容易让人将真正的脚注误认为是有趣的脚注,例如:“342)除其他含义外,原子变量不应衰变。” - James McNellis
6个回答

27
基本上,这是一个“链接器”问题,标准委员会不愿意破坏ABI。 (如果由我决定,我会这样做,这只需要库重新编译,我们已经在启用线程时遇到了这种情况,因此它是可管理的。)
考虑一下它是如何工作的。 假设要求如下:
1.每个析构函数都隐式为noexcept(true) - 可以说,这应该是一个严格的要求。在C ++中,抛出异常的析构函数始终是错误的。
2.每个extern "C"都隐式地为noexcept(true) - 再次相同的论点:C语言异常总是错误的。
3.除非另有规定,否则每个其他函数都隐式为noexcept(false) 4.noexcept(true)函数必须将其所有noexcept(false)调用包装在try {} catch(...){} 中 - 类比于const方法不能调用non-const方法。
5.此属性必须在重载解析,函数指针兼容性等方面表现为不同类型。
听起来合理,对吗?
要实现这一点,链接器需要区分函数的noexcept(true)noexcept(false)版本,就像您可以重载const和非const版本的成员函数一样。
那么这对于名称重整意味着什么呢?为了与现有对象代码保持向后兼容性,我们要求所有现有名称都被解释为具有noexcept(false)的额外重整的noexcept(true)版本。
这将意味着我们无法链接现有的析构函数,除非修改标头以标记它们为noexcept(false) 这将打破向后兼容性, 这可能是不可能的,参见第1点。

我亲自与一个标准委员会成员谈过这件事,他说这是一个仓促的决定,主要是由于容器中移动操作的限制(否则您可能在抛出异常后丢失项目,从而违反基本保证)。请注意,这是一个声称不容忍故障代码是好的的人的设计哲学。得出你自己的结论。

就像我说的,我宁愿破坏ABI而不是破坏语言。 noexcept只是对旧方式的微小改进。静态检查总是更好的。


如果可能的话,静态检查会更好。如果模块A中的nothrow函数调用库B中的nothrow函数,那就没问题。直到库B更改为throwing操作。 - Mooing Duck
4
“每个extern "C"都隐式地具有noexcept(true)”我(和其他人)对此表示反对。没有什么能阻止您从extern "C"函数中抛出异常,而在某些实现中,即使由C代码调用该函数,您也可以这样做,让异常传播回知道如何处理它的C++代码。GCC和Clang使用的异常处理模型明确支持此功能,并且Solaris也支持此功能。这是定义良好且有用的。如果C函数不隐式为非抛出,则无法静态检查异常规范。因此,我们不执行静态检查。 - Jonathan Wakely
1
我不知道GCC/Clang是否支持跨语言异常,但该语言不支持。让异常传播到未定义异常处理的代码中是UB。 C语言不定义异常处理。正确的方式是在到达C之前捕获异常并将其包装到其他结构中。从C语言中抛出异常是荒谬的。 - spraff
1
当前的C++11编译器是否可以静态检查这些约束条件,并在不满足时生成警告(除了重载分辨率)?如果可以,那么它几乎和错误一样好(如果将该警告转换为错误)。 - Alex
也许我有点极端,但我认为正确性不应该成为编译器选项。 - spraff
显示剩余3条评论

25
如果我没记错,throw已经被弃用了,因为没有办法指定模板函数可能抛出的所有异常。即使对于非模板函数,你也需要使用throw子句,因为你已经添加了一些跟踪信息。
另一方面,编译器可以优化不抛出异常的代码。请参阅“关于noexcept的辩论,第一部分”(以及第二部分第三部分)进行详细讨论。主要观点似乎是:

The vast experience of the last two decades shows that in practice, only two forms of exceptions specifications are useful:

  • The lack of an overt exception specification, which designates a function that can throw any type of exception:

    int func(); //might throw any exception
    
  • A function that never throws. Such a function can be indicated by a throw() specification:

    int add(int, int) throw(); //should never throw any exception
    

6
我不明白这个答案如何解释为什么noexcept没有静态检查。此外,Kalev先生的短语“noexcept作为编译时异常规范”似乎与C++标准以及这个问题相矛盾。 - sellibitze

8
请注意,noexcept 检查由失败的 dynamic_cast 和应用于 null 指针的 typeid 抛出的异常,这只能在运行时完成。其他测试确实可以在编译时完成。

1
然而,在try块之外禁止使用dynamic_casttypeid是可行的... - rlbond
2
除非在那个点捕获异常不合适。异常可能会被重新抛出,但这会增加不必要的冗余和开销。 - outis

6
正如其他答案所述,像 dynamic_cast 这样的语句可能会抛出异常,但只能在运行时进行检查,因此编译器无法在编译时确定。
这意味着在编译时,编译器可以让它们通过(即不进行编译时检查)、发出警告或直接拒绝(这是没有用处的)。这只留下警告作为编译器唯一合理的操作。
但这仍然不是真正有用的 - 假设您有一个 dynamic_cast,由于某种原因,您知道它永远不会失败并抛出异常,因为您的程序是这样编写的。编译器可能不知道这一点并发出警告,这变成了噪音,程序员可能只是将其禁用,因为它没有用,从而否定了警告的作用。
类似的问题是,如果您有一个未指定为 noexcept(即可能抛出异常)的函数,并且您想从许多函数中调用该函数,其中一些是 noexcept,一些不是。您知道在 noexcept 函数调用的情况下该函数永远不会抛出异常,但编译器不知道:更多无用的警告。
因此,编译器没有有用的方法在编译时强制执行此操作。这更多地属于静态分析领域,这种方法往往更加严格,并为这种情况发出警告。

3
考虑一个函数
void fn() noexcept
{
   foo();
   bar();
}

你能进行静态检查,确定它是正确的吗?你需要知道foo或者bar是否会抛出异常。你可以强制所有的函数调用在try{}块中执行,例如:

void fun() noexcept
{
    try
    {
         foo();
         bar();
    }
    catch(ExceptionType &)
    {
    }
}

但这是错误的。我们无法知道foo和bar只会抛出那种类型的异常。为了确保我们捕获任何异常,我们需要使用“...”。如果你捕获了任何异常,你会怎么处理?如果出现意外错误,唯一要做的就是中止程序。但基本上默认提供的运行时检查也会这样做。
简而言之,在编译器不能确定函数是否会抛出错误类型的情况下,提供足够的细节以证明给定函数永远不会抛出错误异常将会产生冗长的代码。静态证明的有用性可能不值得这样的努力。

1
我不同意。如果我们有foo和bar的完整原型,那么我们就会知道它们是否具有异常规范。即使foo和bar在动态加载库中,这段代码编译的唯一方式是在编译时为它们提供正确的原型。 - SoapBox
1
@SoapBox: 但是唯一未被弃用的规范是“可能抛出”和“不会抛出”。当foobar具有“可能抛出”的规范时,fun是否抛出取决于它们可能抛出的内容。如果它们抛出ExceptionType对象,则fun是可以的。如果它们抛出OtherExceptionType对象,则fun就不行了。这些原型不足以确定这一点。 - Dennis Zickefoose
此外,如果原型从nothrow更改为not nothrow呢? - Mooing Duck

3
noexceptthrow()的功能上有一些重叠,但它们实际上来自于相反的方向。throw()关注的是正确性,而且仅仅是关注正确性:它是指定如果出现意外异常时的行为方式。 noexcept的主要目的是优化:它允许/鼓励编译器在假定不会抛出异常的情况下进行优化。但是,实际上,它们并没有差别太大,可以视作同一种东西的两个不同名称。虽然背后的动机是不同的。

2
noexcept 的主要目的是确保正确性。抛出异常会破坏之前正确代码(如排序算法和容器调整大小)中的强保证,因此我们需要 std::move_if_noexcept 来以安全的方式重写旧的通用代码。当然,缺乏静态检查使其表现得像 assert 而不是 const,但这是向后兼容性的代价 :-( - spraff
正如 spraff 所说,像容器这样的模板代码可能会根据 noexcept(以及等效的 throw())的存在或不存在而表现出不同的行为,因此它不仅关乎编译器优化,而且还影响库设计和算法选择。做到这一点的关键是使用 noexcept 运算符,它允许代码查询表达式的“抛出性”,_这就是_新东西,它只关心一个是/否的答案,不关心可能抛出的异常类型,只关心是否会抛出异常。 - Jonathan Wakely

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