I'm trying to figure out if doing mock sub-classing in C where the super-class struct is included wholesale in the sub-class struct, not just the sub-class and super-class having the same prefix of members.
In the example code below, I've tried to lay out what my thinking is:
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
enum type {
IS_A,
IS_B,
};
struct header {
enum type type;
};
struct a {
struct header hdr;
float x;
};
struct b {
struct header hdr;
int y;
};
void do_with_a(struct header *obj) {
if (obj->type == IS_A) {
struct a *a = (struct a *)obj;
printf("%f\n", a->x);
}
}
void do_with_b(struct header *obj) {
// Oops forgot to check the type tag
struct b *b = (struct b *)obj;
printf("%d\n", b->y);
}
int main() {
struct a *a = malloc(sizeof(*a));
a->hdr.type = IS_A;
a->x = 3.0;
do_with_a(&a->hdr);
do_with_b(&a->hdr);
}
I'm reasonably certain that do_with_b()
will always be undefined behavior if called with an "a". My primary question is whether do_with_a()
is always defined behavior (assuming I've set the type tag correctly) or if this is setting myself up for disaster when the compiler authors change their minds, or improve their analysis.
As a sub-question: I believe that converting an struct a *
to a struct header *
by &ap->hdr
or (struct header *)ap
would both be well-defined, is this the case?
Looking at the C11 standard, there seem to be two relevant passages, one in section 6.7.2.1 paragraph 15:
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...
And one in 6.5 paragraph 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...
Between these it's not clear to me if this is the intended interpretation of the standard, or if I'm being too hopeful.
I've tried the above code compiled in both GCC and clang, and neither seem to behave differently with optimizations on vs off. GCC, however, does signal warnings about both down-casts when set to -Wstrict-aliasing=1
. The language is somewhat vague, saying that it "might" break strict aliasing, where the description of that flag indicates that false-positives are quite common, so this is inconclusive:
undefined_test.c: In function ‘do_with_a’:
undefined_test.c:26:39: warning: dereferencing type-punned pointer might break strict-aliasing rules [-Wstrict-aliasing]
26 | struct a *a = (struct a *)obj;
| ^
undefined_test.c: In function ‘do_with_b’:
undefined_test.c:33:31: warning: dereferencing type-punned pointer might break strict-aliasing rules [-Wstrict-aliasing]
33 | struct b *b = (struct b *)obj;
|
Related question: will casting around sockaddr_storage and sockaddr_in break strict aliasing
The very end of the accepted answer just about answers the question, but does not seem satisfactory to me. Most of the comments on it seem to mainly refer to the case of the "common-prefix" rather than "nested-struct" case. It's not clear to me that the "nested-struct" case has been sufficiently defended.