[RFC] -fimplicit-constexpr

This is a proposal to add -fimplicit-constexpr (off by default) as an extension to Clang. This extension allows constant evaluation of functions not declared constexpr (or consteval) following limitations of the selected language mode.

This extension mirrors the same flag already shipped in GCC 12 and is highly useful for exploration work when making libraries constexpr-compatible (including the standard library for purposes of standardization). In my personal experience, it’s currently really easy and quick to explore code for constexprification in libstdc++ thanks to this flag in GCC. I would like to do the same thing with libc++.

Specification

Ignore expr.const#10.3

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 invocation of a non-constexpr function

Existing usage

This feature is used in some existing projects at github code search, but its main use-case in my opinion is a development tool for experimentation.

Relation to the C++ standard

It’s basically the same thing I proposed a long time ago in https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1235r0.pdf

The C++ specification for constant-evaluated code since then has changed significantly, focusing more on execution-related limitations (can’t evaluate for example reinterpret_cast) as opposed to the syntactic limitations of the past (can’t contain even unevaluated try/block). With this development, the constexpr keyword in front of a function is basically an opt-in for constant evaluation, and this extension is an opt-in for “everything which can be constant-evaluated according to the language rules minus the requirement for the constexpr keyword”.

In future, it is probably worth it to revisit the discussion of making the constexpr keyword not needed for functions with a body.

Two approaches to implementation

There are basically two approaches to implementing this feature:

Making functions implicitly constexpr

We can automatically add constexpr to all function declarations we see during parsing. This was the first approach I tried. But in older C++ versions, where the existence of some constructs (throw, unevaluated assembly) was directly forbidden in constexpr functions, this led to problems in cases where a forward declaration was implicitly marked constexpr, but the definition ended up containing such invalid constructs, which would cause us to reject the definition.

We can also check the function definition once we have body with CheckConstexprFunctionDefinition, and add constexpr conditionally only if it’s compatible. But this will lead to the constant evaluator sometimes erroring out on a call to non-constexpr function as a sole reason.

Removing checks in constant evaluator

As adding constexpr to everything at the declaration site is user-hostile in older language modes and since doing it conditionally will lead to confusion, I have instead looked into removing the check for the constexpr flag in the evaluator. This accomplishes the same result, and it also leads to better diagnostics, where the user will get the exact reason why something can’t be evaluated directly according to language rules and modes.

Problem with instantiation

The only problem I ran into was Clang implicitly instantiating member functions of templates only if they are marked constexpr. This extension needs to implicitly instantiate all member functions regardless of them being constexpr or not.

This is essentially the same behaviour as what would happen if these functions were marked constexpr by the user at their declarations.

An alternative approach would be lazy instantiation only when needed, but that would require being able to perform template instantiation in the AST library, which is currently impossible and outside of scope of this extension. This is a long-standing issue, see also issue 59966,which we have already discussed at length in the context of C++26’s proposed define_aggregate and friends.

To inline or not to inline?

The current GCC behaviour is to make functions implicitly constexpr only if they are (implicitly or explicitly) declared inline. I have talked with Jason Merrill who implemented this extension in GCC. He told me he is not opposed to allowing this for non-inline functions as well. The reason why he originally did it this way is that GCC by default (-fsemantic-interposition) avoids any interprocedural analysis in non-inline non-COMDAT functions in case they are replaced at runtime.

For now, the implementation requires free functions to be declared inline (note that class members with an inline definition are of course implicitly inline).

Implementation

I have the implementation available at PR#136436. It’s just a new flag and a function to query if a FunctionDecl can be implicitly constexpr. This query function is basically needed in three places: in SemaExpr.cpp to make sure any member functions of templates are instantiated and in ExprConstant.cpp’s CheckConstexprFunction function, as well as in its equivalent in the new interpreter.

In order to be usable for experimentation when making libraries constexpr-compatible, this needs to be implemented in Clang and can’t live outside the tree, as users would need to compile Clang themselves, which not every library developer is willing to do.

The PR contains tests checking various language versions and making sure that the presence of functions that are not constant-evaluatable in those language versions won’t break a build unless used explicitly in constant-evaluated context.

4 Likes

(it seems the link got messed up during copy-pasting: Clang and GCC differ in instantiation strategy of constexpr and incomplete types · Issue #59966 · llvm/llvm-project · GitHub)

I was looking at the testing in the PR and it is pretty bare bones, so I don’t really have a good feeling for the full implications here. We need to see a lot more test cases and I think the RFC itself could use with Tony Tables just showing the intent w/o all the clutter that testing can entail and make hard to digest.

I am especially curious to see how implicit constexpr interacts with undefined behavior. I am also curious to see how mixed implicit and explicit constexpr functions work in more than basic examples. I would hope that even if a explicit contexpr function is called by an implicit one it would treated as explict during its evaluation but that is not clear from reading your proposal and I did not see any tests in your PR that answer that question either.

I have a lot more questions but I would rather see a more complete set of tests and examples b/c maybe after seeing those they would be answered. We would require that anyway before we could land the PR. So sooner rather than later is better.

I am especially curious to see how implicit constexpr interacts with undefined behavior. I am also curious to see how mixed implicit and explicit constexpr functions work in more than basic examples. I would hope that even if a explicit contexpr function is called by an implicit one it would treated as explict during its evaluation but that is not clear from reading your proposal and I did not see any tests in your PR that answer that question either.

Difference between explicit and implicit constexpr with this flag will be exactly nothing. I consider constexpr a wrong default for C++ (at least in direction where it is heading) consteval means “only constant evaluated”, constexpr means both runtime and constant evaluated. With this extension what will you get is just “constant evaluator no longer checks if function has constexpr

All other checks for no UB inside of constant evaluation are untouched. But you still need to enter the constant evaluation (constexpr variable enforces it, static_assert too, const int variable will use it if it’s valid constant evaluation, …)

/* constexpr */ int runtime() {
  return 42;
}

int main() {
  // still normal runtime behaviour
  int a = runtime(); 

  // the evaluator no longers checks for _constexpr_ before the function
  constexpr int b = runtime(); 
}

(I will add additional tests showing this, but tomorrow, it’s getting late here)

1 Like

Added one more file with tests for now you can see examples where it is more visible:

implicit-constexpr-chain.cpp

And one more: constexpr-ub.cpp

This shows if you start running tests in constant evaluated context (maybe with just -fimplicit-constexpr) then you should be able to catch UB, which wouldn’t be possible unless you make everything constexpr.

How much do we care about this? For the use-cases you’ve outlined, I’m not sure why you wouldn’t just use the latest standard version.


I’m skeptical we want to encourage using this… it’s kind of like building your code with -fno-access-control. It’s convenient to bypass interface rules in headers, but you don’t really want to ship software that way.

1 Like

it can be useful in automotive, where I want constexpr, but I’m also at c++14 or c++17 and want to quickly look on what’s problematic before committing to actually doing the change.

1 Like

From a library point of view I’m very much against this. I don’t want users to rely on some code being constexpr without me saying it is. This kind of flag will lead to people saying “but it worked before”, and I have no interest in keeping everything that currently happens to work during constant evaluation that way. We already have way too much work to do, and supporting a viral extension is not on that list. I’d probably add an #error directive into the libc++ headers to avoid people actually using it with libc++ if this was accepted into Clang.

Given that the main motivation is experimentation, I also don’t see why this has to be in clang instead of having a fork. If this flag is something people should only use for experimentation, then we shouldn’t make it possible to do so without making it very clear that you are on your own if you use this flag.

1 Like

Would it help to emit a warning when you use this flag? I don’t want people to depend on it too. But I also want to have it available easily to make the experimentation not a high bar. If it means I need to compile it to start experimenting and not get it in brew / apple’s clang. Then the experimentation itself can be just adding constexpr everywhere manually but everytime I was doing it I forget about something and it took sometimes even few hours to get it right (like when I was doing constexpr containers proposal and want to have implementation experience in libc++).

I’m not opposed to changing the flag name, but to something like -fexperimental-implicit-constexpr-you-are-on-your-own I used current spelling only to mirror GCC exactly. As I said before, currently it’s much easier for WG21 members to look into constexprification of standard library over libstdc++ implementation.

I honestly believe there is a value in being able to turn this on, same as sometimes it is with -fno-access-control some project use when compiling tests. If you turn this on, run your tests and your tests macros can detect it … you should be run most of your tests in constant evaluation context and detect essentially all UB.

How would this interact with VLAs? It looks like GCC does not implicitly promote VLAs to constant sized arrays under -fimplicit-constexpr. Is that the intent? See Compiler Explorer. The test exercises both Clang and GCC with CONSTEXPR expanding to either nothing or constexpr. The right most panel demonstrates that GCC does not promote VLAs under -fimplicit-constexpr (with CONSTEXPR empty).

The link exercises this code:

CONSTEXPR int size() { return 1; }
void f() {
  using vla_t = int[size()];
  int vla[size()];
  vla_t *p = &vla;
}
1 Like

It works in GCC, you didn’t mark the function inline as it’s GCC’s (and this RFC’s associated PR’s too) requirement to make these implicitly constexpr

It should emit a diagnostic in -pedantic mode because it’s use of an extension.

1 Like

Ah, right, thanks! Is that the desired behavior? I’m uncertain since implicitly lifting VLAs to constant sized arrays has the potentially undesirable side effect of masking their (possibly unintended) use.

It will be constant evaluated only if its arguments are not depending on anything constant unknown. Otherwise it will be same as it was just a runtime. It’s same behavior as you would expect if you add constexpr yourself.

Thank you for the RFC!

Experimentation seems to be the main motivation for the feature, but can be done manually, so this isn’t really enabling something the user couldn’t already do themselves. But even if we take ease-of-use for granted, how many people are likely to want to enable this experimentally? context:global -file:.*.… - Sourcegraph has some data we can go off of, which suggests this isn’t really enabled often in practice (most of the 57 results are not actually using the flag). This seems likely to be helpful to members of WG21 and perhaps a few others, but I’m not seeing the evidence there’s a significant user community.

What is the impact on compile time overhead when enabling this flag on a large code base? I would expect it’s minimal overhead, but I’m wondering if you’ve measured.

1 Like

Being able to even answer such a question seems like, in of itself, a good reason to add the flag.

1 Like

Being able to answer the question doesn’t require exposing the flag to users. This can be done in a fork.

I did benchmark of build of full LLVM 20.1.3 release build (on M3 max) with itself with back-ported change (had some issues with main branch on Mac and didn’t want to look into them)

11759.09s user 501.72s system 1432% cpu 14:15.71 total (-fimplicit-constexpr)
11708.27s user 497.63s system 1424% cpu 14:16.84 total 

Difference is ~1s.

2 Likes

Hi @Hana, the Clang area team leads (@AaronBallman , myself, @efriedma-quic ) reviewed this RFC in our meeting today, and we agreed that the motivation for the feature doesn’t quite meet the bar for adding the proposed feature to the mainline Clang compiler.

Speaking for myself, I often say that “every use case is valid”, but I think the specific use case of constexpr-ifying library APIs, standard and otherwise, is fairly niche, and compiler flags have a tendency to live forever. I often find myself advocating for the increased usage of feature flags, but they do increase the configuration matrix of the compiler, and this has a meaningful impact on the maintainability and understandability of Clang’s code.

To reconsider the RFC, I think we’d want to see a stronger motivation, additional interest from other parties, or evidence that there is or will be a significant base of users interested in this flag. I know “no” is probably not the outcome you were looking for, but I hope at the very least this helps you decide how to proceed next.