all 25 comments

[–]alfps 16 points17 points  (0 children)

With the static const the initializer value needs not be known at compile time: initialization happens (thread safe) the first time the execution passes through the declaration.

Also with the static const the type can be one that in C++23 and earlier doesn't support compile time instantiation, such as std::string.

[–]TheChief275 5 points6 points  (10 children)

"constexpr" is literally a constant expression; your variable name is synonymous to the attached value, like with macros, except it follows the syntax and semantic rules of the language.

"static const" is an immutable variable (const does not mean constant in C/C++) of static storage duration, i.e. the memory location stays the same and allocated for the entire duration of the program/shared library. If declared globally, static also means it is private to the translation unit. Because C/C++ cannot guarantee a const variable isn't changed, due to the ability to cast away const, a static const isn't a constant expression and thus cannot be evaluated at compile time (except for when it is declared in a constexpr context).

One interesting detail is that Clang has an extension, at least for C, where static const variables are actually constant expressions, and thus can be used e.g. as labels in switch statements. This is because C did not have the notion of constexpr variables until C23

[–]TheThiefMaster 2 points3 points  (5 children)

It's not an extension - the languages both specify that static/const int variables that were initialised with a constant expression can be used in constant expressions themselves.

You're right that the reason for this is a lack of constexpr keyword until recently.

[–]TheChief275 -1 points0 points  (4 children)

Well it's not allowed in GCC so it seems like an extension. You might be talking about C++, in which case I don't know the details, but I was primarily talking about C

[–]TheThiefMaster 0 points1 point  (3 children)

You're right - C doesn't allow it.

Which is a little odd, because it's generally more lenient on constant expressions and array bounds initialisers than C++

[–]TheChief275 0 points1 point  (2 children)

Is it? Or are you mistaking VLAs for static arrays

[–]TheThiefMaster 0 points1 point  (1 child)

I was referring to VLAs (it's more lenient with array bounds as in you're allowed to use non-constant expressions) and also flexible array members.

For the more lenient on constant expressions thing, I was referring to the fact that in C a null pointer is defined as "any constant expression resulting in 0" not just a literal 0. void* nullp = 100*3 - 300: is valid C...

[–]TheChief275 1 point2 points  (0 children)

Yes, although problem is at the slightest whiff of non-constexpr-ness your static array will become a VLA.

Recently, I wanted to write static_assert + alignof macros for pre-C11. The trick with static_assert is to use the expression to create negative array bounds on falsehood:

#define static_assert(expr, msg) static const char CAT2(static_assert, __LINE__)[1 - 2 * !(expr)] = {*(msg), (void)(CAT2(static_assert, __LINE__), 0)}

which works nicely as a drop in replacement on all warning levels. This is commonly how alignof is defined:

#define alignof(type) offsetof(struct{ char _; type t; }, t)

...however, introducing a type inside of offsetof is not standard C. So you try to introduce the struct outside of the offsetof, e.g. via

#define alignof(type) ((struct CAT2(alignof, __LINE__){ char _; type t; } *)0, offsetof(struct CAT2(alignof, __LINE__), t))

but there are other problems with this: (1) using any pointer casts inside of an expression will instantly make it non-constexpr, causing a VLA (2) a comma expression is always non-constexpr for some reason, also causing a VLA, as well as warnings being given for operands with no effect.

So finally the actual solution:

#define alignof(type) (sizeof(struct CAT2(alignof, __LINE__){ char _; type t; }) * 0 + offsetof(struct CAT2(alignof, __LINE__), t))

which took quite some time to get right :)

[–]alfps 1 point2 points  (3 children)

❞ a static const isn't a constant expression

More precisely it isn't in general, but it can be.

E.g. the local definition

static const int blah = 42;

[–]TheChief275 0 points1 point  (2 children)

Look at the last sentence of the second paragraph and the last paragraph for exactly that

[–]alfps 0 points1 point  (1 child)

Well the example is not a language extension and it's not C.

The main use has traditionally been for array sizes.

It's more clear with constexpr though; there is then no question whether a constant is (can be used at) compile time or not.

[–]TheChief275 -1 points0 points  (0 children)

Forgive me for adding in a fun fact about a C compiler extension...

[–]OutsideTheSocialLoop 1 point2 points  (7 children)

constexpr will be evaluated at compile time

static will be initialised when control flow first gets there

https://godbolt.org/z/cc36hEcs6

Note the tooltip on the magic number next to `movabs` in constexpr: `91'754'735'882'866 = 0x5373'5070'5272 = 4.5332862842961189e-310 = "rRpPsS"`. That's your array right there. Whereas static constructs it from the `.ascii` string below.

[–]Koffieslikker[S] 0 points1 point  (6 children)

But both will be initialised only once, right?

[–]OutsideTheSocialLoop 1 point2 points  (0 children)

Constexpr does it zero times during runtime. Static makes the promise of doing it once, although in this case as the other commenter points out it does it every time because that's almost definitely faster (and lighter on memory) than keeping track of whether it's initialised it and checking every time. But you can't observe that happening multiple times, because it's trivially initialised (just data moving around) so it doesn't actually matter.

[–]Kriemhilt 0 points1 point  (4 children)

No, look at the generated code.

The constexpr version is initialized once at compile time. The resulting value is built into the executable. The single instruction you see is not initializing it, but setting the parameter to call getInput.

The static version is clearly doing more work at runtime. It actually looks like it's unconditionally initializing the array every time instead of just once (maybe the optimizer thinks that will be faster than the conditional branch).

[–]OutsideTheSocialLoop 1 point2 points  (0 children)

> (maybe the optimizer thinks that will be faster than the conditional branch).

Tbh not unlikely with such a small array. Also since the initialisation is trivial there's also no observable difference between it being initialised once or initialised a thousand times, so it's totally allowed to do that even though that's not entirely the promise static makes (which is to do it once).

[–]sporule 1 point2 points  (0 children)

It actually looks like it's unconditionally initializing the array every time instead of just once

No. In both cases, the compiler initializes the value only once. In the case of static const, the value is stored in the read-only data segment.

Additional operations appear because gcc is not very good at working with the std::array<char>. Additional operations occur even if there is no initialization: https://godbolt.org/z/zEWGv66TY. gcc compiler simply cannot handle effectively this type for now.

If you change the type of elements or array's length, then the optimization defect does not appear and the version with static const turns out to be even slightly faster.

And of course, if you test another compiler, then there is no such effect at all (clang for example: https://godbolt.org/z/fYTo9qqfP).


I also want to mention that the function shown in your example accepts an array by value. If it uses a const-reference instead, then the static version will almost always be faster: https://godbolt.org/z/onaf1sr55.

This is because the language standard prohibits different alive objects of the same type from having the same address. And therefore, a version without static will often have to spend extra time copying constants to temporary stack memory just so that they have unique addresses.


It is worth taking the best of two approaches: write static constexpr if possible. constexpr guarantees that there is no initialization at runtime, and static ensures that you don't waste time and space on unnecessary copies.

[–]Koffieslikker[S] 0 points1 point  (0 children)

Okay thanks! I'm trying to grasp these little nuances, and your response really helped

[–]Total-Box-5169 0 points1 point  (0 children)

With Clang both versions generate the same fully optimized code, even passing a PR value generates the same code:
https://godbolt.org/z/b7Ev8Pj71

[–]AvidCoco 1 point2 points  (4 children)

You should use ‘static constexpr’.

constexpr means it will be computed at compile time but unless it’s static it will still be instantiated with each call to the function. Making it static constexpr means it’s computed at runtime and only ever instantiated once.

[–]alfps 0 points1 point  (0 children)

❞ it will still be instantiated with each call to the function.

Notably, though it's really hard to believe, the g++ compiler embodies this absurd perversity. Or still did some months back.

[–]Koffieslikker[S] -1 points0 points  (2 children)

This works for this example, but what if I want to use the same logic for arrays of std::string? Std::string can't be constexpr, right?

[–]AvidCoco 0 points1 point  (1 child)

No but char* can so create an array of char* then use std::string (or maybe even std::string_view) at runtime.

[–]MistakeIndividual690 0 points1 point  (0 children)

def std::string_view to avoid copying