13

I know, that such questions were asked early (for example non-constexpr calls in constexpr functions), but let's take next code:

consteval int factorial(int n)
{
    return n <= 1 ? 1 : (n * factorial(n - 1));
}

factorial(5);

All is OK. We guarantee, that factorial(5) expression is resolved at compile time, because consteval. Right? If so, I think it should mean, that recursive factorial(n - 1) in call factorial(5) is resolved at compile time too. However, we too know, that in declaration int factorial(int n) parameter int n is just a variable, not constexpr. And this influences, if we try to do something like this:

consteval int factorial(int n)
{
  // 1 
  constexpr auto res = factorial(n - 1); // error: ‘n’ is not a constant expression
    
  // 2
  return n <= 1 ? 1 : (n * factorial(n - 1)); // hhhhmmmmmm...but all is ok.. 
}
    
factorial(5);

What we have?

  1. We call consteval function with literal constant. OK.
  2. Within consteval function we make recursive call to this function with non constexpr local parameter at row 2. And all is OK, though we call consteval function with non-constexpr value. Well, we can suggest, that compiler knows, that base call has been done as right consteval call factorial(5), and the whole final expression (with all internal code of factorial) should be interpreted as consteval. Yes? Or, why? Because...
  3. At row 1 we explicitly make a call as constexpr with non-constexpr value. And we get an error.

My question is next: why for explicit consteval call of factorial(5) compiler makes difference between explicit and implicit constexpr recursion call of factorial? Is it bug or feature?

0

1 Answer 1

8

Let's review what a constant expression is. A core constant expression is an expression which, when evaluated, does not cause one of a long list of "bad" behaviors. A constant expression is a core constant expression whose result is "allowed" by some other rules (not important here). In particular, note that these conditions are heavily non-syntactic: a constant expression are not defined positively by defining what expressions are constant expressions, but negatively by defining what constant expressions can't do.

A result of this definition is that an expression can be a constant expression even it requires the evaluations of many non-constant expressions (even non-core constant expressions). In the definitions

consteval int factorial1(int n) {
  if(n == 0) return 1;
  else { // making this correct since undefined behavior interferes with constant expressions
    /*constexpr*/ auto rec = factorial1(n - 1);
    return n * rec;
  }
}
consteval int factorial2(int n) {
  return n == 0 ? 1 : n * factorial2(n - 1);
}

the factorial1(n - 1) in factorial1 is not a constant expression, so adding constexpr to rec is an error. Similarly, the n == 0 ? 1 : n * factorial2(n - 1) in factorial2 is also not a constant expression. The reason is the same: both of these expressions read the value of (perform lvalue-to-rvalue conversion on) the object n, which did not start lifetime within the expression. But this is fine: the bodies of constexpr/consteval functions are simply not checked for being constant expressions. All constexpr really does is whitelist a function's calls for appearing in constant expressions. And, again, an expression can be constant (like factorial1(5)) even if you need to evaluate a non-constant expression on the way (like factorial(n - 1)). (In this case, when evaluating factorial1(5), the lifetime of the n object that is the parameter to factorial does start its lifetime within the expression being checked, so it can be read during evaluation.)

Two places where an expression will be checked for being a constant expression are initializations of constexpr variables and "non-protected" calls to consteval functions. The first one explains why adding constexpr to rec in factorial1 is an error: you're adding an additional check for a constant expression that is not done in the correct factorial1 function, and this extra check (correctly) fails. This should have answered your point 3.

For your point 2: yes, there's a special "protection" for consteval functions called from other consteval functions. Usually, a call to a consteval function is, right at the point it is written, checked for being a constant expression. As we've been discussing, this check would fail for the calls factorial1(n - 1) and factorial2(n - 1) in the above definitions. There is a special case built into the language to save them: a call to a consteval function in an immediate function context (basically, whose immediately enclosing function is also consteval) is not required to be a constant expression.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.