struct foo {
char a : 4;
char b : 4;
};
Is a in the high-order 4 bits, or the lower 4 bits? Both choices are allowed, so it's up to the compiler and makes the code non-portable. x = foo.a
is simpler than x = (foo & FOO_MASK_A) >> FOO_SHIFT_A
and for assignments, the difference is even bigger: foo.a = x
is much better than foo = (foo &~ FOO_MASK_A) | ((a << FOO_SHIFT_A) & FOO_MASK_A)The more frequent perceived use for bit-fields (in the situation where they actually work) is to pack into a serialized data format, such that memory or a data stream can be accessed elsewhere. In that case, "the compiler can do whatever it wants with your data packing" is pretty useless, since your "elsewhere" might have a different compiler that does a totally different thing.
And as for the second part: anything that writes sizeof(struct foo) bytes of struct foo is inherently non-portable. If you portably want to (de)serialize something you want to write the thing explicitly, very often the compiler will optimize it to more direct implementation. (And well, this is only portable to platforms where CHAR_BITS == 8)
Ladies and gentlemen, this thought is why we now consider 8GB of ram to be a "weak device".
No, no no no no, 1000 times no. Every situation is a low ram situation. Every!
Hopes, prayers, and a single version of a single compiler being involved.
64-bit Linux distros and the BSDs follow the convention once set by the "C ABI for Itanium".
In that, bitfields are grouped in declaration order into container words of the same width as the bitfield's type (char, int, etc.). Bitfields don't span multiple container words, and container words don't overlap. On little-endian platforms, bitfields are packed LSB first, but on big-endian platforms they are packed MSB first within their container word. Alignment rules apply only to the container words.
If the instructions emitted and the instructions implemented both happen to match that, on every chip your code must run on, you got lucky.
If you want to produce same sequence of bytes regardless of underlying platform, then you have to do it by hand with uint8_t[] buffers and explicit shifts and masks. Casting pointer to struct to char* and writing it somewhere is inherently non-portable and this gas nothing to do with bitfields and nothing to do with things like __attributte__((packed)), although both of these things are useful when you want to do that and understand the (non-)portability implications.
You know where the bits are within a single word. But if you have a struct with multiple fields, it’s not safe to rely on the exact memory layout even if it doesn’t have any bitfields.
If you need to represent a very specific memory layout, it’s not just bitfields you need to avoid, it’s structs in general.
Conversely, if you don’t need to guarantee a specific layout, bitfields are fine to use, and could be a useful optimisation hint for the compiler.
Say I have a window manager, and I want to attach a bunch of boolean flags to each window object (isVisible, isMaximized, etc). I don’t need to serialize them to disk. It’s highly preferable that they should be efficiently bit-packed, but not strictly essential.
The conservative way to implement that would be bit-shifts and masking (either manually or via a macro). But implementing it with bitfields would be a lot easier and less error-prone, and would work just as well. What problems do you see with the bitfield approach?