Why do we need to mark functions as constexpr?

Preventing client code expecting more than you’re promising

Say I’m writing a library and have a function in there that currently returns a constant:

awesome_lib.hpp:

inline int f() { return 4; }

If constexpr wasn’t required, you – as the author of client code – might go away and do something like this:

client_app.cpp:

#include <awesome_lib.hpp>
#include <array>

std::array<int, f()> my_array;   // needs CT template arg
int my_c_array[f()];             // needs CT array dimension

Then should I change f() to say return the value from a config file, your client code would break, but I’d have no idea that I’d risked breaking your code. Indeed, it might be only when you have some production issue and go to recompile that you find this additional issue frustrating your rebuilding.

By changing only the implementation of f(), I’d have effectively changed the usage that could be made of the interface.

Instead, C++11 onwards provide constexpr so I can denote that client code can have a reasonable expectation of the function remaining a constexpr, and use it as such. I’m aware of and endorsing such usage as part of my interface. Just as in C++03, the compiler continues to guarantee client code isn’t built to depend on other non-constexpr functions to prevent the “unwanted/unknown dependency” scenario above; that’s more than documentation – it’s compile time enforcement.

It’s noteworthy that this continues the C++ trend of offering better alternatives for traditional uses of preprocessor macros (consider #define F 4, and how the client programmer knows whether the lib programmer considers it fair game to change to say #define F config["f"]), with their well-known “evils” such as being outside the language’s namespace/class scoping system.

Why isn’t there a diagnostic for “obviously” never-const functions?

I think the confusion here is due to constexpr not proactively ensuring there is any set of arguments for which the result is actually compile-time const: rather, it requires the programmer to take responsibility for that (otherwise ยง7.1.5/5 in the Standard deems the program ill-formed but doesn’t require the compiler to issue a diagnostic). Yes, that’s unfortunate, but it doesn’t remove the above utility of constexpr.

So, perhaps it’s helpful to switch from the question “what’s the point of constexpr to consider “why can I compile a constexpr function that can never actually return a const value?”.

Answer: because there’d be a need for exhaustive branch analysis that could involve any number of combinations. It could be excessively costly in compile time and/or memory – even beyond the capability of any imaginable hardware – to diagnose. Further, even when it is practical having to diagnose such cases accurately is a whole new can of worms for compiler writers (who have better uses for their time). There would also be implications for the program such as the definition of functions called from within the constexpr function needing to be visible when the validation was performed (and functions that function calls etc.).

Meanwhile, lack of constexpr continues to forbid use as a const value: the strictness is on the sans-constexpr side. That’s useful as illustrated above.

Comparison with non-`const` member functions

  • constexpr prevents int x[f()] while lack of const prevents const X x; x.f(); – they’re both ensuring client code doesn’t hardcode unwanted dependency

  • in both cases, you wouldn’t want the compiler to determine const[expr]-ness automatically:

    • you wouldn’t want client code to call a member function on a const object when you can already anticipate that function will evolve to modify the observable value, breaking the client code

    • you wouldn’t want a value used as a template parameter or array dimension if you already anticipated it later being determined at runtime

  • they differ in that the compiler enforces const use of other members within a const member function, but does not enforce a compile-time constant result with constexpr (due to practical compiler limitations)

Leave a Comment