[P1240R2] proposes that we should use a monotype,
std::meta::info, as part of a value-based reflection. One argument here is that lots of operations return ranges of
meta::info and it would be valuable if we could simply reuse our plethora of existing range algorithms for these use-cases.
Let’s investigate this claim.
We don’t need a working implementation of P1240 to test this, it’s enough to have this very, very loose approximation:
And let’s pick a simple problem. We start some sequence of… we’ll call them types:
We want to ensure that none of them are invalid. This is a problem for which we have a direct algorithm:
none_of. Let’s try using it:
❌. Ill-formed per 188.8.131.52 [expr.prim.id.general]/4:
Neither of those cases apply here.
❌. Ill-formed per 7.7 [expr.const]/13:
❌. Ill-formed per 7.7 [expr.const]/13 again. This time the lambda itself is fine, but now invoking the lambda from inside of
❌. Ill-formed per 7.7 [expr.const]/11:
Here, explicitly converting the lambda to a function pointer isn’t a permitted result because it’s an immediate function.
That exhausts the options here. What if instead of trying to directly
static_assert the result of an algorithm (or, equivalently, use it as an initializer for a constexpr variable or as the condition of an
if constexpr or as a non-type template argument), we did it inside of a new
✅. This one is actually valid per the language rules. Except, per the library rules, it’s unspecified per 184.108.40.206.1 [namespace.std]/6:
|❌. Ill-formed per 7.7 [expr.const]/13 still.|
|❌. Ill-formed per 7.7 [expr.const]/13 still.|
|✅. Valid. Language and library both.|
This leaves a lot to be desired. The only mechanism completely sanctioned by the language and library rules we have today is to write a bespoke consteval function which invokes the algorithm using a non-generic, consteval lambda that just wraps an existing library function and converts it to a function pointer.
Put simply: algorithms are basically unusable with the consteval rules we have.
The problem is that
consteval is a color [what-color].
consteval functions and function templates can only be called by other
consteval functions and function templates.
std::ranges::none_of, like all the other algorithms in the standard library, and probably all the other algorithms outside of the standard library, are not
consteval functions or function templates. Moreover, none of these algorithms are ever going to either become
consteval or be duplicated in order to gain a
consteval twin. Which means that none of these algorithms, including
none_of, can ever invoke any of the facilities proposed by [P1240R2].
One of the new C++23 features is
if consteval [P1938R3], which partially alleviates the
consteval color problem. That facility allows
consteval functions to be called by
constexpr functions in a guarded context, largely in order to solve the same sort of problem that
std::is_constant_evaluated() [P0595R2] set out to solve except in a way that would now allow you to invoke
The way it works is by establishing an immediate function context. This is important because the rule we were constantly running up against was that:
13 An immediate invocation shall be a constant expression.
And a call to an immediate function that is in an immediate function context is not an immediate invocation.
In our case here, we’re not trying to choose between a compile-time friendly algorithm and a run-time efficient one. We’re only trying to write compile-time code. But that’s okay, there’s no rule that says you need an
|❌. Ill-formed per 7.7 [expr.const]/13. Still.|
So… this is fine. We can still use
consteval functions with lambdas, as long as we just wrap all of our lambda bodies in
if consteval (but definitely also not make them
consteval - they’re only kind of
We could avoid putting the burden of sprinkling their code with
if consteval by pushing all of these extra calls into the library.
For instance, we could make this change to
But unfortunately that doesn’t actually work. If
pred(*first) were actually a
consteval call, then even duplicating the check in both branches of an
if consteval doesn’t help us. The call to
pred(*first) in the first sub-statement (the case where we are doing constant evluation) is fine, since now we’re in an immediate function context, but the call to
pred(*first) in the second sub-statement (the “runtime” case) is just as problematic as it was without the
So the attempted solution on the right isn’t just ridiculous looking, it also doesn’t help. The only library solution here (and I’m using the word solution fairly loosely) is to have one set of algorithms that are
constexpr and a completely duplicate set of algorithms that are
This has to be solved at the language level.
Let’s consider the problem in a very local way, going back to the lambda example and presenting it instead as a function (in order to simplify things a bit):
As mentioned multiple times,
pred_bad is ill-formed today because it contains a call to an immediate function outside a consteval context and that call isn’t a constant expression. That is one way we achieve the goal of the
consteval functions that they are only invoked during compile time. But
pred_good is good because that call only appears in an
if consteval branch (i.e. a consteval context), which makes the call safe.
What’s interesting about
pred_good is that while it’s marked
constexpr, it’s actually only meaningful during compile time (in this case, it’s actually UB at runtime since we just flow off the end of the function). So this isn’t really a great solution either. We need to ensure that
pred_good is only called at compile time.
But we have a way to ensure that:
pred_bad is today ill-formed, but only because we need to ensure that it’s not called at runtime. If we could ensure that, then we calling it during compile time is otherwise totally fine. What if the language just did that ensuring for us? If such
constexpr functions, that are only ill-formed because of calls to
consteval functions simply became
consteval, then we gain the ability to use them at compile time without actually losing anything - we couldn’t call them at runtime to begin with.
This paper proposes avoiding the
consteval coloring problem (or, at least, mitigating its annoyances) by allowing certain existing
constexpr functions to implicitly become
consteval functions when those functions can already only be invoked during compile time anyway.
Specifically, these three rules:
constexpr function contains a call to an immediate function outside of an immediate function context, and that call itself isn’t a constant expression, said
constexpr function implicitly becomes a
consteval function. This is intended to include lambdas, function template specializations, special member functions, and should cover member initializers as well.
If an expression-id designates a
consteval function without it being an immediate call in such a context, it also makes the context implicitly consteval. Such expression-id’s are also allowed in contexts that are manifestly constant evaluated.
Other manifestly constant evaluated contexts (like constant-expression and the condition of a constexpr if statement) are now considered to be immediate function contexts.
With these rule changes, no library changes are necessary, and any way we want to write the original call just works:
✅. First, the lambda becomes implicitly
✅. Now, the lambda is explicitly
✅. Still bad based on the library wording, but from the second proposed rule
✅. Previously, this was ill-formed because the conversion to function pointer needed to be (in of itself) a constant expression, but with the third proposed rule this conversion would now occur in an immediate function context. The “permitted result” rule no longer has to apply, so this is fine. As above,
✅. The use of
This has been implemented in EDG by Daveed Vandevoorde. One interesting example he brings up:
Today, even the call
f(1) is ill-formed, because naming
g isn’t allowed in that context (it is neither a subexpression of an immediate invocation nor in an immediate function context).
Per the proposal, the initialization of
r becomes valid.
f implicitly becomes a
consteval function template due to use of
r is at namespace scope, we tentatively try to perform constant initialization, which makes the initial parse manifestly constant evaluated. In such a context,
f(1) does not have to be a constant expression, so the fact that we’re returning a pointer to consteval function is okay. The subsequent invocation
g(2) is fine, and initializes
But even with this proposal, the initialization of
s is ill-formed. The tentative constant initialization fails (because
r isn’t a constant), and in the subsequent dynamic initialization,
f(1) is now actually an immediate invocation (
f still becomes implicitly
consteval, which now must be a constant expression, which now has the rule that its result must be a permitted result, in which context returning a pointer to consteval function is disallowed).
One of the issues with actually wording this proposal is its chicken-and-egg nature. Let’s consider the main example again:
And let’s work through both the status quo reasoning and the proposed reasoning.
|Current Reasoning||Proposed Reasoning|
Essentially, we have a flow of reasoning that starts with a function not being an immediate function and, because of that, becoming an immediate function. This is, admittedly, confusing. But I think it does make sense, and Daveed had less trouble implementing this than we had even attempting to try to reason about wording it.
This wording is known to be incomplete at this point, the primary goal in this revision is to at least set the stage so that people can understand the intent. The crux of the wording change is to extend 7.7 [expr.const]/13:
13 An expression or conversion is in an immediate function context if it is potentially evaluated and either:
- (13.1) its innermost enclosing non-block scope is a function parameter scope of an immediate function,
- (13.2) it is a subexpression of a manifestly constant-evaluated expression or conversion, or
- (13.3) its enclosing statement is enclosed ([stmt.pre]) by the compound-statement of a consteval if statement ([stmt.if]).
13a An expression or conversion is immediate-escalating if it is not initially in an immediate function context and it is either
- (13a.1) a potentially-evaluated id-expression that denotes an immediate function, or
- (13a.2) a potentially-evaluated explicit or implicit invocation of an immediate function that is not a constant expression.
13b An immediate-escalating function is:
- (13b.1) the call operator of a lambda that is declared with neither the constexpr nor consteval specifiers,
- (13b.2) a defaulted special member function that is declared with neither the constexpr nor consteval specifiers, or
- (13b.3) a function that results from the instantiation of a templated entity defined with the constexpr specifier.
13c An immediate function is a function or constructor that is:
An expression or conversion is an immediate invocation if it is
a potentially-evaluated explicit or implicit invocation of an immediate functionimmediate-escalating and is not in an immediate function context. An immediate invocation shall be a constant expression.
With which we can remove 220.127.116.11 [expr.prim.id.general]/4, which is now handled in the above:
And removing the current definition of immediate function from 9.2.6 [dcl.constexpr]/2, since it’s now defined (recursively) above.
constevalspecifier used in the declaration of a function declares that function to be a constexpr function. [Note: A function or constructor declared with the consteval specifier is
calledan immediate function ([expr.const]) -end note ]. A destructor, an allocation function, or a deallocation function shall not be declared with the consteval specifier.
Thanks to Daveed Vandevoorde and Tim Song for discussions around this issue and Daveed for implementing it.
[P0595R2] Richard Smith, Andrew Sutton, Daveed Vandevoorde. 2018-11-09. std::is_constant_evaluated.
[P1240R2] Daveed Vandevoorde, Wyatt Childers, Andrew Sutton, Faisal Vali. 2022-01-14. Scalable Reflection.
[P1938R3] Barry Revzin, Daveed Vandevoorde, Richard Smith, Andrew Sutton. 2021-03-22. if consteval.
[what-color] Bob Nystrom. 2015-02-01. What Color is Your Function?