4

I have made a thread yesterday but I think that it was unclear and the replies I got didn't solve my confusion at all. So, I'll try to make the example simpler.

Why is this allowed:

constexpr int incr(int k1)
{
    return k1 + 5;
}

constexpr int foo(int k)  // runs in compile time
{
    return incr(k);
}

int main() {
    constexpr int x = 5;
    constexpr int y = foo(4);
}

... But this very similar function is not?

constexpr int incr(int k1)
{
    return k1 + 5;
}

constexpr int foo(int k)  // runs in compile time
{
    constexpr int x = incr(k); // k: not a constant expression
    return x;
}

int main() {
    constexpr int x = 5;
    constexpr int y = foo(4);
}

How exactly are these two functions different in the context of constant evaluation? To me they look almost exactly the same, except one returns instantly while the other first computes the value and assigns it to the constexpr variable.

If you can, please also refer to the standard so that I can read further!

user4581301
  • 33,082
  • 7
  • 33
  • 54
  • 1
    You have to remove `constexpr` from `constexpr int x = incr(k);`, then the whole `foo(4)` will end up constexpr. Function parameters are never considered constexpr inside of its body (so they can't be used as array sizes or template parameters, etc). – HolyBlackCat Mar 29 '23 at 17:55
  • 2
    That's exactly what I don't understand. Why is that? As in, why can't the compiler see that the parameter was a constexpr variable in the caller? – SomeoneWithPassion Mar 29 '23 at 17:55
  • 5
    `constexpr` functions _may or may not_ be evaluated at compile time, so you can not know whether `x` is in fact `constexpr` at this point. – tkausl Mar 29 '23 at 17:59
  • 1
    I see, so the reason the first example works and the second one doesn't, is because `constexpr` variables are always required to be evaluated at compile time? But since the function *may* be evaluated at run time, we canno guarantee that this variable will always be evaluated at compile time? – SomeoneWithPassion Mar 29 '23 at 18:01
  • Are you familiar with the difference between `constexpr` and [`consteval`](https://en.cppreference.com/w/cpp/language/consteval)? – Brian61354270 Mar 29 '23 at 18:01
  • 1
    Yeah. In fact my next question (related to my previous answer) was: if that's the case, why doesn't it still work with `consteval`? (The second example) – SomeoneWithPassion Mar 29 '23 at 18:02
  • 1
    Related: [A name for describing `consteval` function argument being known at compile time but not constexpr](https://stackoverflow.com/q/61777120/11082165) and [Will consteval allow using static_assert on function arguments?](https://stackoverflow.com/q/57226797/11082165) – Brian61354270 Mar 29 '23 at 18:08
  • Thanks, that seems very related and I'll make sure to read the document. Just one more question then (I don't know whether the doc answers it yet): So why aren't `constexpr` parameter disallowed? What's the rationale? Would make this a feature basically be the same as `auto` parameters? – SomeoneWithPassion Mar 29 '23 at 18:20

2 Answers2

2

The rule about constexpr variables is [dcl.constexpr]/6,

[...] In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression ([expr.const]). A constexpr variable that is an object, as well as any temporary to which a constexpr reference is bound, shall have constant destruction.

Every constant expression must be a core constant expression, with some additional restrictions that are not relevant here, so I won't get into them ([expr.const]/13). If the initialization of a constexpr variable is not a core constant expression, the program is ill-formed.

The particular rule violated by the second example is [expr.const]/5.9.

An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:

  • [...]
  • an lvalue-to-rvalue conversion unless it is applied to
    • a non-volatile glvalue that refers to an object that is usable in constant expressions, or
    • a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;

In the declaration

constexpr int x = incr(k);

the full-expression "initialize an int variable from incr(k)" fails to be a core constant expression because, when it is evaluated, it needs to perform an lvalue-to-rvalue conversion on k in order to get the value to initialize k1 with. The variable k is not usable in constant expressions, nor did its lifetime begin within the full-expression "initialize an int variable from incr(k)".

In the first example, which your compiler accepts, incr(k) is also being evaluated at compile time, but it is not required to be a core constant expression, so there is no problem. It's very confusing at first, but we need to remember that something that is not a core constant expression can be evaluated as part of a "bigger" evaluation that is a core constant expression. In this case, it is the enclosing constexpr variable initialization (that of y) that is required to be a core constant expression—and it is, because it creates the function parameter k (initializing it with the value 4) and then reads from it. To put it another way, if E is "initialize y with foo(4)", then the lifetime of k begins within E, so the read of k can occur without preventing E from being a core constant expression.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • Thanks for the reply. One thing I am not sure is why it would do a lvalue to rvalue conversion on k, I can’t find that as an implicit conversion anywhere? My example also does not work even if k1 is a reference. – SomeoneWithPassion Mar 30 '23 at 10:51
  • @SomeoneWithPassion You have to perform an lvalue-to-rvalue conversion on an `int` glvalue in order to read the value that it holds. According to [dcl.init.general]/16.9, a standard conversion is used to convert "lvalue of `int`" to the type being initialized (which is `int`). According to [conv.general]/6, the result of an implicit conversion to `int` is a prvalue of type `int`. So it must be produced by lvalue-to-rvalue conversion, which is described in [conv.lval]. – Brian Bi Mar 30 '23 at 12:34
  • @SomeoneWithPassion If you change `k1` to a reference, you merely postpone the point at which the lvalue-to-rvalue conversion is performed: it is no longer needed to initialize `k1`, but is needed when computing `k1 + 5`. – Brian Bi Mar 30 '23 at 12:35
  • I see. Thanks for the references to the standard. So using a reference would postpone the implicit conversion: and since the function in which k1 + 5 happens would have to be evaluated, it would still not be able to produce a constant expression because of the same lvalue-to-rvalue conversion rule? The same error is applied to "k" if I use a reference, with the only a new line saying "in expansion of foo(k)", which I assume is caused by the fact that the implicit conversion happens in `k1 + 5` this time and not in `k` (caller) itself. Is that correct? – SomeoneWithPassion Mar 30 '23 at 13:03
  • @SomeoneWithPassion Your first sentence is correct. That's why it still doesn't compile when `k1` is a reference. I'm not able to understand your other question. If you need to ask about a set of error messages then I suggest you post a new question. The medium of comments is not really suitable for such a discussion. – Brian Bi Mar 30 '23 at 13:24
  • Thanks, I'll accept this answer since it's detailed and refers to the standard! – SomeoneWithPassion Mar 30 '23 at 13:28
1

constexpr modifier, when applied to functions, merely gives a possibility of the function to be run at compile time (moreover, in the C++23 draft a constexpr function doesn't get the former restrictions anymore, thus you better read it as a contract, "promise" given by the author to support constexpr semantic). The modifier doesn't cancel the fact, that the same function might be run at run-time as well:

int main() {
    constexpr int compileVar = foo(2);
    int param = 4;
    int runtimeVar = foo(param);
}

Thus there is no guarantee that the arguments passed to a constexpr function evaluated at compile time.

On the other hand, constexpr modifier, when applied to variables, can only be evaluated at compile time, and the compiler reasonably prevents you from using a (possibly) runtime parameter of the function to evaluate a compile-time variable.

How exactly are these two functions different in the context of constant evaluation?

I think the main misconception here is that all parts of a constexpr function are evaluated at compile time if the function is evaluated at compile time, no matter if the variables inside have constexpr modifier or not. So the proper equivalent of this function:

constexpr int foo(int k)
{
    return incr(k);
}

would be:

constexpr int foo(int k)
{
    int x = incr(k);
    return x;
}
The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49
  • But this reasoning doesn't work well with consteval,: the same example (2) still wouldn't work by using consteval? "Function parameters are never usable in constant expressions". That's the whole point apparently, according to other similar resources I've been given and found on the internet... Or not? – SomeoneWithPassion Mar 29 '23 at 18:51
  • @SomeoneWithPassion the latest standard is somewhat obscure in regards to this exact scenario. I can see almost your code given in [\[expr.const\]/Example 4](https://eel.is/c++draft/expr.const#example-4), with clear and concise comments, but struggle to find which rule in [\[expr.const\]/5](https://eel.is/c++draft/expr.const#5) corresponds to it. Formerly the rule was clearly given in the same section, but the general idea is that `k` has to either be evaluated within the expression or be a `constexpr` itself – The Dreams Wind Mar 29 '23 at 19:48