constexpr 函数中的 Consteval 构造函数和成员函数调用

Consteval constructor and member function calls in constexpr functions

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 拒绝,将 g 上的 constexpr 替换为 consteval 导致所有四个都接受它。

但是,当删除调用 a.f(); 时,仍然在 g 上使用 constexpr,只有 ICC 仍然拒绝该代码。其他三个现在接受了。

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

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


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


如@Enlico 所述,即使将 A a;a.f(); 替换为 A{}.f();g 上的 constexpr 也会导致除 ICC 之外的所有编译器都接受代码,虽然根据我的理解,这个表达式应该导致对直接构造函数调用和直接成员函数调用的两个单独的常量表达式的评估。我认为后一个调用的行为应该与 a.f(); 完全一样,这让这更令人困惑。

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

明显不同

规则是,从[expr.const]/13:

An expression or conversion is in an immediate function context if it is potentially evaluated and its innermost non-block scope is a function parameter scope of an immediate function. An expression or conversion is an immediate invocation if it is a potentially-evaluated explicit or implicit invocation of an immediate function and is not in an immediate function context. An immediate invocation shall be a constant expression.

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

A function or constructor declared with the consteval specifier is called an immediate function.

来自示例:

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

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

调用 a.f() 是立即调用(我们调用的是立即函数,我们不在立即函数上下文中,gconstexpr 而不是 consteval), 所以它必须是常量表达式.

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

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

a modification of an object ([expr.ass], [expr.post.incr], [expr.pre.incr]) unless it is applied to a non-volatile lvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;

我们的对象 a.i 并未在此表达式的求值内开始其生命周期。因此,a.f() 不是常量表达式,因此所有编译器都可以正确拒绝。

有人指出 A().f(); 会很好,因为现在我们在那里遇到了异常 - A() 在这个表达式的评估期间开始了它的生命周期,所以 A().i 也这样做了,因此分配给它很好。

你可以认为这意味着 A() 对常量求值器来说是“已知的”,这意味着 A().i = 3; 完全没问题。同时,a 是未知的 - 所以我们不能做 a.i = 3; 因为我们不知道 a 是什么。


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

并且,在将 g() 作为常量表达式求值时,A a; 的声明现在在表达式的求值范围内,因此 a.f() 不会阻止 g()从常量表达式开始。


之所以出现规则上的差异,是因为consteval函数需要在编译时调用,而constexpr函数仍然可以在运行时调用。