C / STRUCTS, UNIONS AND ENUMS
Bitfields for compact flags and their portability traps
Pack small values into one storage unit with bitfields, predict their wrap-around and promotion behaviour, and know when to use shifts and masks instead.
What you will learn
- Size a bitfield from its value range and always spell out unsigned int or _Bool
- Predict the wrap when a value exceeds the width: 9 into a 3-bit field reads back 1
- Explain why bit order, straddling and unit size make bitfield layout non-portable
- Swap a bitfield for shift-and-mask macros on uint8_t when bit positions are fixed
Understanding Bitfields for compact flags and their portability traps
A bitfield declares a member by how many bits it needs: `unsigned int retries : 3;` asks for three bits, enough for 0 through 7. The compiler picks an addressable storage unit, packs consecutive fields into it, and rewrites every access into a shift and a mask on your behalf. The mental model that keeps you out of trouble is that a bitfield is a named accessor, not a layout directive: reading one loads the whole unit and extracts bits, and writing one loads the unit, patches bits, and stores the unit back.
The declared width fixes the field's value range, and that range is enforced by conversion rather than by an error. Storing 9 into a three-bit unsigned field keeps 9 modulo 8, so the field reads back 1 and nothing complains at run time. Signedness is the sharper edge: the standard leaves the signedness of a plain `int` bitfield to the implementation, and GCC, Clang and MSVC all choose signed, so `int ready : 1;` can only hold 0 and -1. Write `unsigned int` or `_Bool` explicitly, and remember that a narrow field promotes to `int` in expressions, so `%d` is the matching specifier (a full-width `unsigned int : 32` field stays unsigned instead).
What the standard refuses to pin down is exactly what people reach for bitfields to do. The order in which fields are allocated inside a unit (from the low bit up or the high bit down), whether a field may straddle two units, and the size and alignment of the unit itself are all implementation-defined, so one declaration can describe different bits under a different compiler, ABI or endianness. A bitfield also has no address, so `&p.urgent` and `sizeof p.urgent` do not compile and there is no array of bitfields. Use bitfields to shrink your own in-memory data; use explicit shifts and masks over `uint8_t` or `uint32_t` whenever the bit positions are dictated by hardware, a file format or a wire protocol.
Compactness is also a trade, not a free win. Each access costs an extra shift and mask, and a write costs a read-modify-write of the whole unit, so in a hot loop three separate `unsigned char` flags can beat three one-bit fields.
<stdio.h>
/* Four small values inside a single storage unit. */
struct Packet {
unsigned int version : 4; /* 0..15 */
unsigned int urgent : 1; /* 0..1 */
unsigned int retries : 3; /* 0..7 */
unsigned int length : 12; /* 0..4095 */
};
int main(void)
{
struct Packet p = {0};
int r = 9, n = 5000; /* both too wide for their fields */
p.version = 2;
p.urgent = 1;
p.retries = 3;
p.length = 1500;
/* Each field promotes to int when read, so %d matches. */
printf("version=%d urgent=%d retries=%d length=%d\n",
p.version, p.urgent, p.retries, p.length);
p.retries = r; /* kept modulo 8 */
p.length = n; /* kept modulo 4096 */
printf("retries=%d length=%d\n", p.retries, p.length);
printf("sizeof(struct Packet)=%zu\n", sizeof p);
return 0;
}
A bitfield is a compiler-generated shift-and-mask accessor for compactness inside your own program, never a promise about which bits end up where.
Worked examples
A one-bit int flag that is never 1
Shows why the signedness of a plain int bitfield must never be left to the compiler.
<stdio.h>
struct Sloppy { int flag : 1; }; /* signedness is implementation-defined */
struct Safe { unsigned int flag : 1; };
int main(void)
{
struct Sloppy s = {0};
struct Safe t = {0};
int one = 1;
s.flag = one;
t.flag = one;
printf("s.flag = %d\n", s.flag);
printf("t.flag = %d\n", t.flag);
printf("s.flag == 1 ? %s\n", s.flag == 1 ? "yes" : "no");
return 0;
}
Example explained
Line 1`int flag : 1;` is signed on GCC, Clang and MSVC, so its only representable values are 0 and -1.
Line 2`s.flag = one;` converts 1 to a type that cannot hold it; the bit pattern 1 is kept and re-read as the sign bit, giving -1.
Line 3`t.flag` holds the identical bit, but because the field is `unsigned` it reads back as 1.
Line 4The comparison `s.flag == 1` therefore never succeeds, which is how this bug usually shows up: a flag that is set but never seen as set.
The zero-width field, the only placement control you get
Demonstrates that widths alone do not determine struct size, because an unnamed :0 field closes the current storage unit.
<stdio.h>
struct Packed {
unsigned int lo : 4;
unsigned int hi : 4;
};
struct Split {
unsigned int lo : 4;
unsigned int : 0; /* pack nothing more into this unit */
unsigned int hi : 4;
};
int main(void)
{
printf("sizeof(struct Packed) = %zu\n", sizeof(struct Packed));
printf("sizeof(struct Split) = %zu\n", sizeof(struct Split));
return 0;
}
Example explained
Line 1Both fields of `Packed` are 4 bits and fit in one unit, so the whole struct costs 4 bytes.
Line 2`unsigned int : 0;` has no name and stores nothing; it only says that no further field may share the previous unit.
Line 3`hi` is pushed into a second unit, so `Split` doubles to 8 bytes even though it declares the same 8 bits of data.
Line 4Unnamed and zero-width fields are the only placement control the standard defines; every other aspect of where a field lands is the compiler's choice.
Shift and mask when the bit positions are not yours to choose
Builds the same packed byte portably, with each field's position fixed by your own source instead of by the compiler.
<stdio.h>
<stdint.h>
MODE_SHIFT
MODE_MASK/* bits 0..2 */
PRIO_SHIFT
PRIO_MASK/* bits 3..7 */
static uint8_t pack(unsigned mode, unsigned prio)
{
return (uint8_t)(((mode & MODE_MASK) << MODE_SHIFT) |
((prio & PRIO_MASK) << PRIO_SHIFT));
}
int main(void)
{
uint8_t byte = pack(5, 9);
printf("byte = 0x%02X\n", (unsigned)byte);
printf("mode = %u\n", (byte >> MODE_SHIFT) & MODE_MASK);
printf("prio = %u\n", (byte >> PRIO_SHIFT) & PRIO_MASK);
return 0;
}
Example explained
Line 1`prio & PRIO_MASK` discards out-of-range bits before the shift, so a bad argument cannot bleed into the neighbouring field.
Line 2`<< PRIO_SHIFT` puts the field at bit 3, and that position is stated in your source rather than decided by the compiler.
Line 3Unpacking is the exact inverse: shift down first, then mask away the higher fields.
Line 4A bitfield struct cannot promise the value 0x4D; this code produces that same byte on every conforming implementation.
Important notes
Every size shown here is what GCC and Clang produce on x86-64 with a 32-bit int; the standard fixes neither the unit size nor the field placement, so verify them on your own build.
`memcmp` on two bitfield structs also compares indeterminate padding bits and can report a difference for equal fields — compare field by field.
Common mistakes
Declaring `int ready : 1;` and then testing `if (f.ready == 1)`: the field is signed on mainstream compilers, so the stored 1 reads back as -1 and the branch is dead code.
Overlaying a bitfield struct on a network header or a hardware register: allocation order and straddling are compiler-defined, so code that decodes correctly under GCC on x86-64 silently reads the wrong bits under another compiler, ABI or byte order.
Trying to pass a flag by pointer with `&f.dirty`, or calling `sizeof f.dirty`: both are compile errors, because a bitfield has no address of its own — you must pass the whole struct or copy the value out.
Try it yourself
Change, predict, then run
Declare `struct Reg { unsigned int mode : 2; unsigned int speed : 3; unsigned int enabled : 1; };`, set mode to 3, speed to 6 and enabled to 1, and print all three fields. Then write `uint8_t pack(unsigned mode, unsigned speed, unsigned enabled)` that places the same values at bits 0-1, 2-4 and 5, print it in hex, and note which version told you exactly where each bit went.
Open the C workspaceCheck your understanding
Two threads without any lock each set a different flag in `struct { unsigned int a : 1; unsigned int b : 1; };`, and one of the updates is lost. Why?
- Bitfield writes happen one bit at a time, and a single-bit store is not atomic.
- The compiler may reorder the two members, so both threads end up writing the same field.
- Writing one field is a read-modify-write of the whole storage unit, so a thread can store back a stale copy of its neighbour's bit.
- The two flags share a cache line, and a cache line can only be written by one core at a time.
Show answer
The hardware cannot address a single bit, so `a = 1` compiles to load the unit, set the bit, store the unit; if the other thread's store to `b` lands between that load and store, it is overwritten. This is why C11 treats adjacent bitfields in one unit as a single memory location, and why a zero-width separator or separate objects fix it. Option 3 describes false sharing, which costs performance but never loses data, because the hardware keeps concurrent writes to distinct bytes coherent.