Pointer aliasing between struct and first member of struct

Viewed 456

Pointer aliasing in C is normally undefined behavior (because of strict aliasing), but C11 standard seems allow aliasing a pointer to struct and a pointer to the first member of the struct

C11 6.7.2.1 (15)...A pointer to a structure object... points to its initial member... and vice versa...

So does the following code contain undefined behavior?

struct Foo {
    int x;
    int y;
};

// does foe return always 100?
int foe() {
    struct Foo foo = { .x = 10, .y = 20 }, *pfoo = &foo;
    int *px = (int*)pfoo; *px = 100;
    return pfoo->x;
}

2 Answers

This code is correct. All versions of Standard C and C++ allow this , although the wording varies.

There's no strict aliasing issue because you access an object of type int via an lvalue of type int. The strict aliasing rule may apply when the lvalue doing the access has a different type to the object stored at the memory location .

The text you quoted covers that the pointer cast actually points to the int object.

The way the Standard is written, an lvalue of a structure or union type may be used to access an object of member type, but there is no provision that would allow an arbitrary lvalue of struct or union's member type to access an object of the struct or union type. Because it would of course be absurd to say that code couldn't use a struct or union member lvalue (which would of course have that member's type) to access a struct or union, all compilers have supported some common access patterns. Because compilers allow such accesses under different circumstances, however, the Standard treats all support for such accesses as a Quality of Implementation issue rather than trying to specify exactly when such support is required.

The approach most consistent with the Standard's wording, and which would allow the most useful optimizations, while also supporting most code that would need to perform type punning or other techniques, would be to say that for purposes of N1570 6.5p7, a pointer which is visibly derived from a pointer or lvalue of a given type may be used within the context of such derivation to access things that would (for purposes of 6.5p7) be accessible using an lvalue of that type. Under such an approach, given a piece of code like:

struct foo { int index,len; int *dat; };
void test1(struct foo *p)
{
  int *pp = &foo->len;
  *pp = 4;
}
void test2(struct foo *p, int dat)
{
  if (p->index < p->len)
  {
    p->dat[p->index] = dat;
    p->index++;
  }
}

should recognize that within test1, an access to *pp may access the struct foo object *p, since pp is visibly formed from p. On the other hand, the compiler would not be required to accommodate within test2 the possibility that an object of type struct foo, nor members thereof such as p->index, might be modified through the pointer p->dat, because nothing within test2 would cause the address of a struct foo or any portion thereof to be stored in p->dat.

Clang and gcc, however, instead opt for a different approach, behaving as though 6.5p7 allows struct members to be accessed via arbitrary pointers of their types, but union members can't be accessed via pointers at all, excluding the pointer arithmetic implied by bracketed array expressions. Given union { uint16_t h[4]; uint32_t w[2];} u; clang and gcc will recognize that an access to u.h[i] might interact with u.w[j], but will not recognize that *(u.h+i) might interact with *(u.w+j) even though the Standard defines the meaning of the former expressions with brackets as being equivalent to the latter forms.

Given that compilers consistently handle all of these constructs usefully when type-based aliasing is disabled. The Standard, however, doesn't impose any requirements even in many common cases, and clang and gcc make no promises about behavior of constructs not mandated by the Standard, even if all versions to date have handled such constructs usefully. Thus, I would not recommend relying upon clang or gcc to usefully process anything that involves accessing storage as different types at different times except when using -fno-strict-aliasing, and their wackiness isn't an issue when using that option, so I'd recommend simply using that option unless or until clang and gcc adopt a better defined abstraction.

Related