Does inheritance via unwinding violate strict aliasing rule?
Asked Answered
B

1

7

I have a struct X which inherits from struct Base. However, in my current setup, due to alignment, size of X is 24B:

typedef struct {
    double_t a;
    int8_t b;
} Base;

typedef struct {
    Base base;
    int8_t c;
} X;

In order to save the memory, I'd like to unwind the Base struct, so I created struct Y which contains fields from Base (in the same order, always at the beginning of the struct), so the size of the struct is 16B:

typedef struct {
    double_t base_a;
    int8_t base_b;
    int8_t c;
} Y;

Then I'm going to use instance of struct Y in a method which expects a pointer to Base struct:

void print_base(Base* b)
{
  printf("%f %d\n", b->a, b->b);
}
// ...
Y data;
print_base((Base*)&data);

Does the code above violates the strict aliasing rule, and causes undefined behavior?

Blueness answered 8/12, 2017 at 8:45 Comment(6)
Right, fixed the example codeBlueness
Why don't you simply declare Base base; in Y and call the function with print_base(&data.base);? It consumes the same memory as having the members of the struct in the other struct.Wyckoff
@Wyckoff it doesn't. Size of struct X, which uses your approach, takes 24 bytes, while struct Y only 16Blueness
It seems to me that the whole type X is irrelevant in this example. You are casting from Y to Base which are independent on X.Anthozoan
@Anthozoan I wanted to present the right approach, and explain the reason why don't I want to go for it.Blueness
FWIW see this older related question (#3766729). I believe the accepted answer there is wrong, so not marking this as a duplicate.Hildegardehildesheim
K
9

First, Base and Y are not compatible types as defined by the standard 6.2.7, all members must match.

To access an Y through a Base* without creating a strict aliasing violation, Y needs to be "an aggregate type" (it is) that contains a Base type among its members. It does not.

So it is a strict aliasing violation and furthermore, since Y and Base are not compatible, they may have different memory layouts. Which is kind of the whole point, you made them different types for that very reason :)

What you can do in situations like this, is to use unions with struct members that share a common initial sequence, which is a special allowed case. Example of valid code from C11 6.5.2.3:

union {
  struct {
    int alltypes;
  } n;
  struct {
    int type;
    int intnode;
  } ni;
  struct {
    int type;
    double doublenode;
  } nf;
} u;

u.nf.type = 1;
u.nf.doublenode = 3.14;
/* ... */
if (u.n.alltypes == 1)
  if (sin(u.nf.doublenode) == 0.0)
Katelyn answered 8/12, 2017 at 9:0 Comment(5)
Thanks. I'm not sure about the memory layout. Doesn't the standard guarantee, that the offset for two first fields (a,b in Base, base_a, base_b in Y) will be the same?Blueness
@MarcinKolny This is not about memory layout. Memory layout is the same in both types. This is about informing compiler optimizer. You shall define union {Base b; Y y;}; in your header to inform optimizer that he must reload the value b.a from memory anytime when the value y.base_a changed.Anthozoan
@Anthozoan thanks, that sounds interesting. Just curious, could you give me point to some articles where I could read more about this?Blueness
@MarcinKolny For example: blog.regehr.org/archives/1307 . To get more put "strict aliasing" on google.Anthozoan
It is memory layout and informing the compiler both. The compiler may choose to always allocate a struct on an aligned address, but will not necessarily do so if the struct is replaced with all the members that it contained. There may be a difference in padding bytes.Katelyn

© 2022 - 2024 — McMap. All rights reserved.