constexpr result from non-constexpr call
Asked Answered
A

1

9

Recently I was surprised that the following code compiles in clang, gcc and msvc too (at least with their current versions).

struct A {
    static const int value = 42;
};

constexpr int f(A a) { return a.value; }

void g() {
    A a;  // Intentionally non-constexpr.
    constexpr int kInt = f(a);
}

My understanding was that the call to f is not constexpr because the argument i isn't, but it seems I am wrong. Is this a proper standard-supported code or some kind of compiler extension?

Abduct answered 22/3, 2022 at 20:46 Comment(5)
how could i not be a compile-time constant?Hurds
"My understanding was that the call to f is not constexpr because the argument i isn't" - It isn't that simple. Which is good, because that makes constant evaluation more useful. Constant expressions can be produced so long as a fixed list of rules isn't violated during evaluation. So long as f and i don't violate those rules... nothing stops constant evaluation.Mathamathe
@MarcusMüller Because it was not declared as constexpr. For example int x = 5; constexpr int y = x; would not compile, even though the compiler knows the value of x.Abduct
but it is a compile-time constant! You not calling it constexpr doesn't change that.Hurds
AFAIK it'll work even with variable, but it'll generate standard runtime function for it.Treva
D
7

As mentioned in the comments, the rules for constant expressions do not generally require that every variable mentioned in the expression and whose lifetime began outside the expression evaluation is constexpr.

There is a (long) list of requirements that when not satisfied prevent an expression from being a constant expression. As long as none of them is violated, the expression is a constant expression.

The requirement that a used variable/object be constexpr is formally known as the object being usable in constant expressions (although the exact definition contains more detailed requirements and exceptions, see also linked cppreference page).

Looking at the list you can see that this property is required only in certain situations, namely only for variables/objects whose lifetime began outside the expression and if either a virtual function call is performed on it, a lvalue-to-rvalue conversion is performed on it or it is a reference variable named in the expression.

Neither of these cases apply here. There are no virtual functions involved and a is not a reference variable. Typically the lvalue-to-rvalue conversion causes the requirement to become important. An lvalue-to-rvalue conversions happens whenever you try to use the value stored in the object or one of its subobjects. However A is an empty class without any state and therefore there is no value to read. When passing a to the function, the implicit copy constructor is called to construct the parameter of f, but because the class is empty, it doesn't actually do anything. It doesn't access any state of a.

Note that, as mentioned above, the rules are stricter if you use references, e.g.

A a;
A& ar = a;
constexpr int kInt = f(ar);

will fail, because ar names a reference variable which is not usable in constant expressions. This will hopefully be fixed soon to be more consistent. (see https://github.com/cplusplus/papers/issues/973)

Degree answered 23/3, 2022 at 11:57 Comment(2)
I see, so basically, my intuition was that that calling a function would force lvalue-to-rvalue conversion, but it didn't, because the type was empty. Indeed, adding a non-static member to A makes the example fail to compile, but it still works when no function call is present, that is, constexpr int kInt = a.value;. Now it will be easier to rely on that feature. Thank you for explanation!Abduct
@Abduct Yes, and if you let the function take its argument by-reference, then it will work even if you add non-static members, since the only lvalue-to-rvalue conversion on the object happens in the copy constructor, which we can avoid this way.Degree

© 2022 - 2024 — McMap. All rights reserved.