How is the size of a C++ class determined?

Solution 1:

For POD (plain old data), the rules are typically:

  • Each member in the structure has some size s and some alignment requirement a.
  • The compiler starts with a size S set to zero and an alignment requirement A set to one (byte).
  • The compiler processes each member in the structure in order:
  1. Consider the member’s alignment requirement a. If S is not currently a multiple of a, then add just enough bytes S so that it is a multiple of a. This determines where the member will go; it will go at offset S from the beginning of the structure (for the current value of S).
  2. Set A to the least common multiple1 of A and a.
  3. Add s to S, to set aside space for the member.
  • When the above process is done for each member, consider the structure’s alignment requirement A. If S is not currently a multiple of A, then add just enough to S so that it is a multiple of A.

The size of the structure is the value of S when the above is done.

Additionally:

  • If any member is an array, its size is the number of elements multiplied by the size of each element, and its alignment requirement is the alignment requirement of an element.
  • If any member is a structure, its size and alignment requirement are calculated as above.
  • If any member is a union, its size is the size of its largest member plus just enough to make it a multiple of the least common multiple1 of the alignments of all the members.

Consider your TestClass3:

  • S starts at 0 and A starts at 1.
  • char buf[8] requires 8 bytes and alignment 1, so S is increased by 8 to 8, and A remains 1.
  • __m128i vect requires 16 bytes and alignment 16. First, S must be increased to 16 to give the right alignment. Then A must be increased to 16. Then S must be increased by 16 to make space for vect, so S is now 32.
  • char buf2[8] requires 8 bytes and alignment 1, so S is increased by 8 to 24, and A remains 16.
  • At the end, S is 24, which is not a multiple of A (16), so S must be increased by 8 to 32.

So the size of TestClass3 is 32 bytes.

For elementary types (int, double, et cetera), the alignment requirements are implementation-defined and are usually largely determined by the hardware. On many processors, it is faster to load and store data when it has a certain alignment (usually when its address in memory is a multiple of its size). Beyond this, the rules above follow largely from logic; they put each member where it must be to satisfy alignment requirements without using more space than necessary.

Footnote

1 I have worded this for a general case as using the least common multiple of alignment requirements. However, since alignment requirements are always powers of two, the least common multiple of any set of alignment requirements is the largest of them.

Solution 2:

It is entirely up to the compiler how the size of a class is determined. A compiler will usually compile to match a certain application binary interface, which is platform dependent.

The behaviour you've observed, however, is pretty typical. The compiler is trying to align the members so that they each begin at a multiple of their size. In the case of TestClass3, the one of the members is of type __m128i and sizeof(__m128i) == 16. So it will try to align that member to begin at a byte that is a multiple of 16. The first member is of type char[8] so takes up 8 bytes. If the compiler were to place the _m128i object directly after this first member, it would start at position 8, which is not a multiple of 16:

0               8               16              24              32              48
┌───────────────┬───────────────────────────────┬───────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│    char[8]    │            __m128i            │    char[8]    │           
└───────────────┴───────────────────────────────┴───────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄

So instead it prefers to do this:

0               8               16              24              32              48
┌───────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┬───────────────────────────────┬───────────────┐┄┄┄
│    char[8]    │               │           __m128i             │    char[8]    │
└───────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┴───────────────────────────────┴───────────────┘┄┄┄

This gives it a size of 48 bytes.

When you reorder the members to get TestClass4 the layout becomes:

0               8               16              24              32              48
┌───────────────┬───────────────┬───────────────────────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│    char[8]    │    char[8]    │           __m128i             │        
└───────────────┴───────────────┴───────────────────────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄

Now everything is correctly aligned - the arrays are at offsets that are multiple of 1 (the size of their elements) and the __m128i object is at an offset that is a multiple of 16 - and the total size is 32 bytes.

The reason the compiler doesn't just do this rearrangement itself is because the standard specifies that later members of a class should have higher addresses:

Nonstatic data members of a (non-union) class with the same access control (Clause 11) are allocated so that later members have higher addresses within a class object.