Initial values for static objects must be constant expressions or string literals. (C 2018 6.7.9 3: “All the expressions in an initializer for an object that has static or thread storage duration shall be constant expressions or string literals.”)
6.6 7 specifies forms of constant expressions for initializers:
More latitude is permitted for constant expressions in initializers. Such a constant expression shall be, or evaluate to, one of the following:
— an arithmetic constant expression,
— a null pointer constant,
— an address constant, or
— an address constant for a complete object type plus or minus an integer constant expression.
Consider uint64_t bar = (uint64_t)foo + 3;
. foo
is nominally the static array declared earlier, which is automatically converted to a pointer to its first element. This qualifies as an address constant (6.6 9: “An address constant is … a pointer to an lvalue designating an object of static storage duration,… However, it is cast to uint64_t
, which no longer qualifies as an address constant, an address constant plus or minus a constant expression, or a null pointer constant.
Is it an arithmetic constant expression? 6.6 8 excludes it:
… Cast operators in an arithmetic constant expression shall only convert arithmetic types to arithmetic types,…
Thus, (uint64_t)foo + 3
does not qualify as any form of constant expression required by the C standard. However, 6.6 10 says:
An implementation may accept other forms of constant expressions.
So a C implementation may accept (uint64_t) foo + 3
or (uint64_t) foo | 3
as a constant expression. Our question is then why does your C implementation accept the former but not the latter.
A common feature of linkers and object module formats is that the object module can record placeholders for certain expressions, and the linkers can evaluate these expressions and replace the placeholders with calculated values. A primary purpose of this feature is to allow for code in a program to refer to places in data or other code whose locations are not completely known during compilation but that will be decided (at least relative to some base reference point) during linking.
Places in data or code are measured relative to symbols (names) defined in the object modules (or relative to the starts of sections or segments). Thus, a place may be described, in effect, as “34 bytes after the start of routine bar” or “8 bytes after the start of object baz”. So the object module has support for placeholders that are composed of a displacement and a symbol name. After the linker assigns addresses to symbols, it reviews each placeholder, adds the displacement to the assigned address, and replaces the placeholder with the calculated result.
It appears your compiler, in spite of the uint64_t
cast, is able to recognize that (uint64_t) foo
is still the address of foo
, and therefore (uint64_t) foo + 3
may be implemented by the regular use of one of these placeholders.
In contrast, the bitwise OR operator is not supported for use in these placeholders, and therefore the compiler is unable to implement (uint64_t) foo | 3
. It cannot evaluate the expression itself (because it does not know the final address for foo
), and it cannot write a placeholder for the expression. So it does not accept this as a constant expression.