Plain C polymorphism, type punning, and strict aliasing. How legal is this?
Asked Answered
W

2

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.

Whiteside answered 13/2, 2018 at 17:47 Comment(9)
union baz is invalid and doesn't compile. Please fix.Gypsy
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.Pincer
"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.Pincer
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).Pincer
@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.Whiteside
@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?Whiteside
"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.Pincer
@VladDinev C allows for many architectures, not just flat ones. I take All pointers to structure types ... 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.Pincer
@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.Therapeutic
B
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().

Birdwell answered 4/4, 2018 at 23:32 Comment(0)
R
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".

Roney answered 22/3, 2018 at 14:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.