noexcept函数还能调用c++ 17中抛出的函数吗?

Can a noexcept function still call a function that throws in C++17?

本文关键字:函数 调用 c++ noexcept      更新时间:2023-10-16

在P0012R1中,"使异常规范成为类型系统的一部分",
我看到noexcept现在变成了函数类型的一部分。

我不知道这是否会阻止noexcept(true)函数仍然能够调用noexcept(false)函数。

下面的代码在c++ 17中仍然有效吗?

void will_throw() noexcept(false){
  throw 0;
}
void will_not_throw() noexcept(true){
  will_throw();
}

根据cppreference:

注意,函数的noexcept规范不是编译时的检查;它只是程序员通知编译器的一种方法函数是否应该抛出异常。

所以你的代码的语法是有效的,但是std::terminate将在执行时被调用

不幸的是,它在编译时是有效的。

虽然noexcept被编译器用来优化异常处理代码,使代码更高效,但遗憾的是他们没有进一步给noexcept一个语义意义。

理想情况下,当您将方法标记为noexcept时,还应该意味着该方法不应该让任何异常冒泡。因此,如果你有一个标记为noexcept的方法,但它调用了其他未标记为noexcept的方法,那应该会给你一个编译错误,除非有一个try/catch块围绕着任何可能抛出的东西。

简单地调用std::terminate是一个非常糟糕的语言设计选择,因为它没有把任何责任交给编写noexcept方法的人。相反,它甚至使用户无法解决问题,从而损害了软件的重用。

例如,假设我是一个糟糕的库开发人员,我写了以下代码:

我的库附带的头文件Foo.h:

class Foo
{
public:
    void DoSomething() noexcept;
};

你是一个快乐的消费者 fooolib 编写一个Bar应用程序:

Bar.cpp

#include "Foo.h"
int main()
{
    Foo foo;
    try
    {
        foo.DoSomething();
    }
    catch (...)
    {
        std::cout << "Yay!" << std::endl;
    }
    return 0;
}

代码编译得很好,运行得很好,直到你让Foo抛出异常…如果在try/catch块中包含对foo.DoSomething()的调用,则没有任何区别。代码将直接中止。

如果你没有Foo的代码,你不能修复它。在这种情况下,唯一的解决方案是扔掉Foo库,自己编写。

Foo.cpp的内容可以像这样:

static void PotentiallyThrowException()
{
    throw 0;
}
void Foo::DoSomething() noexcept
{
    PotentiallyThrowException();
}

注意,这取决于Foo::DoSomething()的实现者将自己的调用包装到try/catch中。但是由于同样的问题,如果他们调用其他标记为noexcept的方法,而这些开发人员没有这样做,那么现在它是Foo::DoSomething()。等等,等等。

我们可以有把握地说,从语义的角度来看,noexcept不仅无用,而且有害。

noexcept(true)函数可以调用noexcept(false)函数。如果抛出异常,将会出现运行时错误。为什么允许这样做的典型示例是:

double hypotenuse(double opposite, double adjacent) noexcept(true)
{
    return std::sqrt(opposite*opposite + adjacent*adjacent);
}

std::sqrt将抛出domain_error,如果它的参数是负的,但显然这不会在这里发生。

(在理想的情况下,默认情况下,exception_cast允许它在需要时被禁止。如果抛出异常,结果可能是UB,或者std::terminate)。

在函数类型中包含异常说明与函数是否可以调用具有不兼容异常说明的另一个函数是正交的(不处理异常说明中未包含的异常)。前者是关于函数指针的类型安全(因此,您不能通过已知不会抛出的函数指针调用抛出函数)。对于后者,我们可以在编译期间禁止它(如在Java中),或者将其视为运行时错误(导致程序终止,如c++当前标准所选择的那样)。

可以类比为从const(非静态)成员函数调用非const(非静态)成员函数。然而,不同之处在于间接修改通过const成员函数调用的非const成员函数内部的对象将无法检测到(或者检测成本太高),并可能导致严重的错误,这就是为什么必须在编译期间防止它。然而,抛出异常的行为是(应该是)一个异常事件,我们可以在运行时插入检查,检查异常是否符合异常规范,是否应该释放,或者它违反了程序逻辑,是否应该终止程序。