Using Unions for "type punning" is fine in C, and fine in gcc's C++ as well (as a gcc [g++] extension). But, "type punning" via unions has hardware architecture endianness considerations.
This is called "type punning", and it is not directly portable due to endianness considerations. However, otherwise, doing it is just fine. The C standards have NOT been great about making it clear this is just fine, but apparently it is. Read these answers and sources:
- Is type-punning through a union unspecified in C99, and has it become specified in C11?
- Unions and type-punning
- https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Type%2Dpunning - type punning is allowed in gcc C and C++
Additionally, the C18 draft, N2176 ISO/IEC 9899:2017 states in section "6.5.2.3 Structure and union members", the following in footnote 97:
- 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.
See it in this screenshot here:
So, having
typedef union my_union_u
{
uint32_t value;
/// A byte array large enough to hold the largest of any value in the union.
uint8_t bytes[sizeof(uint32_t)];
} my_union_t;
as a means of translating value
into bytes
is just fine in C. In C++ it works as a GNU gcc extension (but not as part of the C++ standard). See @Christoph's explanation in his answer here:
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.
Download the code: you can download and run all the code below from my eRCaGuy_hello_world repo here: "type_punning.c". gcc build and run commands for both C and C++ are found in the comments at the very top of the file.
So, you can do something like this to read the individual bytes out of the uint32_t value
:
TECHNIQUE 1: union-based type punning:
my_union_t u;
// write to uint32_t value
u.value = 1234;
// read individual bytes from uint32_t value
printf("1st byte = 0x%02X
", (u.bytes)[0]);
printf("2nd byte = 0x%02X
", (u.bytes)[1]);
printf("3rd byte = 0x%02X
", (u.bytes)[2]);
printf("4th byte = 0x%02X
", (u.bytes)[3]);
Sample output:
- On a little-endian architecture:
1st byte = 0xD2
2nd byte = 0x04
3rd byte = 0x00
4th byte = 0x00
- On a big-endian architecture:
1st byte = 0x00
2nd byte = 0x00
3rd byte = 0x04
4th byte = 0xD2
You can use raw pointers to obtain bytes from variables too, but this technique also has hardware architecture endianness issues.
This could be done withOUT a union if you wanted by using raw pointers too, like this:
TECHNIQUE 2: reading through raw pointers:
uint32_t value = 1234;
uint8_t *bytes = (uint8_t *)&value;
// read individual bytes from uint32_t value
printf("1st byte = 0x%02X
", bytes[0]);
printf("2nd byte = 0x%02X
", bytes[1]);
printf("3rd byte = 0x%02X
", bytes[2]);
printf("4th byte = 0x%02X
", bytes[3]);
Sample output:
- On a little-endian architecture:
1st byte = 0xD2
2nd byte = 0x04
3rd byte = 0x00
4th byte = 0x00
- On a big-endian architecture:
1st byte = 0x00
2nd byte = 0x00
3rd byte = 0x04
4th byte = 0xD2
You can use bitmasks and bit-shifting to avoid hardware architecture endianness portability issues.
To avoid endianness issues which exist with both the union type punning and raw pointer approaches above, you can use something like the following instead. This avoids endianness differences between hardware architectures:
TECHNIQUE 3.1: use bit-masks and bit shifting:
uint32_t value = 1234;
uint8_t byte0 = (value >> 0) & 0xff;
uint8_t byte1 = (value >> 8) & 0xff;
uint8_t byte2 = (value >> 16) & 0xff;
uint8_t byte3 = (value >> 24) & 0xff;
printf("1st byte = 0x%02X
", byte0);
printf("2nd byte = 0x%02X
", byte1);
printf("3rd byte = 0x%02X
", byte2);
printf("4th byte = 0x%02X
", byte3);
Sample output (the above technique is endianness-independent!):
- On a all architectures: both big-endian AND little-endian:
1st byte = 0xD2
2nd byte = 0x04
3rd byte = 0x00
4th byte = 0x00
OR:
TECHNIQUE 3.2: use a convenience macro to do bit-masks and bit shifting:
#define BYTE(value, byte_num) ((uint8_t)(((value) >> (8*(byte_num))) & 0xff))
uint32_t value = 1234;
uint8_t byte0 = BYTE(value, 0);
uint8_t byte1 = BYTE(value, 1);
uint8_t byte2 = BYTE(value, 2);
uint8_t byte3 = BYTE(value, 3);
// OR
uint8_t bytes[] = {
BYTE(value, 0),
BYTE(value, 1),
BYTE(value, 2),
BYTE(value, 3),
};
printf("1st byte = 0x%02X
", byte0);
printf("2nd byte = 0x%02X
", byte1);
printf("3rd byte = 0x%02X
", byte2);
printf("4th byte = 0x%02X
", byte3);
printf("---------------
");
printf("1st byte = 0x%02X
", bytes[0]);
printf("2nd byte = 0x%02X
", bytes[1]);
printf("3rd byte = 0x%02X
", bytes[2]);
printf("4th byte = 0x%02X
", bytes[3]);
Sample output (the above technique is endianness-independent!):
- On a all architectures: both big-endian AND little-endian:
1st byte = 0xD2
2nd byte = 0x04
3rd byte = 0x00
4th byte = 0x00
---------------
1st byte = 0xD2
2nd byte = 0x04
3rd byte = 0x00
4th byte = 0x00
Otherwise, (my_pixel.RGBA)[0]
, or (u.bytes)[0]
, might be equal to byte0
(as I've defined it above) if the architecture is Little-endian, or equal to byte3
if the architecture is Big-endian.
See this endianness graphic below: https://en.wikipedia.org/wiki/Endianness. Notice that In big-endian, the most-significant-byte of any given variable is stored first (meaning: in lower addresses) in memory, but in little-endian it is the least-significant-byte that is stored first (in lower addresses) in memory. Also remember that endianness describes byte order, NOT bit order (bit order within a byte has nothing to do with endianness), and that each byte is 2 hex characters, or "nibbles", where a nibble is 4 bits.
According to the Wikipedia article above, networking protocols usually use big-endian byte order, whereas most processors (x86, most ARM, etc.), usually are little-endian (emphasis added):
Big-endianness is the dominant ordering in networking protocols, such as in the internet protocol suite, where it is referred to as network order, transmitting the most significant byte first. Conversely, little-endianness is the dominant ordering for processor architectures (x86, most ARM implementations, base RISC-V implementations) and their associated memory.
More notes regarding whether or not "type punning" is supported by the standard
According to Wikipedia's "Type punning" article, writing to union member value
but reading from RGBA[4]
is "unspecified behavior". However, @Eric Postpischil points out in his comment below this answer that Wikipedia is wrong. The other references at the top of this answer also don't align with the Wikipedia answer as it is written now.
Eric Postpischil's comment, which I now understand and agree with, states (emphasis added):
The quoted text, about bytes corresponding to union members other than the last one stored, does not apply to this situation. It applies to a case where, for example, a two-byte short
member is written and a four-byte int
member is read. The extra two bytes are unspecified. This gives a C implementation license to implement the store to the short
as a two-byte store (leaving the remaining bytes of the union unchanged) or a four-byte store (perhaps because it is efficient for the processor). In the case at hand, we have a four-byte uint32_t
member and a four-byte uint8_t [4]
member.
Wikipedia claims (as of 22 Apr. 2021):
For union:
union {
unsigned int ui;
float d