Unions and Type-Punning

Unions and type-punning

To re-iterate, type-punning through unions is perfectly fine in C (but not in C++). In contrast, using pointer casts to do so violates C99 strict aliasing and is problematic because different types may have different alignment requirements and you could raise a SIGBUS if you do it wrong. With unions, this is never a problem.

The relevant quotes from the C standards are:

C89 section 3.3.2.3 §5:

if a member of a union object is accessed after a value has been stored in a different member of the object, the behavior is implementation-defined

C11 section 6.5.2.3 §3:

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member

with the following footnote 95:

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

This should be perfectly clear.


James is confused because C11 section 6.7.2.1 §16 reads

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

This seems contradictory, but it is not: In contrast to C++, in C, there is no concept of active member and it's perfectly fine to access the single stored value through an expression of an incompatible type.

See also C11 annex J.1 §1:

The values of bytes that correspond to union members other than the one last stored into [are unspecified].

In C99, this used to read

The value of a union member other than the last one stored into [is unspecified]

This was incorrect. As the annex isn't normative, it did not rate its own TC and had to wait until the next standard revision to get fixed.


GNU extensions to standard C++ (and to C90) do explicitly allow type-punning with unions. Other compilers that don't support GNU extensions may also support union type-punning, but it's not part of the base language standard.

Unions, aliasing and type-punning in practice: what works and what does not?

Aliasing can be taken literally for what it means: it is when two different expressions refer to the same object. Type-punning is to "pun" a type, ie to use a object of some type as a different type.

Formally, type-punning is undefined behaviour with only a few exceptions. It happens commonly when you fiddle with bits carelessly

int mantissa(float f)
{
return (int&)f & 0x7FFFFF; // Accessing a float as if it's an int
}

The exceptions are (simplified)

  • Accessing integers as their unsigned/signed counterparts
  • Accessing anything as a char, unsigned char or std::byte

This is known as the strict-aliasing rule: the compiler can safely assume two expressions of different types never refer to the same object (except for the exceptions above) because they would otherwise have undefined behaviour. This facilitates optimizations such as

void transform(float* dst, const int* src, int n)
{
for(int i = 0; i < n; i++)
dst[i] = src[i]; // Can be unrolled and use vector instructions
// If dst and src alias the results would be wrong
}

What gcc says is it relaxes the rules a bit, and allows type-punning through unions even though the standard doesn't require it to

union {
int64_t num;
struct {
int32_t hi, lo;
} parts;
} u = {42};
u.parts.hi = 420;

This is the type-pun gcc guarantees will work. Other cases may appear to work but may one day silently be broken.

what really mean by Type Punning is an application of union?

Type-puning means reinterpreting the underlying bytes of one type as bytes of another. Unions can be (mis)used for this because all members share the same memory location.

Reading from non-active union member is undefined behaviour in C++.
It is allowed in C and GNU C++.

The correct C++ approach for trivially copyable types is to use std::memcpy:

#include <cstring>

int main()
{
std::uint32_t x = 12;

float y;
// This is OK, but the value of `y` is implementation-defined.
std::memcpy(&y,&x,sizeof(x));
// THIS IS UNDEFINED BEHAVIOUR.
y = *reinterpret_cast<float*>(&x);

static_assert(sizeof(x)==sizeof(y),"Sanity check");
}

Note that reinterpret_cast<T*>(address) is not enough because it requires T object to exist at address otherwise you are breaking the strict aliasing rule (with some exceptions). There is also no need to worry about performance degradation of using std::memcpy, with optimizations no bytes will be copied unless needed. The call will just serve as a hint to the compiler that you really meant to do that.

Although, the example above does not exhibit undefined behaviour, the value of y still depends on precise bit representation of integers and floats which are mostly implementation-defined. In C++20 the integers are required to use 2-complement but floats are not required to adhere to e.g. IEEE 754.

Why is type punning considered UB?

Ultimately the why is "because the language specification says so". You don't get to argue with that. If that's the way the language is, it's the way it is.

If you want to know the motivation for making it that way, it's that the original C language lacked any way of expressing that two lvalues can't alias one another (and the modern language's restrict keyword is still barely understood by most users of the language). Being unable to assume two lvalues can't alias means the compiler can't reorder loads and stores, and must actually perform loads and stores from/to memory for every access to an object, rather than keeping values in registers, unless it knows the object's address has never been taken.

C's type-based aliasing rules somewhat mitigate this situation, by letting the compiler assume lvalues with different types don't alias.

Note also that in your example, there's not only type-punning but misalignment. The unsigned char array has no inherent alignment, so accessing a uint64_t at that address would be an alignment error (UB for another reason) independent of any aliasing rules.

C union type punning arrays

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.

Type punning and Unions in C

Yes, storing one member of union and reading another is type punning (assuming the types are sufficiently different). Moreover, this is the only kind of universal (any type to any type) type punning that is officially supported by C language. It is supported in a sense that the language promises that in this case the type punning will actually occur, i.e. that a physical attempt to read an object of one type as an object of another type will take place. Among other things it means that writing one member of the union and reading another member implies a data dependency between the write and the read. This, however, still leaves you with the burden of ensuring that the type punning does not produce a trap representation.

When you use casted pointers for type punning (what is usually understood as "classic" type punning), the language explicitly states that in general case the behavior is undefined (aside from reinterpreting object's value as an array of chars and other restricted cases). Compilers like GCC implement so called "strict aliasing semantics", which basically means that the pointer-based type punning might not work as you expect it to work. For example, the compiler might (and will) ignore the data dependency between type-punned reads and writes and rearrange them arbitrarily, thus completely ruining your intent. This

int i;
float f;

i = 5;
f = *(float *) &i;

can be easily rearranged into actual

f = *(float *) &i;
i = 5;

specifically because a strict-aliased compiler deliberately ignores the possibility of data dependency between the write and the read in the example.

In a modern C compiler, when you really need to perform physical reinterpretation of one objects value as value of another type, you are restricted to either memcpy-ing bytes from one object to another or to union-based type punning. There are no other ways. Casting pointers is no longer a viable option.

Type Punning with Unions and Heap

It is non-conforming to cast to a union type, as your code does:

    printer(((union testing)aQuickTest).f);

For that reason, your code does have undefined behavior as far as the Standard is concerned.

More directly to the point, however, no, your approach of putting pointers into a union does not avoid strict aliasing violations with respect to the pointed-to types, even without the casting issue. In your case, the effect is that where your union testing is in scope, implementations cannot assume that objects of type struct test1 ** and struct test2 ** do not alias each other. That does not prevent undefined defined behavior resulting from accessing an object with effective type struct test1 through an lvalue of type struct test2.

Type punning in C using union

Yes your reasonning is correct. This is not undefined behavior, but unspecified behavior according to C11, section 6.2.6.1/7 :

When a value is stored in a member of an object of union type, the
bytes of the object representation that do not correspond to that
member but do correspond to other members take unspecified values.

Section 3.19.3 clarifies what this means:

unspecified value: valid value of the relevant type where this International Standard imposes no requirements on which value is
chosen in any instance

This is reminded in Annex J: Portability Issues

J.1 Unspecified behavior

1 The following are unspecified:

— ...

— The value of padding bytes when storing values in structures or
unions (6.2.6.1).

— The values of bytes that correspond to union
members other than the one last stored into (6.2.6.1).

— ...

Nothing about accessing union members is specified in J2 which is about undefined behavior

This being said, portability issues can be severe as section 6.2.6.1/6 reminds:

The value of a structure or union object is never a trap
representation, even though the value of a member of the structure or
union object may be a trap representation.

A trap representation is an "object representation that need not represent a value of the object type" (definition), being understood that "fetching a trap representation might perform a trap but is not required to" (footnote). So accessing the inactive value may lead to the interruption of the programme, but if it doesn't, it's just that there is no guarantee about it.

Is there any way to use type-punning in the c-union-style with c++?

Yes, there is a way. Copy the bytes onto an object of type that you want to read the bytes as. There is a standard function for this: memcpy.

Example:

struct Color{
unsigned char r;
unsigned char g;
unsigned char b;
unsigned char a;
};

static_assert(sizeof(Color) == sizeof(std::int32_t));
std::int32_t rgba = some_value;
Color c;
std::memcpy(&c, &rgba, sizeof c);


Related Topics



Leave a reply



Submit