9

I've been trying to work out how legal the below is and I could really use some help.

#include <stdio.h>
#include <stdlib.h>

typedef struct foo {
    int foo;
    int bar;
} foo;

void make_foo(void * p)
{
    foo * this = (foo *)p;

    this->foo = 0;
    this->bar = 1;
}

typedef struct more_foo {
    int foo;
    int bar;
    int more;
} more_foo;

void make_more_foo(void * p)
{
    make_foo(p);

    more_foo * this = (more_foo *)p;
    this->more = 2;
}

int main(void)
{
    more_foo * mf = malloc(sizeof(more_foo));

    make_more_foo(mf);
    printf("%d %d %d\n", mf->foo, mf->bar, mf->more);

    return 0;
}

As far as I've gathered, doing this is type punning and is supposed to violate the strict aliasing rule. Does it, though? The pointers passed around are void. You are allowed to interpret a void pointer any way you wish, correct?

Also, I read that there may be memory alignment issues. But struct alignment is deterministic. If the initial members are the same, then they'll get aligned the same way, and there should be no problems accessing all foo members from a more_foo pointer. Is that correct?

GCC compiles with -Wall without warnings, the program runs as expected. However, I'm not sure if it's UB or not and why.

I also saw that this:

typedef union baz {
    struct foo f;
    struct more_foo mf;
} baz;

void some_func(void)
{
    baz b;
    more_foo * mf = &b.mf; // or more_foo * mf = (more_foo *)&b;

    make_more_foo(mf);
    printf("%d %d %d\n", mf->foo, mf->bar, mf->more);
}

seems to be allowed. Because of the polymorphic nature of unions the compiler would be ok with it. Is that correct? Does that mean that by compiling with strict aliasing off you don't have to use an union and can use only structs instead?

Edit: union baz now compiles.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
Vlad Dinev
  • 432
  • 2
  • 9
  • 1
    `union baz` is invalid and doesn't compile. Please fix. – Paul Ogilvie Feb 13 '18 at 17:59
  • The AA violation, _I think_, happens with `make_foo(p); more_foo * this = (more_foo *)p; this->more = 2;` as the compiler can ignore that `p` and `this` point to overlapped data. So the _order_ of evaluation of `make_foo(p);` and `more_foo * this = (more_foo *)p; this->more = 2;` can happen in either order or _concurrent_. Now the compiler could think it can use 2x wide `int` access to `this` and over-write what `make_foo(p);` did. Sounds contrived, yet that is AA. – chux - Reinstate Monica Feb 13 '18 at 19:54
  • 1
    "You are allowed to interpret a void pointer any way you wish, correct?" No. Any `void*` can be changed to a character pointer or to its original pointer type. But if the pointer was originally the address of an `int`, it may not covert to a `struct foo*`. I do not think this issue negates your investigation here though. – chux - Reinstate Monica Feb 13 '18 at 19:57
  • C does specify "All pointers to structure types shall have the same representation and alignment requirements as each other." C11dr §6.2.5 28 so struct alignment may not be an issue. (unless this alignment spec refers to the pointer itself). – chux - Reinstate Monica Feb 13 '18 at 20:00
  • @chux What do you mean by "the compiler could think it can use 2x wide `int` access" ? The width of an `int` is known. Even if the order of evaluation is switched the members do not overlap in memory. – Vlad Dinev Feb 13 '18 at 23:01
  • @chux "All pointers to structure types shall have the same representation and alignment requirements as each other." This may be a stupid question, but what does that even mean? – Vlad Dinev Feb 13 '18 at 23:08
  • "Even if the order of evaluation is switched the members do not overlap in memory" but the compiler does not need to know if they overlap or not. Due to AA, it can assume the _entire_ `*this` and `*p` do not overlap. Assume the compiler might have access to a fast wide 128-bit read instruction and read the entire `*this` at once into a reg. (Compiler also padded `struct more_foo` to 128 bits.) Does `this->more = 2;` in the reg and writes it out back to `*this`. At the _same time_ `make_foo(p);` was occurring. The order of these two is indeterminate and UB per AA - as I understand it. – chux - Reinstate Monica Feb 13 '18 at 23:12
  • @VladDinev C allows for many architectures, not just flat ones. I take [All pointers to structure types ...](https://stackoverflow.com/questions/48772697/plain-c-polymorphism-type-punning-and-strict-aliasing-how-legal-is-this?noredirect=1#comment84555966_48772697) to mean any `struct *` is similarly encoded like other `struct *` and they point to data in the same memory area that shares a common alignment requirement. `char` objects may exist elsewhere with a different alignment and pointer representation. Same for `double` object may exist elsewhere. – chux - Reinstate Monica Feb 13 '18 at 23:25
  • @chux I would think there wouldn't be any SA violation inside of `make_more_foo` as the only conversions are to/from `void *`. It's given a `void *`, passes to another function expecting a `void *`, then converts it to a `more_foo *` and dereferences. – dbush Mar 22 '18 at 15:14

2 Answers2

2

The authors of the Standard didn't think it necessary to specify any means by which an lvalue of a struct or union's member type may be used to access the underlying struct or union. The way N1570 6.5p7 is written doesn't even allow for someStruct.member = 4; unless member if of character type. Being able to apply the & operator to struct and union members wouldn't make any sense, however, unless the authors of the Standard expected that the resulting pointers would be useful for something. Given footnote 88: "The intent of this list is to specify those circumstances in which an object may or may not be aliased", the most logical expectation is that it was only intended to apply in cases where lvalues' useful lifetimes would overlap in ways that would involve aliasing.

Consider the two functions within the code below:

struct s1 {int x;};
struct s2 {int x;};
union {struct s1 v1; struct s2 v2;} arr[10];

void test1(int i, int j)
{
  int result;
  { struct s1 *p1 = &arr[i].v1; result = p1->x; }
  if (result)
    { struct s2 *p2 = &arr[j].v2; p2->x = 2; }
  { struct s1 *p3 = &arr[i].v1; result = p3->x; }
  return result;
}

void test2(int i, int j)
{
  int result;
  struct s1 *p1 = &arr[i].v1; result = p1->x;
  if (result)
    { struct s2 *p2 = &arr[j].v2; p2->x = 2; }
  result = p1->x; }
  return result;
}

In the test1, even if i==j, all pointer that will ever be accessed during p1's lifetime will be accessed through p1, so p1 won't alias anything. Likewise with p2 and p3. Thus, since there is no aliasing, there should be no problem if i==j. In test2, however, if i==j, then the creation of p1 and the last use of it to access p1->x would be separated by another action which access that storage with a pointer not derived from p1. Consequently, if i==j, then the access via p2 would alias p1, and per N1570 5.6p7 a compiler would not be required to allow for that possibility.

If the rules of 5.6p7 are applicable even in cases that don't involve actual aliasing, then structures and unions would be pretty useless. If they only apply in cases that do involve actual aliasing, then a lot of needless complexity like the "Effective Type" rules could be done away with. Unfortunately, some compilers like gcc and clang use the rules to justify "optimizing" the first function above and then assuming that they don't have to worry about the resulting alias which is present in their "optimized" version but wasn't in the original.

Your code will work fine in any compiler whose authors make any effort to recognize derived lvalues. Both gcc and clang, however, will botch even the test1() function above unless they are invoked with the -fno-strict-aliasing flag. Given that the Standard doesn't even allow for someStruct.member = 4;, I'd suggest that you refrain from the kind of aliasing seen in test2() above and not bother targeting compilers that can't even handle test1().

supercat
  • 77,689
  • 9
  • 166
  • 211
0

I'd say it isn't strict since if you change "foo" structure, "more foo" structure will have to change with it . "foo" must become the base of "more foo", this is inheritance, not quite polymorphism. But you can use function pointers to introduce polymorphism to help with these structures.

Example

#include <stdio.h>
#include <stdlib.h>

#define NEW(x) (x*)malloc(sizeof(x));

typedef struct 
{
    void(*printme)(void*);

    int _foo;
    int bar;

} foo;

typedef struct 
{
    // inherits foo
    foo base;
    int more;
} more_foo;


void foo_print(void *t)
{
    foo *this = (foo*)t;

    printf("[foo]\r\n\tfoo=%d\r\n\tbar=%d\r\n[/foo]\r\n", this->bar, this->_foo);
}

void more_foo_print(void *t)
{
    more_foo *this = t;

    printf("[more foo]\r\n");

    foo_print(&this->base);

    printf("\tmore=%d\r\n", this->more);

    printf("[/more foo]\r\n");
}


void foo_construct( foo *this, int foo, int bar )
{
    this->_foo = foo;
    this->bar = bar;

    this->printme = foo_print;
}

void more_foo_construct(more_foo *t, int _foo, int bar, int more)
{
    foo_construct((foo*)t, _foo, bar);

    t->more = more;

    // Overrides printme
    t->base.printme = more_foo_print;
}

more_foo *new_more_foo(int _foo, int bar, int more)
{
    more_foo * new_mf = NEW(more_foo);

    more_foo_construct(new_mf, _foo, bar, more);

    return new_mf;
}

foo *new_foo(int _foo, int bar)
{
    foo *new_f = NEW(foo);

    foo_construct(new_f, _foo, bar);

    return new_f;
}

int main(void)
{
    foo * mf = (foo*)new_more_foo(1, 2, 3);
    foo * f = new_foo(7,8);

    mf->printme(mf);

    f->printme(f);

    return 0;
}
  • printme() is overridden when creating "more foo". (polymorphism)

  • more_foo includes foo as a base structure (inheritance) so when "foo" structure changes, "more foo" changes with it (example new values added).

  • more_foo can be cast as "foo".

pm101
  • 1,309
  • 10
  • 30