Short version: sizeof in C++ can be misleading and cause problems,
particularly inside assignment overloads, and public/private has
surprising effects on alignment.

Object alignment requirements can cause some subtle and
hard-to-diagnose bugs in C++. The demonstrations here show two issues
that have caused problems that I ended up debugging, and I hope they
help others find similar issues buried in their code. (Issues seen
with gcc-10, gcc-14, clang-14, clang-17, and others. Note that all
examples here are distilled down from much more complex code, so many
questions about alternative implementations are out of scope.)

First, some background.

Most C and C++ programmers are aware that struct and class members are
placed on "natural" alignment boundaries. This placement is dependent
on CPU architecture and a number of other factors (including "packed"
options supported by some, but not all, compilers and architectures),
but the most common rule is that by default each member is placed at
an offset is that is a multiple of the size of the object. That is, a
'char' is on any byte boundary, a two-byte 'short' is on an even byte
boundary, a four-byte 'int' is on a multiple of 4 bytes, and so on. If
the offset after the preceding element doesn't provide the right
alignment, then the compiler automatically inserts hidden padding to
make it right. For example:

struct {
  char first;   /* must be at offset 0 in the struct */
  short second; /* perhaps offset 2; 1 one byte pad inserted before */
};

The overall size of that struct above may be either 3 or 4 (or perhaps
even more), depending on the requirements of the platform. This is why
it's somewhat common practice to put the large objects first in a
structure, followed by smaller ones, or to group small objects
together in clusters. Both strategies minimize this wasteful
padding. Note that this padding is between successive members in a
structure.

In general, these size-based rules apply to fundamental types, and do
not directly apply to compounds (nested structures). Instead, a
compound has composite size and alignment based on its contents. That
is, a struct has a required alignment that is equal to the strictest
(largest) requirement of any member inside, and not its overall size.

A subtle corollary of the above rules is that sizeof() on a struct (or
class) must return a value that is rounded up based on the alignment
of the strictest member inside, effectively producing trailing hidden
padding. That's because sizeof() must always give the proper stride of
objects within an array. For example:

struct {
  int val1;
  short val2;
};

This struct should have size 6 (assuming a 4-byte int and 2-byte
short), with no padding. But the natural alignment of that 'int' is on
a 4-byte boundary, so the sizeof() will be 8, making sure that
adjacent array entries are all on natural alignment boundaries when
multiple objects are allocated. Note that this shows trailing padding,
after the last member.

The first problem, shown in align-surprise1.cpp, is that the C++
compiler will naturally pack together member variables using an
alignment that is less strict than the alignment required for the base
class. This requires some explanation.

The definition of SimplePOD includes a naive optimization: we know
that all of the members are just plain old data, and the class itself
is not virtual (thus has no vft pointer to worry about), so why not
take advantage of that, and use memset/memcpy rather than individually
assigning each object?

The subtle error here is that the size being copied includes that
alignment padding at the end, and the compiler is free to insert
other, unrelated members inside that padding. It can't place them
between members (as best I can tell), but the trailing padding is fair
game.

In this case, this means that the data copied by the SimplePOD
assignment operator includes an extra 4 bytes (actual length is 20 but
sizeof is 24). This means that the private members in both TestClass
and TestClass2 are overwritten by the assignment of the base
class. This test just shows the effect of that issue, which is
effectively memory corruption. In the case where I originally
encountered this problem, the contents of a smart pointer was copied,
resulting in a reference count mismatch and a crash.

You can see the problem demonstrated by running "make" and then
executing "./surprise1-fail". The fixed version is built as
"./surprise1-pass".

The fix (enabled by FIX_BUG) computes the actual size of SimplePOD and
uses that for the copy. Note that commenting out the assignment
operator overload in SimplePOD also fixes the problem, as the compiler
will internally compute the correct amount to copy. C++ itself just
provides no convenient means for the user to compute this value, which
is important if an overload is needed by the overall class design.

The second problem is shown by align-surprise2.cpp and is even more
shocking. In some cases, changing from "public" to private or
protected will cause the member alignment and the overall object size
both to change. This is demonstrated by the output of
"./surprise2-public" and "./surprise2-private". The only difference is
whether the members are public or private.

The surprise2-public output looks like this:

baz 12 foo 8
a 0
b 4
c 8
d 9

This shows that the size of baz is 12, and that the alignment of 'c'
starts on the next int boundary. But surprise2-private (and
surprise2-protected) show this:

baz 8 foo 8
a 0
b 4
c 5
d 6

The size of baz is down to 8 and the alignment of 'c' has changed to
start on a char boundary. Again, the only change is the visibility of
the members.

This has several implications. One is that if (say) you are debugging
a problem and change some members from private to public just to
simplify some temporary debug code, you might also be inadvertently
changing the actual offset of those members within the object. If the
entire project isn't recompiled, you could have mysterious behavior or
even crashes as a result. Another is that seemingly innocuous
improvements in C++ code (for example, making public members private
and providing accessors instead) could easily change the size of the
object and affect cache alignment, drastically altering performance by
creating new opportunities for false sharing. Still another is that if
you cast pointers back and forth between different classes, you may
find that the actual offsets of members in those classes depend on the
visibility specified, and thus memory corruption may occur.

It's a jungle out there!
