Suppose we have two structs:
typedef struct Struct1
{
short a_short;
int id;
} Struct1;
typedef struct Struct2
{
short a_short;
int id;
short another_short;
} Struct2;
Is it safe to cast from Struct2 * to Struct1 * ? What does the ANSI spec says about this?
I know that some compilers have the option to reorder structs fields to optimize memory usage, which might render the two structs incompatible. Is there any way to be sure this code will be valid, regardless of the compiler flag?
Thank you!
It is safe, as far as I know.
But it's far better, if possible, to do:
typedef struct {
Struct1 struct1;
short another_short;
} Struct2;
Then you've even told the compiler that Struct2 starts with an instance of Struct1, and since a pointer to a struct always points at its first member, you're safe to treat a Struct2 * as a Struct1 *.
struct pointers types always have the same representation in C.
(C99, 6.2.5p27) "All pointers to structure types shall have the same
representation and alignment requirements as each other."
And members in structure types are always in order in C.
(C99, 6.7.2.1p5) "a structure is a type consisting of a sequence of
members, whose storage is allocated in an ordered sequence"
No, the standard does't allow this; accessing the elements of a Struct2 object through a Struct1 pointer is undefined behavior. Struct1 and Struct2 are not compatible types (as defined in 6.2.7) and may be padded differently, and accessing them via the wrong pointer also violates aliasing rules.
The only way something like this is guaranteed to work is when Struct1 is included in Struct2 as its initial member (6.7.2.1.15 in the standard), as in unwind's answer.
The language specification contains the following guarantee
6.5.2.3 Structure and union members
6 One special guarantee is made in order to simplify the use of unions: if a union contains
several structures that share a common initial sequence (see below), and if the union
object currently contains one of these structures, it is permitted to inspect the common
initial part of any of them anywhere that a declaration of the completed type of the union
is visible. Two structures share a common initial sequence if corresponding members
have compatible types (and, for bit-fields, the same widths) for a sequence of one or more
initial members.
This only applies to type-punning through unions. However, this essentially guarantees that the initial portions of these struct types will have identical memory layout, including padding.
The above does not necessarily allow one to do the same by casting unrelated pointer types. Doing so might constitute a violation of aliasing rules
6.5 Expressions
7 An object shall have its stored value accessed only by an lvalue expression that has one of
the following types:
— 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 only question here is whether accessing
((Struct1 *) struct2_ptr)->a_short
constitutes access to the whole Struct2 object (in which case it is a violation of 6.5/7 and it is undefined), or merely access to a short object (in which case it might be perfectly defined).
It general, it might be a good idea to stick to the following rule: type-punning is allowed through unions but not through pointers. Don't do it through pointers, even if you are dealing with two struct types with a common initial subsequence of members.
It will most probably work. But you are very correct in asking how you can be sure this code will be valid. So: somewhere in your program (at startup maybe) embed a bunch of ASSERT statements which make sure that offsetof( Struct1.a_short ) is equal to offsetof( Struct2.a_short ) etc. Besides, some programmer other than you might one day modify one of these structures but not the other, so better safe than sorry.
Yes, it is ok to do that!
A sample program is as follows.
#include <stdio.h>
typedef struct Struct1
{
short a_short;
int id;
} Struct1;
typedef struct Struct2
{
short a_short;
int id;
short another_short;
} Struct2;
int main(void)
{
Struct2 s2 = {1, 2, 3};
Struct1 *ptr = &s2;
void *vp = &s2;
Struct1 *s1ptr = (Struct1 *)vp;
printf("%d, %d \n", ptr->a_short, ptr->id);
printf("%d, %d \n", s1ptr->a_short, s1ptr->id);
return 0;
}
Related
I have the following code:
#include <stdio.h>
#include <stdint.h>
typedef struct E_s {
uint32_t a;
uint32_t b;
uint32_t c;
} E_t;
typedef struct S_s {
uint32_t data_sz;
char data[];
} S_t;
typedef struct F_s {
E_t E;
S_t S;
char data[16];
//} __attribute__((packed)) full_msg_t;
} F_t;
int main(int argc, char* argv[])
{
F_t out;
printf("sizeof(out.data) = %lu\n", sizeof(out.data));
printf("sizeof(out.E) = %lu\n", sizeof(E_t));
printf("sizeof(out.S) = %lu\n", sizeof(S_t));
printf("sizeof(out) = %lu\n", sizeof(F_t));
return EXIT_SUCCESS;
}
When I run the code, I see the following output:
sizeof(out.data) = 16
sizeof(out.E) = 12
sizeof(out.S) = 4
sizeof(out) = 32
Question: Why is the size of S_t 4 (third line of output)? I was expecting it to be 8 (uint32_t + char[]). Why is the size of char[] not included?
Furthermore, both out.data and out.S.data point to the same memory location, which caused me to dive deep and find the above observation. Any clue here will also be very helpful. I was not expecting those 2 variables to overlap.
The standard specifies that the variable part of a structure with a flexible array member (FAM) is ignored when the size is calculated:
As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. In most situations, the flexible array member is ignored. In particular, the size of the structure is as if the flexible array member were omitted except that it may have more trailing padding than the omission would imply. However, when a . (or ->) operator has a left operand that is (a pointer to) a structure with a flexible array member and the right operand names that member, it behaves as if that member were replaced with the longest array (with the same element type) that would not make the structure larger than the object being accessed; the offset of the array shall remain that of the flexible array member, even if this would differ from that of the replacement array. If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it.
Emphasis added
Note that the struct F_s (aka F_t) should not be accepted; that violates the constraint in §6.7.2.1 ¶3:
A structure or union shall not contain a member with incomplete or function type (hence, a structure shall not contain an instance of itself, but may contain a pointer to an instance of itself), except that the last member of a structure with more than one named member may have incomplete array type; such a structure (and any union containing, possibly recursively, a member that is such a structure) shall not be a member of a structure or an element of an array.
The compiler should reject that (or, at least, emit a diagnostic) because constraint violations require a diagnostic. Even if the compiler doesn't reject it outright, you can't actually use the FAM of the embedded S_t because the data member of F_t doesn't move — the offsets of the elements of a structure are fixed at compile time. It would, de facto, use the data element of F_t, but that isn't defined behaviour.
In this struct:
typedef struct S_s {
uint32_t data_sz;
char data[];
} S_t;
The data member is a flexible array member. Such a member does not contribute to the size of a struct as its size is not specified. This is spelled out in section 6.7.2.1p18 of the C standard:
As a special case, the last element of a structure with more than one
named member may have an incomplete array type; this is called a
flexible array member. In most situations, the flexible array member
is ignored. In particular, the size of the structure is as if the
flexible array member were omitted except that it may have more
trailing padding than the omission would imply.
So the size of S_t does not include the data member, which is why sizeof(S_t) is 4.
Such a member can only be used when memory for the struct is dynamically allocated. For example:
S_t *s = malloc(sizeof(S_t) + 10);
This allows you to access from s->data[0] to s->data[9]
This also means that you can't put a struct with a flexible array member inside of another struct or in an array, because there's no way to know exactly where the flexible array member ends.
This is spelled out in section 6.7.2.1p3:
A structure or union shall not contain a member with incomplete or
function type (hence, a structure shall not contain an instance of
itself, but may contain a pointer to an instance of itself), except
that the last member of a structure with more than one named member
may have incomplete array type; such a structure (and any union
containing, possibly recursively, a member that is such a structure)
shall not be a member of a structure or an element of an array
char data[]; is a flexible array member and it is explicitly guaranteed not to have its size counted. Because it is mainly supposed to be used as malloc(sizeof(St_t) + n), where n is the size of the data array.
As for S_t S; inside the other struct, that's invalid C since the struct containing a flexible array member must be placed at the end and in the outer-most struct and you didn't do that. So your code doesn't compile in standard C and there it isn't possible to make assumptions that out.S.data and out.data are somehow the same memory, because all of that is beyond the scope of the C language. I suppose it might be possible that GNU C offers deterministic behavior in the form of non-standard extensions, but I'm not aware of any such guarantees.
Because char[] in structure S_t is called flexible array, which is a feature introduced in the C99 standard of the C programming language.
This maybe helpful flexible-array-members-structure-c
This code snippet prints the value 5. I don't understand why.
#include <stdio.h>
struct A
{
int x;
};
struct B
{
struct A a;
int y;
};
void printA(struct A *a)
{
printf("A obj: %d\n", a->x);
}
int main(void)
{
struct B b = {
{
5
},
10
};
struct A *a = (struct A*)&b;
printA(a);
printf("Done.\n");
return 0;
}
When I create b, a pointer to it would point to the data { {5}, 10 }.
When I cast &b to struct A*, I'm assuring the compiler that this struct A* points to a struct of a single data element of data type int. Instead, I'm providing it a pointer to a struct of two data elements of data types struct A, int.
Even if the second variable is ignored (since struct A has only one data member) I am still providing it a struct whose member is of data type struct A, not int.
Thus, when I pass in a to printA, the line a->x is performed, essentially asking to access the first data element of a. The first data element of a is of data type struct A, which is a type mismatch due to the %d expecting a digit, not a struct A.
What exactly is happening here?
When I create b, a pointer to it would point to the data { {5}, 10 }.
Yes, in the sense of that being the text of a type-appropriate and value-correct C initializer. That text itself should not be taken literally as the value of the structure.
When I cast &b to struct A*, I'm assuring the compiler that this
struct A* points to a struct of a single data element of data type
int.
No, not exactly. You are converting the value of the expression &b to type struct A *. Whether the resulting pointer actually points to a struct A is a separate question.
Instead, I'm providing it a pointer to a struct of two data
elements of data types struct A, int.
No, not "instead". Given that struct B's first member is a struct A, and that C forbids padding before the first member of a structure, a pointer to a struct B also points to a struct A -- the B's first member -- in a general sense. As #EricPostpischi observed in comments, the C standard explicitly specifies the outcome in your particular case: given struct B b, converting a pointer to b to type struct A * yields a pointer to b's first member., a struct A.
Even if the second variable is ignored (since struct A has only one
data member) I am still providing it a struct whose member is of data
type struct A, not int.
The first sizeof(struct A) bytes of the representation of a struct B form the representation of its first member, a struct A. That the latter is a member of the former has no physical manifestation other than their overlap in memory.
Even if the language did not explicitly specify it, given your declaration of variable b as a struct B, there would be no practical reason to expect that the expression (struct A*)&b == &b.a would evaluate to false, and there can be no question that the right-hand pointer can be used to access a struct A.
Thus, when I pass in a to printA, the line a->x is performed,
essentially asking to access the first data element of a.
Yes, and this is where an assertion enters that a really does point to a struct A. Which it does in your case, as already discussed.
The first
data element of a is of data type struct A,
No. *a is by definition a struct A. Specifically, it is the struct A whose representation overlaps the beginning of the representation of b. If there were not such a struct A then the behavior would be undefined, but that's not an issue here. Like every struct A, it has a member, designated by x, that is an int.
which is a type mismatch
due to the %d expecting a digit, not a struct A.
You mean expecting an int. And that's what it gets. That's what the expression a->x reads, supposing the behavior is defined at all, because that is the type of that expression. Under different circumstances the behavior might indeed not be defined, but under no circumstance does that expression ever provide a struct A.
What exactly is happening here?
What seems to be happening is that you are imagining different, higher-level semantics than C actually provides. In particular, you seem to have a mental model of structures as lists of distinguishable member objects, and that's leading you to form incorrect expectations.
Perhaps you are more familiar with a weakly typed language such as Perl, or a dynamically typed language such as Python, but C works differently. You cannot look at a C object and usefully ask "what is your type"? Instead, you look at each and every object through the lens of the static type of the expression used to access it.
The language-lawyer explanation of why the code is fine:
Any pointer in C may be converted to any other pointer type. (C17 6.3.2 §7).
If it is safe to dereference the pointed-at object after conversion depends on: 1) if the types are compatible and thereby correctly aligned, and 2) if the respective pointer types used are allowed to alias.
As a special case, a pointer to a struct type is equivalent to a pointer to its first member. The relevant part of C17 6.7.2 §15 says:
A pointer to a structure object,
suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in
which it resides), and vice versa.
This means that (struct A*)&b is fine. &b is suitably converted to the correct type.
There is no violation of "strict aliasing", since we fulfil C17 6.5 §7:
An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
a type compatible with the effective type of the object,
...
an aggregate or union type that includes one of the aforementioned types among its members
The effective type of the initial member being struct A. The lvalue access that happens inside the print function is fine. struct B is also an aggregate type that includes struct A among its members, so strict aliasing violations are impossible, regardless of the initial member rule cited at the top.
There is a special rule in the C standard for this case. C 2011 6.7.2.1 15 says:
A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa.
Take the following example:
typedef struct array_struct {
unsigned char* pointer;
size_t length;
} array;
typedef struct vector_struct {
unsigned char* pointer;
// Reserved is the amount of allocated memory not being used.
// MemoryLength = length + reserved;
size_t length, reserved;
} vector;
// Example Usage:
vector* vct = (vector*) calloc(sizeof(vector), 1);
vct->reserved = 0;
vct->length = 24;
vct->pointer = (unsigned char*) calloc(arr->length, 1);
array* arr = (array*) vct;
printf("%i", arr->length);
free(arr->pointer);
free(arr);
C seems to allocate memory for struct members in the order they're defined in the struct. Which means that if you cast vector -> array you'll still get the same results if you perform operations on array as you would as if you did it on vector since they have the same members and order of members.
As long as you only down cast from vector -> array as if array was a generic type for vector you shouldn't run into any problems.
Is this undefined and bad behavior despite the similar structure of the types?
This is well-defined behavior if you permit type aliasing (which C doesn't but most compilers do, either by default or by some compilation flag), and it is undefined behavior if you prohibit this type of type aliasing (which is commonly referred to as "strict aliasing" because the rules are pretty strict). From the N1570 draft of the C standard:
6.5.2.3
6 One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the complete type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.
That section is about unions, but in order for that behavior to be legal in unions, it restricts padding possibilities and thus requires the two structures to share a common layout and initial padding. So we've got that going for us.
Now, for strict aliasing, the standard says:
6.5
7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
a type compatible with the effective type of the object
[...]
A "compatible type" is:
6.2.7
1 Two types have compatible type if their types are the same.
It goes on to explain that more and list a few cases that have a little more "wiggle room" but none of them apply here. Unfortunately for you, the buck stops here. This is undefined behavior.
Now, one thing you could do to get around this would be:
typedef struct array_struct {
unsigned char* pointer;
size_t length;
} array;
typedef struct vector_struct {
array array;
size_t reserved;
} vector;
I can't seem to wrap my head around certain parts of the C standard, so I'm coming here to clear up that foggy, anxious uncertainty that comes when I have to think about what such tricks are defined behaviour and what are undefined or violate the standard. I don't care whether or not it will WORK, I care if the C standard considers it legal, defined behaviour.
Such as this, which I am fairly certain is UB:
struct One
{
int Hurr;
char Durr[2];
float Nrrr;
} One;
struct Two
{
int Hurr;
char Durr[2];
float Nrrr;
double Wibble;
} Two;
One = *(struct One*)&Two;
This is not all I am talking about. Such as casting the pointer to One to int*, and dereferencing it, etc. I want to get a good understanding of what such things are defined so I can sleep at night. Cite places in the standard if you can, but be sure to specify whether it's C89 or C99. C11 is too new to be trusted with such questions IMHO.
I think that technically that example is UB, too. But it will almost certainly work, and neither gcc nor clang complain about it with -pedantic.
To start with, the following is well-defined in C99 (§6.5.2.3/6): [1]
union OneTwo {
struct One one;
struct Two two;
};
OneTwo tmp = {.two = {3, {'a', 'b'}, 3.14f, 3.14159} };
One one = tmp.one;
The fact that accessing the "punned" struct One through union must work implies that the layout of the prefix of struct Two is identical to struct One. This cannot be contingent on the existence of a union because the a given composite type can only have one storage layout, and its layout cannot be contingent on its use in a union because the union does not need to be visible to every translation unit in which the struct is used.
Furthermore, in C all types are no more than a sequence of bytes (unlike, for example, C++) (§6.2.6.1/4) [2]. Consequently, the following is also guaranteed to work:
struct One one;
struct Two two = ...;
unsigned char tmp[sizeof one];
memcpy(tmp, two, sizeof one);
memcpy(one, tmp, sizeof one);
Given the above and the convertibility of any pointer type to a void*, I think it is reasonable to conclude that the temporary storage above is unnecessary, and it could have been written directly as:
struct One one;
struct Two two = ...;
unsigned char tmp[sizeof one];
memcpy(one, two, sizeof one);
From there to the direct assignment through an aliased pointer as in the OP is not a very big leap, but there is an additional problem for the aliased pointer: it is theoretically possible for the pointer conversion to create an invalid pointer, because it's possible that the bit format of a struct Two* differs from a struct One*. Although it is legal to cast one pointer type to another pointer type with looser alignment (§6.3.2.3/7) [3] and then convert it back again, it is not guaranteed that the converted pointer is actually usable, unless the conversion is to a character type. In particular, it is possible that the alignment of struct Two is different from (more strict than) the alignment of struct One, and that the bit format of the more strongly-aligned pointer is not directly usable as a pointer to the less strongly-aligned struct. However, it is hard to see an argument against the almost equivalent:
one = *(struct One*)(void*)&two;
although this may not be explicitly guaranteed by the standard.
In comments, various people have raised the spectre of aliasing optimizations. The above discussion does not touch on aliasing at all because I believe that it is irrelevant to a simple assignment. The assignment must be sequenced after any preceding expressions and before any succeeding ones; it clearly modifies one and almost as clearly references two. An optimization which made a preceding legal mutation of two invisible to the assignment, would be highly suspect.
But aliasing optimizations are, in general, possible. Consequently, even though all of the above pointer casts should be acceptable in the context of a single assignment expression, it would certainly not be legal behaviour to retain the converted pointer of type struct One* which actually points into an object of type struct Two and expect it to be usable either to mutate a member of its target or to access a member of its target which has otherwise been mutated. The only context in which you could get away with using a pointer to struct One as though it were a pointer to the prefix of struct Two is when the two objects are overlaid in a union.
--- Standard references:
[1] "if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible."
[2] "Values stored in non-bit-field objects of any other object type consist of n × CHAR_BIT
bits, where n is the size of an object of that type, in bytes. The value may be copied into
an object of type unsigned char [n] (e.g., by memcpy)…"
[3] "A pointer to an object type may be converted to a pointer to a different object type… When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object."
C99 6.7.2.1 says:
Para 5
As discussed in 6.2.5, a structure is a type consisting of a sequence
of members, whose storage is allocated in an ordered sequence
Para 12
Each non-bit-field member of a structure or union object is aligned in
an implementation-defined manner appropriate to its type.
Para 13
Within a structure object, the non-bit-field members and the units in
which bit-fields reside have addresses that increase in the order in
which they are declared. A pointer to a structure object, suitably
converted, points to its initial member (or if that member is a
bit-field, then to the unit in which it resides), and vice versa. There
may be unnamed padding within a structure object, but not at its
beginning
That last paragraph covers your second question (casting the pointer to One to int*, and dereferencing it).
The first point - whether it is valid to "Downcast" a Two* to a One* - I could not find specifically addressed. It boils down to whether the other rules ensure that the memory layout of the fields of One and the initial fields of Two are identical in all cases.
The members have to be packed in ordered sequence, no padding is allowed at the beginning, and they have to be aligned according to type, but the standard does not actually say that the layout needs to be the same (even though in most compilers I am sure it is).
There is, however, a better way to define these structures so that you can guarantee it:
struct One
{
int Hurr;
char Durr[2];
float Nrrr;
} One;
struct Two
{
struct One one;
double Wibble;
} Two;
You might think you can now safely cast a Two* to a One* - Para 13 says so. However strict aliasing might bite you somewhere unpleasant. But with the example above you don't need to anyway:
One = Two.one;
A1. Undefined behaviour, because of Wibble.
A2. Defined.
S9.2 in N3337.
Two standard-layout struct (Clause 9) types are layout-compatible if
they have the same number of non-static data members and corresponding
non-static data members (in declaration order) have layout-compatible
types
Your structs would be layout compatible and thus interchangeable but for Wibble. There is a good reason too: Wibble might cause different padding in struct Two.
A pointer to a standard-layout struct object, suitably converted using
a reinterpret_cast, points to its initial member (or if that member is
a bit-field, then to the unit in which it resides) and vice versa.
I think that guarantees that you can dereference the initial int.
The following code crashes my microprocessor at runtime.
struct dummytype dummy;
struct crummytype crummy;
*(unsigned int*)&dummy = *(unsigned int*)&crummy;
Assuming both structs are the same size, is there something about this code that is not valid C? Is its validity contingent on anything particular?
This is only valid if both structures have an unsigned int as the first member.
C99 §6.7.2.1/13
Within a structure object, the non-bit-field members and the units in which bit-fields
reside have addresses that increase in the order in which they are declared. A pointer to a
structure object, suitably converted, points to its initial member (or if that member is a
bit-field, then to the unit in which it resides), and vice versa. There may be unnamed
padding within a structure object, but not at its beginning.
Putting that simply, given an address of a structure object, you can cast that address to a pointer-to-first-member-type:
struct A
{
unsigned int n;
char junk[5];
};
struct A a;
unsigned int *p = (unsigned int *)&a; // OK. pointer is proper for first member type
unsigned long*x = (unsigned long *)&a; // BAD
In short, your code is only legitimate if both structure types have an unsigned int for their first member. Anything else is undefined behavior (void * not withstanding, but since it is non-dereferencable, it really isn't applicable here). Each structure type being "big enough" to hold an unsigned int isn't enough. Their first member must actually be an unsigned int.
Even through both structures have same size, overlapping behavior is undefined.
if the structures are like this ,above statement will gives result what you are looking.
struct dummytype
{
int a;
};
struct crummytype
{
int b;
};
if the structures are like this ,you can't say what would be the result.
struct dummytype
{
char name[20];
int a;
};
struct crummytype
{
char name1[20];
int b;
};
It's invalid C by both the alignment requirements of int and the aliasing rules (accessing an object via an lvalue of type different from the effective type of the object).
C99 draft standard states (Annex J):
J.2 Undefined behavior
The behavior is undefined in the following circumstances:
[...]
An object is assigned to an inexactly overlapping object or to an exactly overlapping
object with incompatible type (6.5.16.1).
then, for what concerns compatible types:
6.2.7 Compatible type and composite type
Two types have compatible type if their types are the same. Additional rules for
determining whether two types are compatible are described in 6.7.2 for type specifiers,
in 6.7.3 for type qualifiers, and in 6.7.5 for declarators. Moreover, two structure,
union, or enumerated types declared in separate translation units are compatible if their
tags and members satisfy the following requirements: If one is declared with a tag, the
other shall be declared with the same tag. If both are complete types, then the following
additional requirements apply: there shall be a one-to-one correspondence between their
members such that each pair of corresponding members are declared with compatible
types, and such that if one member of a corresponding pair is declared with a name, the
other member is declared with the same name. For two structures, corresponding
members shall be declared in the same order. For two structures or unions, corresponding
bit-fields shall have the same widths. For two enumerations, corresponding members
shall have the same values.
making the two structs the same size is not sufficient to make them compatible types, so the behavior is undefined.
Edit: for sake of completeness I add the excerpt cited by #PascalCuoq in a comment to another answer in this thread, which is also relevant:
6.5 Expressions
[...]
7
An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
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.