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.