Search code examples
c++c++23consteval

Calling a consteval function within if consteval causes error in non-constexpr context


The following code does not compile with g++ 14.1 or clang++ 18.1:

#include <type_traits>

consteval int
plusone(int n)
{
  return n+1;
}

constexpr int
maybeplusone(int n)
{
  if (std::is_constant_evaluated()) {
    n = plusone(n);
  }
  return n;
}

int
not_consteval()
{
  return maybeplusone(1);
}

The compiler complains that n is not a constant expression. If I change the if (std::is_constant_evaluated()) to if consteval, then the code compiles with clang++ but not g++. My questions:

  1. Why isn't the code valid?

  2. Should if consteval be different from if (std::is_constant_evaluted()), and if so is clang++ correct to accept the code with if consteval?

  3. Is there a better way to define a function like plusone that should not accidentally be called at runtime, but without causing such errors? Something like static_assert(std::is_runtime_evaluated())--except that's presumably useless as the static assert will always be evaluated at compile time.

For what it's worth, in my more complex application I have different memory allocators for compile time and run time, so I want to do something like this

if consteval {
  p = my_consteval_allocator(n);
}
else {
  p = my_real_allocator(n);
}

with similar logic for deallocation. Unfortunately, the compiler is trying to call my_consteval_allocator in non-constexpr contexts when it knows the allocation size required.


Solution

  • if (std::is_constant_evaluted()) is just a completely normal if control flow, only that std::is_constant_evaluted() evaluates to either true or false depending on whether or not the evaluation happens in a context that is manifestly constant-evaluated.

    In particular, if a call to a consteval function appears in an if (std::is_constant_evaluted()) branch then, usually, the call to this function must in itself be a constant expression where it appears.

    Because n is a function parameter whose value is not known at compile time plusone(n) when the function is defined, it is not a constant expression.

    if consteval has stronger semantics. It is known that the first branch of such an if can only ever be executed at compile time and therefore calls to consteval functions appearing within it are not required to be themselves constant expressions. They must only be constant subexpressions when evaluating the whole constant expression that leads to the call of the function containing the if consteval statement.

    So, always use if consteval and forget that std::is_constant_evaluted() exists, unless you need to support C++20. But if you need that, then you can't use consteval functions either.

    In not_consteval, the call to maybeplusone is not a context that is manifestly constant-evaluated and therefore the runtime branch of if consteval will be chosen. The code is valid with if consteval instead of if(std::is_constant_evaluated()) and it compiles for me on both GCC and Clang.


    Regarding your last example: With if consteval it should be fine and the compiler ought to not call my_consteval_allocator except in the context of an evaluation that is manifestly constant-evaluated. The items that can likely give you problems are the special cases for initialization of const integral or enumeration type variables and for static or thread storage duration variables. In these cases constant expression evaluation can happen although it isn't obvious from the syntax.

    However, it is a problem that the runtime and compile-time branches have different meaning. You can easily cause an allocation to happen in a manifestly constant-evaluated context, then leak the pointer into a runtime context and accidentally call the runtime deallocation on it. You would always have to manually verify the context in which the functions are called so that they match.

    Of course such a leak shouldn't happen. An allocation in a constant expression must also always be deallocated in the same expression. So if my_consteval_allocator uses new, then the compiler will complain about this situation already on the allocation anyway.