I think being explicit is a good thing in C++. Suppose there is not constexpr in C++ and the following works:
inline int foo(int x) { return x + 42; }
int arr[foo(1)];
I think it would qualify as spooky action-at-a-distance if modifying foo causes arr to be malformed. And if they are in different libraries it restricts the ways the original function can be changed, making backwards compatibility slightly harder.
This would make more sense if constexpr was actually constant like say, Rust's const.
The Rust const fn foo which gives back x + 42 for any x, is genuinely assured to be executed at compile time when given a constant parameter. If we modify the definition of foo so that it's not constant the compiler rejects our code.
But C++ constexpr just says "Oh, this might be constant, or it might not, and, if it isn't don't worry about that, any constant uses will now magically fail to compile", exactly the spooky action at a distance you didn't want.
When originally conceived it served more or less the purpose you imagine, but of course people wanted to "generalize" it to cover cases which actually aren't constant and so we got to where we are today.
Slapping the constexpr keyword on a function is useless by itself, but it becomes useful when you combine it with a constexpr or constinit variable. Which is not all that different from Rust:
// C++
constexpr Foo bar() { /* ... */ }
constexpr Foo CONSTANT = bar(); // Guaranteed to be evaluated at compile time
constinit Foo VARIABLE = bar(); // Guaranteed to be evaluated at compile time
// Rust
const fn bar() -> Foo { /* ... */ }
const CONSTANT: Foo = bar(); // Guaranteed to be evaluated at compile time
static VARIABLE: Foo = bar(); // May or may not be evaluated at compile time
So Rust is actually less powerful than C++ when it comes to non-constant globals because AFAIK it doesn't have any equivalent to constinit.
That Rust constant named CONSTANT, is an actual constant (like an old-school #define) whereas in C++ what you've named CONSTANT is an immutable variable with a constant initial value that the language promises you can refer to from other constant contexts. This is a subtle difference, but it's probably at the heart of how C++ programmers end up needing constfoo, constbar, const_yet_more_stuff because their nomenclature is so screwed up.
The VARIABLE in Rust is an immutable static variable, similar to the thing you named CONSTANT in C++ and likewise it is constant evaluted so it happens at compile time, there is no "may or may not" here because the value is getting baked into the executable.
If we want to promise only that our mutable variable is constant initialized, like the C++ VARIABLE, in Rust we write that as:
let mut actually_varies = const { bar() }; // Guaranteed to be evaluated at compile time
And finally, yes if we write only:
let mut just_normal = bar(); // This may or may not be evaluated at compile time
So, I count more possibilities in the Rust, and IMNSHO their naming is clearer.
static VARIABLE: Foo = bar(); // May or may not be evaluated at compile time
Under what circumstances would this not be evaluated at compile time? As far as I know the initializer must be const.
Unlike C++, Rust does not have pre-main (or pre-_start) runtime initializers, unless you use something like the ctor crate. And the contents of that static must be already initialized, otherwise reading it (especially via FFI) would produce unknown values.
To evaluate something at runtime you'd have to use a LazyCell (and LazyCell::new() is const), but the LazyCell's construction itself will still be evaluated at compile time.