WG14 member Jens Gustedt says in a blog post on strict aliasing rules:
Character arrays must not be reinterpreted as objects of other types.
Is that, in fact, true? (I guess the corresponding language in the standard is the part saying that if an object has a declared type, then that type is its effective type.) If so, does it mean that an allocator that parcels out memory from a statically declared memory region is unimplementable in standard C?
I know TeX ignores most of Pascal’s type system and treats everything as an array of words because of a similar issue, but I hoped that if I ever found myself in a similar situation in (malloc-less) C, I could just declare a maximally aligned char array and keep using structs the usual way. I also fail to see what the point of _Alignas could possibly be in such a world, except as a standardized device for expressing non-standard requirements (similar to volatile).
The rules on aliasing are specified in section 6.5p7 of the C standard:
An object shall have its stored value accessed only by an lvalue expression that has one of the following types: 88)
- a type compatible with the effective type of the object,
- a qualified version of a type compatible with the effective type of the object,
- a type that is the signed or unsigned type corresponding to the effective type of the object,
- a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
- an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
- a character type.
- The intent of this list is to specify those circumstances in which an object may or may not be aliased
Note that this list allows any object to be accessed via a char *, but not the reverse, i.e. an object declared as an array of one or more characters can't be accessed as an lvalue of some other type.
This also means that malloc can't be implemented in a standard compliant way, since there's no way to create memory with no effective type without it. However malloc is considered part of the implementation and therefore can take of advantage of its knowledge of implementation internals to return a pointer to a block of memory that a compliant program can use.
The wording “Character arrays must not be reinterpreted as objects of other types” is imprecise. A correct statement is that if you reinterpret a character array as an object of another type (except as allowed by C 2018 6.5 7), the C standard does not define the behavior.
As always, if we want to accomplish a task, and the C standard does not define the behavior we want, we can look to other things to define the behavior we want.
If so, does it mean that an allocator that parcels out memory from a statically declared memory region is unimplementable in standard C?
Such an allocator is unimplementable in strictly conforming C, which is C code that does not rely on an unspecified, undefined, or implementation-defined behavior (and does not exceed any minimum implementation limit). It is entirely possible to write such an allocator in conforming C, which is C with extensions. Quite simply, one could put the memory allocation routines in one source file and compile them with a switch that supports aliasing memory as different types. (This is an extension, such as GCC’s -fno-strict-aliasing switch.) Then, when compiling other source files with common compilers, the compiler is blind to the effective type of the memory in the memory allocation source file, so it cannot be affected by the fact that the memory allocation routines use character arrays. (This is another extension, albeit the behavior arises implicitly from our understanding of how compilers and linkers are designed.)
The Standard clearly allows implementations which are intended to be suitable for tasks requiring static memory pools to extend the semantics of the language to support them, and allows "conforming" (but not strictly conforming) programs to exploit such extensions. In fact, the vast majority of C implementations can be configured to support such tasks in mutually-compatible fashion. The Standard does not require that implementations or configurations which are not intended to be usable for such purposes support such constructs. Implementations which don't support the constructs necessary to accommodate static memory pools would, almost by definition, be unsuitable for tasks requiring static memory pools, but the Standard makes no attempt to require that all implementations be suitable for all purposes.
Consequently, when writing rules about type-based aliasing, the authors of the Standard did not exercise anything near the level of care that would have been appropriate if they intended such rules to serve as a boundary between programs that should be expected to work and programs that shouldn't. It may seem odd that C99 rules which have never been even remotely satisfactory, as evidenced by the confusion and controversy surrounding them for the last 20 years, have remained unchanged, but there's a simple reason for that: changing the rules would require reaching a consensus as to what they're supposed to say, and it would be impossible to write a single set of rules, suitable for all purposes, to distinguish between operations that should or should not be regarded as meaningful, since the question of whether an implementation should be expected to process a construct meaningfully depends upon the purposes for which it is designed and configured.
When the Standard characterizes an action as "undefined behavior", or as violating a constraint, it means nothing more nor less than that the Standard itself imposes no requirements about how implementations process code in the relevant situation. The Standard makes no attempt to distinguish actions which are clearly erroneous from those which may not be portable to every conceivable implementation but should be expected to behave identically on 99% of them. Nor does the Standard make much effort to consider all of the corner cases where an action which would generally invoke UB might (and perhaps should) be processed in the same meaningful way by all implementations.
Code which expects to do "weird" things with memory should be processed using configurations that make allowance for that, even if the code is strictly conforming. Handling all of the tricky corner cases in the rules as written would require foregoing optimizations that would often be useful, and both clang nor gcc will ignore such corner cases rather than forego the optimizations. The question of whether a piece of code will be processed meaningfully thus depends much more strongly on compiler configuration than upon whether the code jumps through all the hoops given in the Standard.