Bitstruct
I was looking at I^2^C code, and noted that there was an awful lot of manual bit manipulation. Unfortunately the native solution in c++, bitfields, are non-portable and for that reason is discouraged in code bases. For that reason, I have written a library called bitstructs, which aims to do what bitfields do but portably.
What are bitfields
Bitfields are a bit of an obscure feature in C++, aimed at embedded developers. It allows for struct members to have a length of less than one byte. For example, let’s say we have a device with I2C registers as such:
Bit 8 | Bit 7..3 | Bit 2.. 1 |
---|---|---|
Enable Flag | Length | Type enum |
You could manually set the bits to the settings you want. However, manual bit manipulation is tedious, opaque, and potentially error-prone. For that reason, bitfields were introduced to allow this to be done like anything else in the language. It is done as such:
struct Register{
uint8_t type: 2;
uint8_t length: 5;
uint8_t flag: 1;
};
Register r;
r.type = ENUM_TYPE;
r.length = 5;
r.flag = true;
This is neat, but suffers from a few issues. First off, it does not work with non-integer types, forcing you to use weakly-typed raw enums. It is also entirely implementation-dependent behavior. Although GCC and Clang, to my knowledge, don’t do anything weird, apparently certain implementations would pad the members, resulting in broken code. Since bitfields are entirely implementation defined, you couldn’t trust the implementation to do what you think it would.
Enter: Bitstructs
Of course, we can do better. My library, bitstructs, utilizes the power metaprogramming and reference proxies to fix this. See it in action:
enum struct Type{
A, B;
};
struct Register: bit::bitstruct<8>{
auto type() noexcept { return get<0, 2, Type>(); }
auto length() noexcept { return get<2, 5>(); }
auto flag() noexcept { return get<7, 1, bool>(); }
};
Register r;
r.type() = Type::A;
r.length() = 5;
r.flag() = true;
Benefits
- Strong typing
- Portable
- Header-only
How does it work
Bitstruct works by using proxy references, objects that overload assignment and casting.
template<size_t i>
size_t truncate(size_t s) { return (s << i) >> i; }
template<size_t index, size_T extent>
struct BitReference{
uint8_t& byte;
operator uint8_t()const{
return truncate<index + extent>(byte >> index);
}
// bit clearing omitted for clarity
BitReference& operator=(uint8_t b){
b = truncate<index + extent>(b);
byte |= b;
return *this;
}
};
Since all bitshifting is done by compile-time constants, it ends up being about as efficient as if you had done the bitshifting by hand. It’s also constexpr-friendly and, with NDEBUG enabled, does not throw anything.