5
struct A {       
    int i;
    consteval A() { i = 2; };
    consteval void f() { i = 3; }
};

constexpr bool g() {
    A a;
    a.f();
    return true;
}

int main() {
    static_assert(g());
}

https://godbolt.org/z/hafcab7Ga

该程序被所有 GCC、Clang、MSVC 和 ICC 拒绝,并被替换为所有四个都接受它的constexpr结果。gconsteval

但是,当删除呼叫时a.f();,仍然使用constexpron g,只有 ICC 仍然拒绝代码。其他三个现在接受它。

我不明白为什么会这样。我的理解是,没有constevalon g,表达式a.f()不在直接函数上下文中,这将导致成员函数调用本身被评估为单独的常量表达式,然后无法修改i成员,因为成员的生命周期在评估期间没有开始那个不变的表达。

但是为什么构造函数可以在同一个上下文中对同一个对象执行相同的操作呢?的生命周期是否a被认为是在评估 consteval 构造函数期间开始的?


另请注意, 的存在static_assert不会影响这些结果。constexpr从那时起完全删除g也不会改变编译器行为的任何内容。


正如@Enlico 所指出的,即使在除接受代码的ICC 之外的所有编译器中都替换A a;a.f();替换A{}.f();constexprong结果,尽管据我了解,此表达式应导致对立即构造函数调用和立即成员函数的两个单独的常量表达式进行评估调用。我认为后一个调用的行为应该与 完全相同a.f();,这使得这更加混乱。

(阅读@Barry 的回答后,我现在意识到最后一句话没有任何意义。更正:A{}将是构造函数立即调用的一个常量表达式,并且A{}.f()作为一个整体将是成员函数立即调用的第二个常量表达式。这显然与表达式不同a.f()。)

4

1 回答 1

5

规则是,来自[expr.const]/13

如果表达式或转换可能被评估并且其最内部的非块范围是立即函数的函数参数范围,则表达式或转换位于立即函数上下文中。如果表达式或转换是立即函数的潜在评估显式或隐式调用并且不在立即函数上下文中,则它是立即调用。立即调用应为常量表达式。

其中,立即函数只是(来自[dcl.constexpr]/2)的术语:

使用说明符声明的函数或构造函数consteval称为立即函数

从例子:

struct A {       
    int i;
    consteval A() { i = 2; };
    consteval void f() { i = 3; }
};

constexpr bool g() {
    A a;
    a.f();
    return true;
}

该调用a.f()是立即调用(我们正在调用立即函数,但我们不在立即函数上下文中,gis constexprnot consteval),所以它必须是一个常量表达式。

它本身必须是一个常量表达式。不是整个调用g(),只是a.f().

是吗?No.通过写入 来改变,这违反了 [ a.f()expr.const ]/5.16。成为常量表达式的限制之一是您不能拥有:aa.i

对对象([expr.ass]、[expr.post.incr]、[expr.pre.incr])的修改,除非它应用于引用非易失性对象的非易失性左值的字面量类型生命周期开始于评估E

我们的对象 ,a.i并没有在这个表达式的计算中开始它的生命周期。因此,a.f()不是一个常量表达式,因此所有编译器都可以正确拒绝。

注意到这A().f();很好,因为现在我们在那里遇到了异常——A()在评估这个表达式的过程中开始了它的生命周期,所以A().i也这样做了,因此分配给它很好。

您可以将其视为A()常量评估者“知道”的含义,这意味着做事A().i = 3;完全没问题。同时,a是未知的——所以我们不能做a.i = 3;,因为我们不知道是什么a


如果g()是一个consteval函数,则 thea.f()将不再是立即调用,因此我们不再要求它本身是一个常量表达式。现在唯一的要求是g()常量表达式。

并且,当g()作为常量表达式求值时,A a;现在的声明在表达式的求值范围内,因此a.f()不妨碍g()成为常量表达式。


规则的不同是因为函数consteval需要在编译时调用,而函数仍然可以在运行时调用。constexpr

于 2022-01-12T14:48:54.477 回答