22
$\begingroup$

I noticed C23 has constexpr but only for variables, not for functions. While C++ does, this would allow a function to be defined in such a way that if the function is called with compile-time constant arguments, it can be evaluated at compile time, and thus the results are also compile-time constants. But C23 explicitly did not add this capability.

Why might it be impractical to evaluate a function at compile-time?

$\endgroup$
3
  • 2
    $\begingroup$ Hi there! I may be misinterpreting your post, but the question in the title seems to be a little different from the one in the question (why should a language have constant time evaluation vs why shouldn't a language have compile time evaluation of functions). Could you please edit your post to clarify which of those your question is? $\endgroup$
    – user
    Jul 2, 2023 at 22:55
  • 1
    $\begingroup$ Not a C/C++ answer, but the Jai programming language has a #run statement capable of running any code at compile time. Here's Jonathan Blow showcasing a Space Invaders game, where you have to play and win a round for the compilation to succeed. $\endgroup$
    – BoppreH
    Jul 3, 2023 at 8:46
  • $\begingroup$ See also this answer of mine and its question on CS SE. Some kinds of programs are difficult to write, and this influences interpreters and compilers. I'm asking a followup question which will hopefully bridge the gap between communities. $\endgroup$
    – Corbin
    Jul 4, 2023 at 3:30

2 Answers 2

33
$\begingroup$

There are many things in C++ that C has chosen not to adopt. Every feature adds complexity to the language, which burdens both its users (since they may have to read or use code that uses it, even if they choose not to use it themselves) and compiler writers. C values simplicity much more than C++ does for a handful of reasons, some historical, some cultural, and some practical. C, much more so than C++, is often used as a “portable assembly” language, and C compilers exist for a staggering number of targets. Many of those compilers are quite simple, and they often struggle to implement even C99. While full constexpr support would not be a significant implementation obstacle for GCC or Clang, it would be for those smaller compilers.

Frankly, I suspect this appeal to simplicity is the main reason that C23 did not opt to adopt constexpr wholesale—after all, if people want to write C++, they can write C++—but perhaps it’s not entirely obvious why this feature adds so much complexity. For starters, let us agree it is desirable that expressions we evaluate at compile-time

  1. will not perform side-effects,

  2. will produce a result in finite time,

  3. behave in reasonably the same fashion on different platforms,

  4. can be implemented in a reasonably efficient fashion.

Point 1 is not terribly hard to guarantee with a sufficiently restrictive system. We will not be so lucky with the other ones.

In C++, constexpr already fails to provide point 2, as constexpr is Turing-complete. This is not especially radical for C++, since C++ template metaprogramming was already known to be Turing-complete as well, but it would be quite a bit more significant for C. C could have chosen to implement a restricted form of constexpr functions that are not Turing-complete, but supporting functions certainly makes it harder to be completely confident some Turing-completeness hasn’t snuck in.

Furthermore, even if constexpr functions were restricted enough to ensure they were not Turing-complete, this may not be comforting enough to some people. After all, a Turing-incomplete language can still express programs that take arbitrarily long amounts of time to evaluate. C compilation is currently quite fast, while C++ compilation has a well-earned reputation for being glacially slow; I imagine the C standards committee was not keen on opening that can of worms.

Point 3 sounds easy enough in theory, but keep in mind that C (somewhat infamously) has a great deal of both undefined behavior and implementation-defined behavior. What should expressions that trigger undefined or implementation-defined behaviors do when evaluated at compile time? Are all compiler vendors required to detect it and signal an error? Or should it simply be undefined or implementation-defined? These are thorny questions, and I imagine the committee did not feel like they were worth hashing out.

Finally, point 4 requires that every C compiler have some mechanism of running C code at compile-time that probably doesn’t involve just running the whole pipeline, generating a native binary, linking it, invoking it, and extracting the result. Furthermore, this all needs to be done while the compiler is in the middle of compiling something else! This adds a great deal of complexity to the task of implementing a C compiler and likely involves also writing a C interpreter, which is not as easy a feat as it may sound, especially if you are doing it in a systems language.

tl;dr: constexpr functions are just not in C’s complexity budget.

$\endgroup$
3
  • 3
    $\begingroup$ C++ constexpr also behaves as a function color, which results in the vast majority of code being constexpr-by-default, even if in practice it's never called in a constant-evaluable way. $\endgroup$
    – Bbrk24
    Jul 3, 2023 at 2:40
  • $\begingroup$ Point #2 could be tackled by specifying that the ability to resolve certain kinds of potentially-constant expressions is a quality-of-implementation issue, and allowing programmers to specify what should happen if a particular expression cannot be practically resolved at compile time (possible choices include lazy evaluation, initialization-time evaluation, falling back to normal function behavior, or refusing compilation). The situations where compile-time resolution would be most useful largely coincide with those where it would be cheapest; cases where it would be more expensive... $\endgroup$
    – supercat
    Dec 11, 2023 at 8:38
  • $\begingroup$ ...would more often benefit from the use of an outside tool to perform the computations and embed the results into the source seen by a C compiler. $\endgroup$
    – supercat
    Dec 11, 2023 at 8:39
18
$\begingroup$

I have no specific insight into the decisions made by the C language designers, but I can offer a few reasons why this might not have been included:

  • Replacing an expression like foo(23) with its result is only possible when foo is a pure function; if it has side-effects then the compiled program must execute those side-effects, and if the function depends on external state or is otherwise not deterministic, then replacing it with a constant result would likewise change the program's behaviour. Inferring or verifying purity isn't necessarily easy.

  • Related to purity, if the function foo depends directly or indirectly on outside context (e.g. environment variables), then evaluating it at compile-time would use the environment from the machine it was compiled on, instead of the environment from the machine the program is run on.

  • If foo(23) does not terminate, the compiler would generally have no way of knowing this in advance, so attempting to compute it at compile-time could hang the compiler in an infinite loop. Alternatively, the compiler might require extra complexity to stop trying to compute it at compile-time if it has taken too long. This could also cause the compiler itself to be non-deterministic, since the same program compiled twice might reduce foo(23) to a constant in one compilation but not the other.

  • If foo(23) takes a finite but very long time to compute, then the purpose of the program might be to compute it. In this case the user doesn't want it to be computed at compile-time, because the program might be intended to run on a different computer than the one it is compiled on. This is somewhat common in computational mathematics, where an exhaustive search for some combinatorial problem might be intended to run on a supercomputer, and be expected to take months to complete.

These problems can be summarised as: the compiler can't easily infer which functions it is correct or possible to do this with, and the compiler needs to avoid doing it if the user intends for the function to be evaluated at runtime.


Both of those concerns could be addressed by making compile-time evaluation of functions opt-in. Rust does this: a function can be declared const, in which case the compiler checks that the function doesn't do anything not allowed in a const function, and the user can choose not to label functions as const if they are not intended to be evaluated at compile-time.

However, this adds to the complexity of implementing the compiler, since it's also necessary to check that const functions obey the rules. It's also necessary to specify what those rules are, so that different compilers will agree on which functions are allowed to be declared const.

$\endgroup$
3
  • 1
    $\begingroup$ We seem to have written almost identical answers. :) I think I’ll keep mine, since it mentions a couple other details, but +1. $\endgroup$
    – Alexis King
    Jul 2, 2023 at 23:45
  • 1
    $\begingroup$ @AlexisKing There's some overlap but a lot in your answer that isn't in mine. $\endgroup$
    – kaya3
    Jul 2, 2023 at 23:46
  • 1
    $\begingroup$ I've upvoted both - very interesting and separate points. $\endgroup$
    – AnoE
    Jul 3, 2023 at 8:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .