Consteval constructor and member function calls in constexpr functions
Asked Answered
S

1

7
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

The program is rejected by all of GCC, Clang, MSVC and ICC and replacing constexpr on g by consteval results in all four accepting it.

However, when removing the call a.f();, still with constexpr on g, only ICC still rejects the code. The other three now accept it.

I don't understand why this is the case. My understanding is that without consteval on g, the expression a.f() is not in an immediate function context, which will cause the member function call itself to be evaluated as separate constant expression, which then cannot modify the i member because the member's lifetime didn't begin during evaluation of that constant expression.

But why can the constructor perform the same operation on the same object, in the same context? Is a's lifetime considered to have begun during the evaluation of the consteval constructor?


Also note that the presence of the static_assert doesn't affect these results. Removing in addition constexpr completely from g then also doesn't change anything about the compiler behavior.


As noted by @Enlico, even replacing both A a; and a.f(); by A{}.f(); with constexpr on g results in all compilers except ICC accepting the code, although by my understanding, this expression should result in evaluation of two separate constant expressions for the immediate constructor invocation and for the immediate member function invocation. I think the latter call should behave exactly as a.f();, making this even more confusing.

(After reading @Barry's answer, I realize now that the last sentence didn't make any sense. Correction: A{} would be one constant expression for the constructor immediate invocation and A{}.f() as a whole would be the second constant expression for the member function immediate invocation. This is clearly different from the expression a.f().)

Sublime answered 12/1, 2022 at 11:51 Comment(6)
Never discount the possibility of compiler bugs (not that I'm saying this is what you're seeing, but still).Kostman
If you change a.f(); to A{}.f();, you'll also see only ICC errors. Maybe you want to add this info too, to the question.Germanize
@Germanize Interesting. I have extended on my thought on that in the question. This seems to make it even more likely to be compiler bugs, I think.Sublime
Not necessarily weird. The full-expression is a constant expression in the case of A{}.f().Ginnie
@user17732522, I'm confused too. My biggest problem with C++20 is that Scott Meyers won't write a book about it :'(Germanize
@StoryTeller-UnslanderMonica Nevermind, for some reason I didn't realize that A{} would be a subexpression of the function call in A{}.f().Sublime
A
10

The rule is, from [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.

Where, an immediate function is simply the term for (from [dcl.constexpr]/2):

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

From the example:

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

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

The call a.f() is an immediate invocation (we're calling an immediate function and we're not in an immediate function context, g is constexpr not consteval), so it must be a constant expression.

It, by itself, must be a constant expression. Not the whole invocation of g(), just a.f().

Is it? No. a.f() mutates a by writing into a.i, which violates [expr.const]/5.16. One of the restrictions on being a constant expression is that you cannot have:

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;

Our object, a.i, didn't begin its lifetime within the evaluation of this expression. Hence, a.f() isn't a constant expression so all the compilers are correct to reject.

It was noted that A().f(); would be fine because now we hit the exception there - A() began its lifetime during the evaluation of this expression, so A().i did as well, hence assigning to it is fine.

You can think of this as meaning that A() is "known" to the constant evaluator, which means that doing A().i = 3; is totally fine. Meanwhile, a was unknown - so we can't do a.i = 3; because we don't know what a is.


If g() were a consteval function, the a.f() would no longer be an immediate invocation, and thus we would no longer require that it be a constant expression in of itself. The only requirement now is that g() is a constant expression.

And, when evaluating g() as a constant expression, the declaration of A a; is now within the evaluation of the expression, so a.f() does not prevent g() from being a constant expression.


The difference in rules arises because consteval functions need to be only invoked during compile time, and constexpr functions can still be invoked at runtime.

Asternal answered 12/1, 2022 at 14:48 Comment(4)
I guess my main question is why the constructor is allowed to access the i member. Inside the constructor does object whose lifetime began within the evaluation of E; apply to i to exempt the access from the constant expression restriction?Sublime
Very interesting. This seems like a defect to me, because now you have to have duplicate functions again if you want to use the function both in compiler and runtime context.Intramural
@Sublime Well... yes. It's the constructor. That's where the object begins.Asternal
@Intramural If you want to use the function in a runtime context, don't use consteval, that's not what it's for. Use constexprAsternal

© 2022 - 2024 — McMap. All rights reserved.