#106 – How padding secretly increases the size of your structs

A struct’s size is not always the sum of its members.

The compiler may insert unused bytes between members so each member is placed at an address suitable for its alignment requirement. Those unused bytes are called padding, and they can make two structs with the same members have different sizes.

Three things to take away:

  • Struct members may require specific alignment.
  • The compiler can insert padding between members and at the end of a struct.
  • Reordering members from largest alignment to smallest can reduce wasted space.

Why alignment exists

Processors usually access memory more efficiently when objects are stored at suitably aligned addresses.

For example, an int is commonly 4 bytes and often has a 4-byte alignment requirement. That means the compiler prefers to place it at an address divisible by 4.

C++ exposes this requirement with alignof:

#include <iostream>

int main(){
    std::cout << "sizeof(char): " << sizeof(char) << '\n';
    std::cout << "sizeof(short): " << sizeof(short) << '\n';
    std::cout << "sizeof(int): " << sizeof(int) << '\n';

    std::cout << "alignof(char): " << alignof(char) << '\n';
    std::cout << "alignof(short): " << alignof(short) << '\n';
    std::cout << "alignof(int): " << alignof(int) << '\n';
}

Terminal output on a typical system:
sizeof(char): 1
sizeof(short): 2
sizeof(int): 4
alignof(char): 1
alignof(short): 2
alignof(int): 4

The exact values can vary by platform and ABI, but these values are common on modern desktop systems.

The important point is that size and alignment are related, but they are not the same concept.

sizeof(T) tells you how many bytes an object occupies.

alignof(T) tells you the address boundary the object should be placed on.

Struct size is not just member size

Consider this struct:

#include <iostream>

struct BadPacketLayout {
    char status {};
    int sampleCount {};
    short checksum {};
};

int main(){
    std::cout << "Sum of member sizes: "
              << sizeof(char) + sizeof(int) + sizeof(short) << '\n';

    std::cout << "sizeof(BadPacketLayout): "
              << sizeof(BadPacketLayout) << '\n';

    std::cout << "alignof(BadPacketLayout): "
              << alignof(BadPacketLayout) << '\n';
}

Terminal output on a typical system:
Sum of member sizes: 7
sizeof(BadPacketLayout): 12
alignof(BadPacketLayout): 4

The members add up to 7 bytes:

char  + int + short
1     + 4   + 2     = 7

But the struct is 12 bytes.

The extra 5 bytes are padding.

Where the padding comes from

A typical layout for BadPacketLayout looks like this:

offset 0: status       char   1 byte
offset 1: padding             3 bytes
offset 4: sampleCount  int    4 bytes
offset 8: checksum     short  2 bytes
offset 10: padding            2 bytes

Why?

The first member is a char, so it can start at offset 0.

The next member is an int. If int requires 4-byte alignment, it should start at an offset divisible by 4. Offset 1 is not suitable, so the compiler inserts 3 bytes of padding.

After the int, the short can start at offset 8.

The struct then has 10 bytes of actual layout so far, but the complete struct must also satisfy its own alignment requirement. Since the struct contains an int, its alignment is commonly 4. The final size must be a multiple of 4 so that arrays of this struct keep each element correctly aligned.

So the compiler adds 2 bytes of tail padding:

10 → 12

This makes sizeof(BadPacketLayout) equal to 12.

Member order can reduce padding

Now reorder the exact same members:

#include <iostream>

struct BadPacketLayout {
    char status {};
    int sampleCount {};
    short checksum {};
};

struct BetterPacketLayout {
    int sampleCount {};
    short checksum {};
    char status {};
};

int main(){
    std::cout << "sizeof(BadPacketLayout): "
              << sizeof(BadPacketLayout) << '\n';

    std::cout << "sizeof(BetterPacketLayout): "
              << sizeof(BetterPacketLayout) << '\n';
}

Terminal output on a typical system:
sizeof(BadPacketLayout): 12
sizeof(BetterPacketLayout): 8

The two structs contain the same members:

char
int
short

Only the order changed.

The better layout places the largest-alignment member first:

int sampleCount {};
short checksum {};
char status {};

A typical layout is:

offset 0: sampleCount  int    4 bytes
offset 4: checksum     short  2 bytes
offset 6: status       char   1 byte
offset 7: padding             1 byte

The struct now occupies 8 bytes instead of 12.

That is a 33% size reduction for the same data.

Padding matters more in arrays

Padding often looks insignificant for one object.

A difference of 4 bytes may not matter here: BadPacketLayout packet {};

But it matters when you store many of them:

#include <iostream>

struct BadPacketLayout {
    char status {};
    int sampleCount {};
    short checksum {};
};

struct BetterPacketLayout {
    int sampleCount {};
    short checksum {};
    char status {};
};

int main(){
    constexpr int packetCount { 1000 };

    std::cout << "Bad layout array: "
              << sizeof(BadPacketLayout) * packetCount << " bytes\n";

    std::cout << "Better layout array: "
              << sizeof(BetterPacketLayout) * packetCount << " bytes\n";
}

Terminal output on a typical system:
Bad layout array: 12000 bytes
Better layout array: 8000 bytes

The wasted space scales with the number of objects.

This is why padding matters in data-heavy programs, embedded systems, serialization code, and cache-sensitive systems.

Inspecting member offsets

The standard library macro offsetof can show where each member begins.

#include <cstddef>
#include <iostream>

struct PacketLayout {
    char status {};
    int sampleCount {};
    short checksum {};
};

int main(){
    std::cout << "status offset: "
              << offsetof(PacketLayout, status) << '\n';

    std::cout << "sampleCount offset: "
              << offsetof(PacketLayout, sampleCount) << '\n';

    std::cout << "checksum offset: "
              << offsetof(PacketLayout, checksum) << '\n';

    std::cout << "sizeof(PacketLayout): "
              << sizeof(PacketLayout) << '\n';
}

Terminal output on a typical system:
status offset: 0
sampleCount offset: 4
checksum offset: 8
sizeof(PacketLayout): 12

This confirms that sampleCount does not start immediately after status.

There is padding between them.

alignof for structs

A struct’s alignment is usually the strictest alignment requirement of its members.

#include <iostream>

struct PacketLayout {
    char status {};
    int sampleCount {};
    short checksum {};
};

int main(){
    std::cout << "alignof(char): " << alignof(char) << '\n';
    std::cout << "alignof(short): " << alignof(short) << '\n';
    std::cout << "alignof(int): " << alignof(int) << '\n';
    std::cout << "alignof(PacketLayout): " << alignof(PacketLayout) << '\n';
}

Terminal output on a typical system:
alignof(char): 1
alignof(short): 2
alignof(int): 4
alignof(PacketLayout): 4

Because the struct contains an int, the whole struct commonly needs 4-byte alignment.

This is also why tail padding exists: if you create an array of structs, every element in the array must start at an address that satisfies the struct’s alignment.

A practical ordering rule

A simple rule often reduces padding:

Order members from largest alignment requirement to smallest.

For many basic structs, that means placing members roughly from largest to smallest:

double
int
short
char
bool

Example:

struct BetterLayout {
    double measurement {};
    int count {};
    short flags {};
    char status {};
};

This is not a law. Member order should still reflect the meaning of the type where readability matters. But when a struct is stored in large arrays or used in memory-sensitive code, layout order is worth checking.

Takeaway

Struct padding is the compiler inserting unused bytes to satisfy alignment requirements.

This means a struct’s size is not always the sum of its member sizes. Two structs with the same members can have different sizes purely because the members are declared in a different order.

The rule is simple: if struct size matters, check sizeof, check alignof, and consider ordering members from largest alignment to smallest.