constexpr是否暗示noexcept

Does constexpr imply noexcept?

本文关键字:noexcept 暗示 是否 constexpr      更新时间:2023-10-16

constexpr说明符是否意味着函数的noexcept说明符?对于inline说明符,类似问题的答案是肯定的,但Eric Niebler的文章让我想知道当前问题的可能答案。在我看来,答案可能取决于使用constexpr函数的上下文:它是常量表达式上下文还是运行时上下文,即函数的所有参数在编译时是否已知。

我原以为答案是"是",但简单的检查表明事实并非如此。

constexpr
bool f(int) noexcept
{
    return true;
}
constexpr
bool g(int)
{
    return true;
}
static_assert(noexcept(f(1)));
static_assert(noexcept(g(2))); // comment this line to check runtime behaviour
#include <cassert>
#include <cstdlib>
int
main(int argc, char * [])
{
    assert(noexcept(f(argc)));
    assert(noexcept(g(argc)));
    return EXIT_SUCCESS;
}

不,情况不可能是这样,因为不是constexpr函数的每个inovacation都必须能够被计算为核心常量表达式的子表达式。我们只需要一个允许这样做的参数值。因此,只要我们有一个不调用该分支的参数值,constexpr函数就可以包含throw语句。

这在草案C++14标准部分7.1.5中介绍。constexpr说明符[dcl.constexpr]告诉我们constexpr函数中允许的内容:

constexpr函数的定义应满足以下约束:

  • 它不应是虚拟的(10.3);

  • 其返回类型应为文字类型;

  • 其每个参数类型应为文字类型;

  • 其函数体应为=删除、=默认或不包含的复合语句

    • asm定义,

    • goto声明

    • 试块,或

    • 非文字类型变量、静态变量或线程存储持续时间变量的定义不执行初始化。

正如我们所看到的,这并没有禁止CCD_。

下面我们看到的规则是,如果至少存在一个参数值,那么constexpr函数的形式就很好,这样它就可以被计算为核心常量表达式的子表达式:

对于非模板、非默认的constexpr函数或非模板、无默认、无继承constexpr构造函数,如果不存在参数值,则调用函数或构造函数可能是核心常量表达式(5.19)的求值子表达式,则程序格式错误;不需要诊断。

在这段下面,我们有下面的例子,它展示了这种情况的一个完美例子:

constexpr int f(bool b)
  { return b ? throw 0 : 0; } // OK
constexpr int f() { return f(true); } // ill-formed, no diagnostic required

因此,我们期望以下示例的输出:

#include <iostream>
constexpr int f(bool b)   { return b ? throw 0 : 0; } 
int main() {
    std::cout << noexcept( f(1) ) << "n" 
              << noexcept( f(0) ) << "n" ; 
}

成为(看到它与gcc一起生活):

 0
 1

Visual Studio通过网络编译器也会产生相同的结果。正如hvd所指出的,clang有一个bug,正如bug报告所记录的那样。noexcept应该检查表达式是否是常量表达式。

缺陷报告1129

缺陷报告1129:constexpr函数的默认nothrow提出了相同的问题:

constexpr函数不允许通过异常返回。应该认识到这一点,并且在没有显式异常规范的情况下声明为constexpr的函数应该被视为声明为noexcept(true),而不是通常的noxcept(false)。对于在没有显式异常规范的情况下声明为constexpr的函数模板,当且仅当给定实例化中遵守constexpr关键字时,应将其视为noexcept(true)。

结果是:

前提是不正确的:只有在需要常量表达式的上下文中调用constexpr函数时,才禁止异常。作为一个普通函数,它可以抛出。

它修改了5.3.7[expr.unary.noexcept]第3段第1项(添加了强调):

函数、成员函数、函数指针或成员函数指针的潜在求值调用80,该调用没有非抛出异常规范(15.4[ixcept.spec]),除非该调用是常量表达式(5.20[expr.const]),

关于noexcept,有人说:

如果表达式包含对任何没有非抛出异常规范的函数类型的[…]调用,则结果为false,除非它是常量表达式。

此外,关于constexpr,确实:

对于常量表达式,noexcept运算符总是返回true

在任何情况下,这似乎都不意味着constexpr说明符强制为被包围的表达式使用noexcept说明符,正如有人在评论中用反例显示的那样,您也验证了这一点。

不管怎样,从文档中,关于noexceptconstexpr之间的关系,有以下有趣的注释:

因为noexcept运算符对于常量表达式总是返回true,所以它可以用于检查constexpr函数的特定调用是否采用常量表达式分支

EDIT:GCC示例

感谢@hvd对GCC关于我最后一句话的有趣评论/示例。

constexpr int f(int i) {
    return i == 0 ? i : f(i - 1);
}
int main() {
    noexcept(f(512));
    return noexcept(f(0)) == noexcept(f(0));
}

上面的代码返回0,并警告语句noexcept(f(512))无效
在注释掉该语句时,返回值将更改为1

编辑:clang上的已知错误

同样,感谢@hvd提供了这个链接,这是关于clang中关于问题中提到的代码的一个众所周知的错误。

引用错误报告:

引用C++的书[expr.unary.noexcept]p3:

"如果在可能求值的上下文中,表达式将包含对没有非抛出异常规范(15.4)的函数、成员函数、函数指针或成员函数指针的可能求值调用,则noexcept运算符的结果为false,除非该调用是常量表达式(5.19)";。

我们不执行最后一句话。

允许在constexpr函数中引发异常。它是这样设计的,以便实现者可以向编译器指示错误。假设您具有以下功能:

constexpr int fast_sqrt(int x) {
    return (x < 0) ? throw invalid_domain() : fast_sqrt_impl(x);
}

在这种情况下,如果x为负数,我们需要立即停止编译,并通过编译器错误向用户指示问题。这遵循了编译器错误比运行时错误(快速失败)更好的思想。

C++标准在(5.20):中说明了这一点

条件表达式e是核心常量表达式,除非根据抽象机(1.9),将评估以下表达式之一:

--抛出表达式(5.17)

不,通常不会。

constexpr函数可以在允许抛出异常的非constepr上下文中调用(当然,如果手动将其指定为noexcept(true),则除外)。

但是,作为常量表达式的一部分(例如在您的示例中),它的行为应该像指定为noexcept(true)一样(当然,如果表达式的求值会导致抛出异常,这不会导致对std::terminate的调用,因为程序尚未运行,而是会导致编译时错误)。

正如我所料,您的示例不会触发MSVC和g++的静态断言。我不确定,这是clang中的错误还是我对标准中的某些内容理解错误。