Search code examples
c++language-lawyerc++20consteval

Nested call of consteval functions with a reference argument


The following program

template<class T>
consteval auto foo(const T&) {
   return 0;
}

template<class T>
consteval auto bar(const T& t) {
   auto n = foo(t);
   return n;
}

int main() {
   static_assert(foo("abc") == 0);
   static_assert(bar("abc") == 0);
}

is built fine in GCC, but Clang rejects it with the messages:

error: call to consteval function 'foo<char[4]>' is not a constant expression
note: in instantiation of function template specialization 'bar<char[4]>' requested here
   static_assert(bar("abc") == 0);
note: function parameter 't' with unknown value cannot be used in a constant expression
   auto n = foo(t);

Demo: https://gcc.godbolt.org/z/M6GPnYdqb

Is it some bug in Clang?


Solution

  • This is a clang bug. gcc and msvc are correct to accept it.

    There are two relevant rules in question:

    All immediate invocations must be constant expressions. This comes from [expr.const]/13:

    An expression or conversion is in an immediate function context if it is potentially evaluated and either:

    • its innermost enclosing non-block scope is a function parameter scope of an immediate function, or
    • its enclosing statement is enclosed ([stmt.pre]) by the compound-statement of a consteval if statement ([stmt.if]).

    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.

    And touching an unknown reference is not allowed in constant expressions (this is [expr.const]/5.13):

    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 id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either
      • it is usable in constant expressions or
      • its lifetime began within the evaluation of E;

    For more on this latter rule, see my post on the the constexpr array size problem and my proposal to resolve this (hopefully for C++23).


    Okay, back to the problem. foo is obviously fine, it doesn't do anything.

    In bar, we call foo(t). This is not a constant expression (because t is an unknown reference), but we are in an immediate function context (because bar is consteval), so it doesn't matter that foo(t) is not a constant expression. All that matters is that bar("abc") is a constant expression (since that is an immediate invocation), and there's no rule we're violating there. It is pretty subtle, but the reference t here does have its lifetime begin within the evaluation of E -- since E here is the call bar("abc"), not the call foo(t).

    If you mark bar constexpr instead of consteval, then the foo(t) call inside of it becomes an immediate invocation, and now the fact that it is not a constant expression is relevant. All three compilers correctly reject in this case.