Is compiler allowed to call an immediate (consteval) function during runtime?
Asked Answered
O

2

16

This might be a stupid question, but I am confused. I had a feeling that an immediate (consteval) function has to be executed during compile time and we simply cannot see its body in the binary.

This article clearly supports my feeling:

This has the implication that the [immediate] function is only seen at compile time. Symbols are not emitted for the function, you cannot take the address of such a function, and tools such as debuggers will not be able to show them. In this matter, immediate functions are similar to macros.

The similar strong claim might be found in Herb Sutter's publication:

Note that draft C++20 already contains part of the first round of reflection-related work to land in the standard: consteval functions that are guaranteed to run at compile time, which came from the reflection work and are designed specifically to be used to manipulate reflection information.

However, there is a number of evidences that are not so clear about this fact.

From cppreference:

consteval - specifies that a function is an immediate function, that is, every call to the function must produce a compile-time constant.

It does not mean it has to be called during compile time only.

From the P1073R3 proposal:

There is now general agreement that future language support for reflection should use constexpr functions, but since "reflection functions" typically have to be evaluated at compile time, they will in fact likely be immediate functions.

Seems like this means what I think, but still it is not clearly said. From the same proposal:

Sometimes, however, we want to express that a function should always produce a constant when called (directly or indirectly), and a non-constant result should produce an error.

Again, this does not mean the function has to be evaluated during compile time only.

From this answer:

your code must produce a compile time constant expression. But a compile time constant expression is not an observable property in the context where you used it, and there are no side effects to doing it at link or even run time! And under as-if there is nothing preventing that

Finally, there is a live demo, where consteval function is clearly called during runtime. However, I hope this is due to the fact consteval is not yet properly supported in clang and the behavior is actually incorrect, just like in Why does a consteval function allow undefined behavior?

To be more precise, I'd like to hear which of the following statements of the cited article are correct:

  1. An immediate function is only seen at compile time (and cannot be evaluated at run time)
  2. Symbols are not emitted for an immediate function
  3. Tools such as debuggers will not be able to show an immediate function
Oaxaca answered 19/10, 2019 at 17:28 Comment(12)
I guess it could be called during link time without violating anything you've quoted (depending on how you interpret "compile time" - does that include the linker?).. With Link Time Optimization a lot of stuff happens at link time.Haakon
Well, always keep in mind the as-if-rule.Mullite
Well, link time in this regard is much closer to compile time, then to run time. It still is a little disturbing to me, since there might be weird interference with other TUs, but I just want to have a clear picture in mind.Oaxaca
@Mullite If "as-if" allows the compiler to move something that should clearly be done on compile/link time to run time, then it does not seem that "as-if-y" to me.Oaxaca
@Oaxaca compile time versus run time seems very "as-if"y to me. If the final output of the program is identical regardless of the computation being done at compile or run time, then the compiler can do either since doing one is the same "as if" it had done the other. If there is no observable difference in the behaviour of the abstract machine, that the standard is implemented in terms of, then "as-if" applies (afaik).Haakon
@JesperJuhl but it kind of defeats the purpose of consteval, doesn't it?..Oaxaca
None of this is guaranteed by the standard—are you asking for “typical implementations” (of the future)?Incurrent
@DavisHerring I would be interested in both points of views: theoretical and practical.Oaxaca
@Oaxaca It doesn't defeat the purpose of it being a fatal error if the value is not a compile time constant. You can rely on that analysis and error, regardless of the compiler then still desiding to calculate the value at run time. Note: I'm speculating here. I agree that it defeats the "programmer intended" behaviour (and efficiency), but I don't think such behaviour actually violates the letter of the law (the standard).Haakon
@JesperJuhl How is the compiler expected to be able to emit diagnostics if computation is deferred?Precincts
@Precincts It can do the computation, emit the diagnostic, then defer the calculation (even though it has already done it) and do it again at run time. Silly, yes, but permitted.Haakon
@JesperJuhl Yeeees. It can also insert empty long loops randomly here and there...Precincts
D
11

To be more precise, I'd like to hear which of the following statements of the cited article are correct:

  1. An immediate function is only seen at compile time (and cannot be evaluated at run time)
  2. Symbols are not emitted for an immediate function
  3. Tools such as debuggers will not be able to show an immediate function

Almost none of these are answers which the C++ standard can give. The standard doesn't define "symbols" or what tools can show. Almost all of these are dealer's choice as far as the standard is concerned.

Indeed, even the question of "compile time" vs. "run time" is something the standard doesn't deal with. The only question that concerns the standard is whether something is a constant expression. Invoking a constexpr function may produce a constant expression, depending on its parameters. Invoking a consteval function in a way which does not produce a constant expression is il-formed.

The one thing the standard does define is what gets "seen". Though it's not really about "compile time". There are a number of statements in C++20 that forbid most functions from dealing in pointers/references to immediate functions. For example, C++20 states in [expr.prim.id]/3:

An id-expression that denotes an immediate function shall appear only

  • as a subexpression of an immediate invocation, or

  • in an immediate function context.

So if you're not in an immediate function, or you're not using the name of an immediate function to call another immediate function (passing a pointer/reference to the function), then you cannot name an immediate function. And you can't get a pointer/reference to a function without naming it.

This and other statements in the spec (like pointers to immediate function not being valid results of constant expressions) essentially make it impossible for a pointer/reference to an immediate function to leak outside of constant expressions.

So statements about the visibility of immediate functions are correct, to some degree. Symbols can be emitted for immediate functions, but you cannot use immediate functions in a way that would prevent an implementation from discarding said symbols.

And that's basically the thing with consteval. It doesn't use standard language to enforce what must happen. It uses standard language to make it impossible to use the function in a way that will prevent these things from happening. So it's more reasonable to say:

  1. You cannot use an immediate function in a way that would prevent the compiler from executing it at compile time.

  2. You cannot use an immediate function in a way that would prevent the compiler from discarding symbols for it.

  3. You cannot use an immediate function in a way that would force debuggers to be able to see them.

Quality of implementation is expected to take things from there.

It should also be noted that debugging builds are for... debugging. It would be entirely reasonable for advanced compiler tools to be able to debug code that generates constant expressions. So a debugger which could see immediate functions execute is an entirely desirable technology. This becomes moreso as compile-time code grows more complex.

Dew answered 19/10, 2019 at 19:48 Comment(8)
"even the question of "compile time" vs. "run time" is something the standard doesn't deal with" That's just patently false, and absurd. Are you saying that C++ doesn't mandate compile time computations? Compile time type checking?Precincts
@curiousguy: The standard doesn't mandate "compilation" of any sort. It declares what is well formed code and what is not, as well as what the behavior of well-behaved code will be. If compilation is not a thing in the standard, then neither is "compile time".Dew
I did some search through the standard, and it seems like it really tries to avoid mentioning anything related to compilation (e.g. in most cases it appears in notes), but there are exceptions. Namely, description of standard library and this funny paragraph: eel.is/c++draft/temp.expl.spec#8Oaxaca
@NicolBolas I think we can all agree that the std doesn't mandate separate compilation, a separate linker, or even the ability to link with obj files from other compilers. There is no distinction made between "compiler errors" and "linker errors" in the std. There is no guarantee that linking will be distinct or that asm or binary code won't be generated or optimized during linking. And yet I feel these remarks were not your point. Also compilation is a generic term for translation to exec code, and it can describe the generation of unlinked or linked code.Precincts
@Oaxaca Yes; see previous comment. Well formed code must "compile" and "link"; usually different constraints are checked during "compilation" and "linking". The std doesn't enforce that.Precincts
Some good answers in there @NicolBolas. One nit: "Statements like this make it essentially impossible for a pointer/reference to an immediate function to actually be a thing you can use." It's perfectly fine to use pointers/references to consteval functions in constant-expressions, and particularly in consteval functions. In fact, our current proposal for reflection (P1240R1) counts on that (see, e.g., the members_of API).Catabasis
All that said, our (EDG's) implementation of consteval functions currently doesn't ever generate target code for them. So they'll behave like @Oaxaca expects. I'd expect that to be true for most mainstream implementations. (The behavior seen on Compiler Explorer is presumably because that implementation is still in flux.)Catabasis
@DaveedV.: I updated the answer based on your correction. It turns out that the draft spec I was using was slightly older; N4835 has updated working that more neatly spells out how you can and cannot use the names of immediate functions.Dew
C
7

The proposal mentions:

One consequence of this specification is that an immediate function never needs to be seen by a back end.

So it is definitely the intention of the proposal that calls are replaced by the constant. In other words, that the constant expression is evaluated during translation.

However, it does not say it is required that it is not seen by the backend. In fact, in another sentence of the proposal, it just says it is unlikely:

It also means that, unlike plain constexpr functions, consteval functions are unlikely to show up in symbolic debuggers.


More generally, we can re-state the question as:

Are compilers forced to evaluate constant expressions (everywhere; not just when they definitely need it)?

For instance, a compiler needs to evaluate a constant expression if it is the number of elements of an array, because it needs to statically determine the total size of the array.

However, a compiler may not need to evaluate other uses, and while any decent optimizing compiler will try to do so anyway, it does not mean it needs to.

Another interesting case to think about is an interpreter: while an interpreter still needs to evaluate some constant expressions, it may just do it lazily all the time, without performing any constant folding.

So, as far as I know, they aren't required, but I don't know the exact quotes we need from the standard to prove it (or otherwise). Perhaps it is a good follow-up question on its own, which would answer this one too.

For instance, in [expr.const]p1 there is a note that says they can, not that they are:

[Note: Constant expressions can be evaluated during translation. — end note]

Chocolate answered 19/10, 2019 at 18:9 Comment(9)
It might be worth stating that compilers don’t have to do anything: the whole thing could be interpreted. The common strategy of knowing how many bytes each variable occupies when writing a binary is what makes compilers “definitely need” to evaluate constant expressions.Incurrent
@DavisHerring Yeah, the "definitely need" was about those cases. Good point on interpreters, I will add it.Chocolate
The “quotes we need from the standard” are simply that only the observable behavior of the program is constrained. Debuggers and “compile time” simply do not exist.Incurrent
@curiousguy: The implementation need not have user-visible “compile” and “run” steps: think about something like the online IDEs that just let you “run some code” (ignoring how that’s typically implemented).Incurrent
@DavisHerring I don't know those. When is the program parsed? When is overload resolution done? Etc.Precincts
@curiousguy: The important bit is that (via a minimal interface) you can’t tell when they happen, only that the correct results appear.Incurrent
@DavisHerring Frankly I have no idea what you are saying. Of course the different phases of compilations might not be apparent to the user, and compile/link might be so deeply tied that you don't know when one ends and the other begins. But you certainly can see f.e. that undefined names are diagnosed before execution in compiled languages, unlike in command shells.Precincts
@curiousguy: Even that isn’t required: a conforming implementation must issue at least one diagnostic for an ill-formed (non-NDR) program, but nothing stops it from also executing it, perhaps up to the line containing the error, so long as it does eventually produce the diagnostic even if that line is never executed. I’m not saying that you should expect to find such an unusual implementation in the wild—analyzing C++ is too complex to have novelty interpreters. But you can’t properly understand questions like “Is a compiler allowed to…” without such considerations.Incurrent
@DavisHerring Allowing execution of incomplete programs that cannot link, by making all accesses to the missing symbols (read/write of objects, calling for functions) call abort() (possibly via a signal) doesn't seem very complex, and would be practically useful.Precincts

© 2022 - 2024 — McMap. All rights reserved.