Will Consteval Allow Using Static_Assert on Function Arguments

Keeping consteval-ness of function arguments

In {fmt} 8.0 and later you can do this by using the format_string template that, as the name suggests, represents a format string (https://godbolt.org/z/bqvvMMnjG):

struct my_exception : std::runtime_error {
template <typename... T>
my_exception(fmt::format_string<T...> fmt, T&&... args)
: std::runtime_error(fmt::format(fmt, std::forward<T>(args)...)) {}
};

Will consteval functions allow template parameters dependent on function arguments?

No.

Whatever changes the paper will entail, which is little at this point, it cannot change the fact that a non-template function definition is only typed once. Moreover, if your proposed code would be legal, we could presumably find a way to declare a variable of type std::integral_constant<int, i>, which feels very prohibitive in terms of the ODR.

The paper also indicates that parameters are not intended to be treated as core constant expressions in one of its examples;

consteval int sqrsqr(int n) {
return sqr(sqr(n)); // Not a constant-expression at this point,
} // but that's okay.

In short, function parameters will never be constant expressions, due to possible typing discrepancy.

Nested call of consteval functions with a reference argument

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.

A name for describing `consteval` function argument being known at compile time but not constexpr

TL;DR: No, there is no term for such a thing in the C++ standard.

As far as the standard is concerned, "known at compile time" is not a thing. There is the concept of a "constant expression" and there is the concept of "constant evaluation".

A constexpr function (a function declared with constexpr or consteval) can be called in a constant expression context. A constant expression context is a place where the language requires an expression to be a constant expression. Template arguments, initializers for constexpr/constinit variables, and so forth are constant expression contexts.

When constexpr functions are called in a constant expression context, they produce constant expressions... or you didn't construct your function/arguments correctly and get a compile error. That's pretty much it, as far as the standard is concerned.

Oh yes, there are rules about constexpr functions. They are forbidden from performing certain C++ actions. And there are rules about invoking them from a constant expression context. But otherwise, that's it.

The distinction you're referring to is merely an outgrowth of what is allowed within a constexpr function. You can return the parameter of a constexpr function because the return value of a constexpr function is not grammatically a constant expression. That function can undergo constant evaluation given the right circumstances, but that's about all the standard needs to say on the subject.

The parameter itself is not special to C++. What's special is what the function's definition is (ie: is your function valid in accord with constexpr rules, and does this evaluation do non-constexpr things), how the function was called (ie: did you call it in a constant expression context), and how the parameter got filled in (ie: was the argument a constant expression).

To the standard, there are expressions which are constant expressions and expressions which are not. But expressions whose values are generated via constant expression evaluation but are not themselves linguistically constant expressions is just not a concept the standard needs to define. They're just values; whether they're in constant expression evaluation or not is not really relevant to the behavior of the program.

So there is no name for such things.

An immediate function (a function declared with consteval) is just a constexpr function with a couple of extra rules in it. Those rules prevent you from leaking the address of an immediate function (ie: compilers don't have to generate real functions for them). And the standard says that invoking an immediate function is always a constant expression context (and therefore must be invoked in accord with those rules).

Consteval constructor and member function calls in constexpr functions

The rule is, from [expr.const]/13:

An expression or conversion is in an immediate function context if it is potentially evaluated and its innermost non-block scope is a function parameter scope of an immediate function. 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.

Where, an immediate function is simply the term for (from [dcl.constexpr]/2):

A function or constructor declared with the consteval specifier is called an immediate function.

From the example:

struct A {       
int i;
consteval A() { i = 2; };
consteval void f() { i = 3; }
};

constexpr bool g() {
A a;
a.f();
return true;
}

The call a.f() is an immediate invocation (we're calling an immediate function and we're not in an immediate function context, g is constexpr not consteval), so it must be a constant expression.

It, by itself, must be a constant expression. Not the whole invocation of g(), just a.f().

Is it? No. a.f() mutates a by writing into a.i, which violates [expr.const]/5.16. One of the restrictions on being a constant expression is that you cannot have:

a modification of an object ([expr.ass], [expr.post.incr], [expr.pre.incr]) unless it is applied to a non-volatile lvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;

Our object, a.i, didn't begin its lifetime within the evaluation of this expression. Hence, a.f() isn't a constant expression so all the compilers are correct to reject.

It was noted that A().f(); would be fine because now we hit the exception there - A() began its lifetime during the evaluation of this expression, so A().i did as well, hence assigning to it is fine.

You can think of this as meaning that A() is "known" to the constant evaluator, which means that doing A().i = 3; is totally fine. Meanwhile, a was unknown - so we can't do a.i = 3; because we don't know what a is.


If g() were a consteval function, the a.f() would no longer be an immediate invocation, and thus we would no longer require that it be a constant expression in of itself. The only requirement now is that g() is a constant expression.

And, when evaluating g() as a constant expression, the declaration of A a; is now within the evaluation of the expression, so a.f() does not prevent g() from being a constant expression.


The difference in rules arises because consteval functions need to be only invoked during compile time, and constexpr functions can still be invoked at runtime.

static_assert compile-time argument check with C++20 concept

Function parameters are not constexpr.

template <Unsigned U, U N, U K>
constexpr double binom()
would allow your static_assert.



Related Topics



Leave a reply



Submit