0

I was testing the behavior of structs with bit fields in C++, but I encountered something confusing. My operating system is Windows 10 x64.

The code that I use is as follows:

struct BitFieldTest
{
    bool flag1 : 1;
    bool flag2 : 1;
    bool flag3 : 1;
    bool flag4 : 1;
    unsigned char counter1 : 4;
    unsigned int counter2 : 4;
};

struct NormalFieldTest
{
    bool flag1;
    bool flag2;
    bool flag3;
    bool flag4;
    unsigned char counter1;
    unsigned int counter2;
};
struct NormalFieldTest2
{
    bool flag1;
    bool flag2;
    bool flag3;
    bool flag4;
    unsigned char counter1;
};

int main()
{
    std::cout << "Size of bool: " << sizeof(bool) << std::endl;
    std::cout << "Size of unsigned char: " << sizeof(unsigned char) << std::endl;
    std::cout << "Size of unsigned int: " << sizeof(unsigned int) << std::endl;

    std::cout << "Size of struct with bit field: " << sizeof(BitFieldTest) << std::endl;
    std::cout << "Size of struct without bit field: " << sizeof(NormalFieldTest) << std::endl;
    std::cout << "Size of struct without bit field: " << sizeof(NormalFieldTest2) << std::endl;
    return 0;
}

The output is:

Size of bool: 1
Size of unsigned char: 1
Size of unsigned int: 4
Size of struct with bit field: 8
Size of struct without bit field: 12
Size of struct without bit field 2: 5

I don't understand why the size of the structures are like this. Can anyone explain or share some links about the topic?

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • [The Lost Art of Structure Packing](http://www.catb.org/esr/structure-packing/) – dbush May 13 '20 at 21:59
  • _"...The following properties of bit fields are __implementation-defined__..."_: _"Everything about the actual allocation details of bit fields within the class object"_ source https://en.cppreference.com/w/cpp/language/bit_field – Richard Critten May 13 '20 at 22:00
  • https://stackoverflow.com/questions/119123/why-isnt-sizeof-for-a-struct-equal-to-the-sum-of-sizeof-of-each-member – user3386109 May 13 '20 at 22:01
  • The struct is padded to have an address that is dividible by 4. This way accessing this data is faster than having an arbitrary address. – Gertjan Brouwer May 13 '20 at 22:19

1 Answers1

4

Bit fields only compact together so long as the type of the bit field is the same. So a bit field with:

struct Test
{
    char Test1 : 1;
    char Test2 : 1;
    char Test3 : 1;
    char Test4 : 1;
    short Test5 : 4;
}

will not be one byte long, but four.

This is because of two things - first, the bit fields do not cross a type boundary. The bit fields in the char cannot intermix with the bit fields in the short.

Second, the short must be placed on a two byte boundary from the start of the structure for alignment purposes. so the compiler changes this to be:

struct Test
{
    char Test1 : 1;
    char Test2 : 1;
    char Test3 : 1;
    char Test4 : 1;
    char BitPadding : 4;
    char AlignmentPadding;
    short Test5 : 4;
    short BitPadding2 : 12;
}

which brings the struct up to four bytes long.

Now we go through each struct in turn.

struct BitFieldTest
{
    bool flag1 : 1;
    bool flag2 : 1;
    bool flag3 : 1;
    bool flag4 : 1;
    unsigned char counter1 : 4;
    unsigned int counter2 : 4;
};

Here, flag1, flag2, flag3, and flag4 all have the same type, and condense down to a four bit value stored in a single byte. counter1 is of a different type, and has a natural alignment of one, so it moves to the next byte boundary. counter2 is also of a different type, and it has a natural alignment of four, so it moves to the next four byte boundary. If we then sum up the sizes of the individual sections and the intermediate padding, we have:

1 byte with four flags, 1 byte with a four bit counter, 2 bytes of alignment padding, and 4 bytes with a four bit counter.

This adds up to 8 bytes, exactly what the compiler reported.

The second structure has no bit fields, but shows off the alignment issue:

struct NormalFieldTest
{
    bool flag1;
    bool flag2;
    bool flag3;
    bool flag4;
    unsigned char counter1;
    unsigned int counter2;
};

Here, we have:

1 byte for boolean flag1, 1 byte for boolean flag2, 1 byte for boolean flag3, 1 byte for boolean flag4, 1 byte for counter1, 3 bytes for alignment padding, and 4 bytes for counter2.

This adds up to 12, which is, again, what the compiler reported.

The third structure is much the same as the second, but lacks the internal alignment padding:

struct NormalFieldTest2
{
    bool flag1;
    bool flag2;
    bool flag3;
    bool flag4;
    unsigned char counter1;
};

Here, we have:

1 byte for boolean flag1, 1 byte for boolean flag2, 1 byte for boolean flag3, 1 byte for boolean flag4, and 1 byte for counter1.

This adds up to 5 bytes, as the compiler has reported.

As an additional note, natural alignment of internal types can also leak out into the structure itself. Consider the following structure:

struct TrailingAlignment
{
    int Field1;
    short Field2;
}

This structure outwardly looks to be 6 bytes, but will compile to 8. This is because the structure might be used in an array, and if it was 6 bytes in length, every other item in the array would contain a misaligned Field1, causing major problems in systems that do not support misaligned access (like some flavors of ARM). To avoid this, the compiler inserts two bytes at the end of the structure to ensure the next instance of this structure in an array will be aligned on a four byte boundary, assuming the first structure was aligned properly.

This doesn't happen for NormalFieldTest2 because the natural alignment for bool and char is one byte, so it will always be aligned no matter where the structure is in memory.

Alex
  • 1,794
  • 9
  • 20