Home > Software engineering >  C union type punning arrays
C union type punning arrays

Time:03-16

Given the following code, I have some questions related to type punning. I do not see any way that this isn't violating strict aliasing rules, but I cannot point to the specific violation. My best guess is that passing the union members into the function violates strict aliasing.

The following code is on Compiler Explorer.

#include <stdint.h>

union my_type
{
    uint8_t m8[8];
    uint16_t m16[4];
    uint32_t m32[2];
    uint64_t m64;
};

int func(uint16_t *x, uint32_t *y)
{
    return *y  = *x;
}

int main(int argc, char *argv[])
{
    union my_type mine = {.m64 = 1234567890};
    return func(mine.m16, mine.m32);
}

My observations:

  • Assuming the arguments to func do not alias each other, func does not violate strict aliasing.
  • In C, it is permissible to use a union for type punning.
  • Passing m16 and m32 into func must violate something.

My questions:

  • Is type punning with arrays like this valid?
  • What exactly am I violating by passing the pointers into func?
  • What other gotchas am I missing in this example?

CodePudding user response:

The rule violated is C 2018 6.5.16.1 3:

If the value being stored in an object is read from another object that overlaps in any way the storage of the first object, then the overlap shall be exact and the two objects shall have qualified or unqualified versions of a compatible type; otherwise, the behavior is undefined.

Specifically, in *y = *x, the value being stored in the object pointed to by y, mine.m16, is read from another object, mine.m32, that overlaps the storage of mine.m16, but the overlap is not exact and neither do the objects have compatible types, regardless of qualifiers.

Note that this rule is for simple assignment, as in E1 = E2, whereas the code has a component assignment, E1 = E2. However, the compound assignment E1 = E2 is defined in 6.5.16.2 3 to be equivalent to E1 = E1 E2 except that the lvalue E1 is evaluated only once.

Is type punning with arrays like this valid?

Yes, the C standard allows aliasing via union members; reading a member other than the last one stored will reinterpret the bytes in the new type. However, this does not absolve a program of conforming to other rules if its behavior is to be defined by the C standard, notably the rule quoted above.

What exactly am I violating by passing the pointers into func?

No rule is violated by passing the pointers. The assignment using the pointers violates a rule, as answered above.

What other gotchas am I missing in this example?

If we change func:

int func(uint16_t *x, uint32_t *y)
{
    *y  = 1;
    *x  = 1;
    return *y;
}

then the rule in 6.5.16.1 3 does not apply, as there is no assignment involving overlapping objects. And the aliasing rules in 6.5 7 are not violated, as *y is an object defined as the type used to access it, uint16_t, and *x is an object defined as the type used to access it, uint32_t. Yet, if this function is translated in isolation (without the union definition visible), the compiler is permitted to assume *x and *y do not overlap, so it may cache the value of *y produced by *y = 1; and return that cached value, in ignorance of the fact that *x = 1; changes *y. This is a defect in the C standard.

CodePudding user response:

Passing m16 and m32 into func must violate something.

func(uint16_t *x, uint32_t *y) is free to assume *x and *y do not overlap as x, y are different enough pointer types. Since the referenced data does overlap in OP's code, we have a problem.

The special issues about unions and aliasing do not apply here in the body of func() as the union-ness of the calling code is lost.

Alternate "safe" code could have been:

// Use volatile to prevent folding these 2 lines of code.
// The key is that even with optimized code, 
// the sum must be done before *y assignment.
volatile uint32_t sum = *y   *x;
*y = sum;

return (int) (*y);

What exactly am I violating by passing the pointers into func?

Passing pointers to overlapping data that the function func() is not obliged to account for.


Is type punning with arrays like this valid?

I do not see this as an array or union issue, just one of passing pointers to overlapping data that the function func() is not obliged to account for.

What other gotchas am I missing in this example?

Minor: int may be 16-bit, potentially causing implementation defined behavior in the conversion of uint32_t to int.


Consider the difference between

uint32_t fun1(uint32_t *a, uint32_t *b);
uint32_t fun2(uint32_t * restrict a, uint32_t * restrict b);

fun1() would have to consider an overlap potential. fun2() would not.

CodePudding user response:

My observations:

  • Assuming the arguments to func do not alias each other, func does not violate strict aliasing.

Not reliably true. The so-called strict-aliasing rule is expressed in terms of the lvalue used to access a given object, relative to that object's effective type. The two arguments to func() do not need to alias each other for execution of func() to produce a strict-aliasing violation. Example:

uint32_t x = 0, y = 1;
func((uint16_t *)&x, &y);
// func will violate strict aliasing when it dereferences its first parameter

Issues revolving around function parameters aliasing each other would be the realm of restrict-qualified pointers, which you are not using.


  • In C, it is permissible to use a union for type punning.

Yes, provided that the punning is performed via the union object. This is covered by C17 6.5/7, the aforementioned strict-aliasing rule:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

[...]

  • an aggregate or union type that includes one of the aforementioned types among its members

Note well that this isn't about the storage being accessed actually being inside a union object, but rather about the type of lvalue used to access it relative to the actual (effective) type of the object being accessed.


  • Passing m16 and m32 into func must violate something.

It does, though the language specification could be a lot clearer about that than it is. It does, however, say:

The value of at most one of the members can be stored in a union object at any time.

(C17 6.7.2.1/16)

In your particular example, neither mine.m16 nor mine.m32 has a value stored in it at the time of the call, but under any circumstance, at most one of them could have a value. When func then tries to read the values stored in those objects the results are not defined (because they don't actually have values stored in them).

That interpretation is supported by the inclusion in the spec of paragraph 6.5.2.3/6:

One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible.

No such special provision would be needed if it were generally ok to access random union members regardless of which one actually had a value stored.


My questions:

  • Is type punning with arrays like this valid?

Not like that, no. There are other, variations on array type-punning that are allowed by the spec.


  • What exactly am I violating by passing the pointers into func?

The call itself does not violate anything. It is legal to take the address of a union member, even one that does not currently have a value stored in it, and it is legal to pass the resulting pointer values to functions. But when called with those arguments, the function commits strict-aliasing violations when it attempts to dereference one or both pointers, as described above.


  • What other gotchas am I missing in this example?

Contrary to one of your other answers, the code presented does not run afoul of paragraph 6.5.16.1/3. The value being stored in *y is not read from overlapping object *x, but rather is the sum of that value with the original value of *y. That sum is computed, not read from an object, so 6.5.16.1/3 does not apply. But you may be missing that it would violate 6.5.16.1/3 if func() performed a simple assignment instead of a plussignment.

  • Related