Skip to main content

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 8Bit 7..3Bit 2.. 1
Enable FlagLengthType 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.