Preventing bit flags from accidentally breaking ABI

Greetings! I hope this is a right place to ask a C++ related question.

I have been following this guide: Policies/Binary Compatibility Issues With C++ - KDE Community Wiki to preserve binary stability of my classes.

I wanted the compiler to assist me to make sure that ABI is not accidentally broken when adding new bit field members. I would like you to take a look at my solution and tell me if I am doing it correctly.

The problem: suppose we have a class like this:

class Test
{
public:
    bool flag1             : 1;
    bool flag2             : 1;
    bool flag3             : 1;
    uint8_t something_else : 1;
    
    uint8_t  reserved_01 : 4;
    uint8_t  reserved_02 : 8;
    uint16_t reserved_03 : 15;
    
    Test ();
    
    // Other members, PIMPL, etc.
};

And in the new version we need to add another bit field. Here is one of the possible mistakes that breaks ABI that could be made:

class Test
{
public:
    bool flag1             : 1;
    bool flag2             : 1;
    bool flag3             : 1;
    uint8_t something_else : 1;
    
    uint8_t MISTAKE : 4; // ABI broken because we forgot to remove the reserved_01 member, class size changes.
    
    uint8_t  reserved_01 : 4;
    uint8_t  reserved_02 : 8;
    uint16_t reserved_03 : 15;
    
    Test ();
    
    // Other members, PIMPL, etc.
};

This is what I came up with to prevent that from happening:

struct TestBase
{
    bool flag1             : 1;
    bool flag2             : 1;
    bool flag3             : 1;
    uint8_t something_else : 1;
    
    uint8_t  reserved_01 : 4;
    uint8_t  reserved_02 : 8;
    uint16_t reserved_03 : 15;
    
    TestBase () { static_assert (sizeof (TestBase) == 4, "TestBase's size is not 32 bits"); }
};

class Test : public TestBase
{
    // Other members, PIMPL, etc.
};

Now the compiler raises an error if ABI is accidentally broken, preventing the disaster. Is this the correct solution? I wanted to do that without subclassing, but could not do it reliably due to compiler taking alignment into its own hands

ABI can be broken by many things, not just change in class size. Linux C++ ABI has a spec, so there probably some tools that can check this as part of CI process.

I found one by quick googling, though is doesn’t seem to be actively developed: GitHub - lvc/abi-compliance-checker: A tool for checking backward API/ABI compatibility of a C/C++ library

That looks like a very useful tool, thanks for pointing it out!

I do realise I might be walking on thin ice here, and even considered just using std::bitset instead, but in this particular case bit fields provide the tersest code possible (no need for getters/setters, enum declarations, etc.), so I hope if I can get it done right there will be no trouble.

This quote from the article gave me a bit if confidence I could pull it off:

So I assumed that as long as I do not remove, do not reorder and do not change the types of the non-reserved bit fields I can add new bit field members without breaking things down. When adding new bit fields I will remove an equal number of bits from the reserved members, paying attention to the boundaries of the underlying types.

For example:

struct TestBase // New version
{
    bool flag1             : 1;
    bool flag2             : 1;
    bool flag3             : 1;
    uint8_t something_else : 1;

    uint8_t NEW_MEMBER : 2;
    
    uint8_t  reserved_01 : 2; // Was 4, removed two bits because of adding a new member
    uint8_t  reserved_02 : 8;
    uint16_t reserved_03 : 15;
    
    TestBase () { static_assert (sizeof (TestBase) == 4, "TestBase's size is not 32 bits"); }
};

Is it safe or am I risking too much for a slightly more convenient syntax?