Is there a way to detect padding bits in a bitfield?

160 Views Asked by At

I extensively utilize bit-fields in my C++ embedded application, and I have encountered a problem. Below is an example showcasing my usage:

struct {
    uint8_t /* Reserved */ : 3;
    uint8_t foo : 3;
    uint8_t bar : 3;
} bits;

To ensure that I have correctly specified the number of bits in a bit-field, I employ the static_assert(...) mechanism:

// assertion fails due to 1 extra bit
static_assert(sizeof(bits) == sizeof(uint8_t));

This approach allows me to verify if I have surpassed the size limitation. However, I am currently unable to determine whether there are any padding bits in the struct. Is there a method to detect such situations?

struct {
    uint8_t /* Reserved */ : 3;
    uint8_t foo : 3;
    uint8_t bar : 1;
} bits;
// assertion passes, but I want it to fail, because one bit is padding
static_assert(sizeof(bits) == sizeof(uint8_t)); 
4

There are 4 best solutions below

1
Artyer On BEST ANSWER

Clang's -Wpadded does exactly what you want.

If you are not using clang, you can rename your unnamed bitfields into something like Reserved1 through ReservedN. Then you can use std::has_unique_object_representations which tells you if your class has padding or not:

struct {
    [[deprecated("This field should not be used")]] uint8_t Reserved1 : 2;
    uint8_t foo : 3;
    uint8_t bar : 3;
} bits;

// If `Reserved1` had a bitsize of 3, it would have padding and this would fail
static_assert(std::has_unique_object_representations_v<decltype(bits)>);
0
Jan Schultke On

It is not possible to get the exact amount of bits in a bit-field using sizeof, or any other way.

Problems with Padding in Bit-Fields

The sizeof operator gives you the amount of bytes that your struct occupies, not the amount of bits. In your second example, your bit-field consists of 7 bits, and assuming that bytes are 8 bits, the line:

static_assert(sizeof(bits) == sizeof(uint8_t));

is checking 1 == 1, since your struct is padded up to one byte.

Furthermore, mixing different member types can result in way more bits than one would expect:

struct {
    int a: 3;
    short b: 3;
} bits;

static_assert(sizeof(bits) == 1);

This gives us the error message:

<source>:5:28: error: static assertion failed
    5 | static_assert(sizeof(bits) == 1);
      |               ~~~~~~~~~~~~~^~~~
<source>:5:28: note: the comparison reduces to '(4 == 1)'

Even though we only use 6 bits for a and b, our bits occupies 4 bytes.

Ultimately, the amount of padding between bit-field members is implementation-defined, so you cannot rely on any specific amount of bits or bytes. You can only hope that the compiler laid the bit-field out the way you intended, and perhaps use static_assert with an upper bound for the bit-field size in bytes.

Note: with the non-standard [[gnu::packed]] attribute, the above example would have compiled.

For more information, see the cppreference article on bit-fields.

Using Macros as a Workaround

We cannot detect the exact amount of bits, but we can use a macro so that we simply know the amount:

// This list is being used to define the bit-field members
// and to compute the sum of bits.
#define LIST \
  E(char, , 3) \
  E(char, foo, 3) \
  E(char, bar, 1)

// Define what an entry in the list means.
#define E(type, name, size) type name: size;

struct {
// LIST expands to char: 3; char foo: 3; char bar: 1;
    LIST
} bits;

// Re-define what an entry means.
#undef E
#define E(type, name, size) +size

// LIST expands to +3 +3 +1
constexpr int bits_bits = LIST;

#undef E
#undef LIST

// This assertion passes, since we have 7 bits in total.
static_assert(bits_bits == 7);
// This assertion fails, but would have passed if we had 8 bits.
// We could also check if bits_bits is a multiple of 8 with
// bits_bits % 8 == 0.
static_assert(sizeof(bits) * 8 == bits_bits);
2
ams On

There's not a way to do it nicely.

But, you can deliberately add one more bit field and check that the size increases.

struct {
    uint8_t /* Reserved */ : 3;
    uint8_t foo : 3;
    uint8_t bar : 1;
#ifdef DEBUG
    uint8_t BOOM : 1;
#endif
} bits;
#ifdef DEBUG
static_assert(sizeof(bits) > sizeof(uint8_t)); // Will assert in debug mode because there are insufficient bits
#else
static_assert(sizeof(bits) == sizeof(uint8_t)); // Will NOT assert
#endif

Of course, this only helps you if the changed struct layout doesn't make the rest of the program explode.

2
Pepijn Kramer On

If you allow for a bit of extra declaration with a constexpr helper function then testing both too many or not enough bits (at 1 bit resolution) is possible at compile time like this:

#include <array>

// create a constexpr to see if all bit field constants
// add up to the size of the underlying type.
// if not then assert at compile time
template<typename underlying_t, std::size_t...sz>
static constexpr auto checked_bit_fields()
{
    // the numbers will be returned as an array
    std::array<std::size_t, sizeof...(sz)> bit_layout{ sz... };

    // (sz + ...) is fold expression calculating the sum of the bit fields
    // 8 * sizeof calculates the size in bits of the underlying type.
    static_assert((sz + ...) == 8ul * sizeof(underlying_t));
    return bit_layout;
}

struct bits_t
{
private:
    // the template parameters with number of bits per field.
    // if the sum of the bitfield sizes do not match the underlying type size
    // the code will not compile (test it out)
    static constexpr auto bits = checked_bit_fields<uint8_t, 2, 3, 3>();

    // regretably the standard does not allow for a structured binding here
    // so I could not do [unused,foo_bits,bar_bits] = checked_bit_fields<uint8_t,2,2,3>(); 
    // an we need to access the bit field sizes from the array

public:
    uint8_t : bits[0]; // == 2
    uint8_t foo : bits[1]; // == 3
    uint8_t bar : bits[2]; // == 3 
};


int main()
{
    bits_t bits;
    bits.foo = 3;
    return 0;
}