Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 6 – References, pointers and memory

Memory layout: sizeof, alignment and padding

Predict sizeof for C++ structs, see where padding goes and why member order matters on five platforms, and read raw bytes safely with std::bit_cast.

  • Intermediate
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Predict sizeof for simple structs
  • Order members to reduce padding
  • Convert between object representations safely

Before you start

sizeof gives the number of bytes an object of a type occupies, and for a struct that is often more than the sum of its members. Every type has an alignment: the address of an object must be a multiple of it, 8 for a double on the platforms of this track. To keep every member at an aligned address, the compiler inserts unused padding bytes between members and at the end of the struct. A struct holding a char, a double and an int therefore takes 24 bytes on a 64-bit machine although its members need 13, and the same members in another order take 16.

Who decides the numbers? Not the C++ standard, which only says that alignments are powers of two chosen by each implementation. The platform’s ABI, its application binary interface, fixes them, so that code from different compilers on one platform agrees. This lesson measures structs on five platforms, shrinks them by reordering members, and shows the safe ways to look at an object’s bytes: std::bit_cast, std::memcpy and the byte-order tools of <bit>.

Where the padding goes

The compiler follows four rules, and with them you can predict any simple struct:

  1. Members are laid out in the order they are declared, each one at a higher address than the one before.
  2. Each member starts at an offset that is a multiple of its own alignment, so padding is inserted before it when needed.
  3. The struct’s alignment is the largest alignment of its members.
  4. The struct’s size is rounded up to a multiple of its alignment, so that in an array every element starts aligned.

Take an order record whose members were written down in the order someone thought of them:

layout.cpp C++ · layout.cpp
#include <cstddef>
#include <print>

struct Order {        // the members in the order someone first wrote them
    char status;      // 'N' new, 'P' paid, 'S' shipped
    double price;
    int quantity;
};

struct OrderSorted {  // the same members, largest alignment first
    double price;
    int quantity;
    char status;
};

struct alignas(64) Counter {  // asks for a 64-byte boundary, the size of a cache line on many CPUs
    long long hits;
};

int main() {
    std::println("alignof: char {}, int {}, double {}", alignof(char), alignof(int), alignof(double));
    std::println("Order:       status at {}, price at {}, quantity at {}; sizeof {}, alignof {}",
                 offsetof(Order, status), offsetof(Order, price), offsetof(Order, quantity),
                 sizeof(Order), alignof(Order));
    std::println("OrderSorted: price at {}, quantity at {}, status at {}; sizeof {}, alignof {}",
                 offsetof(OrderSorted, price), offsetof(OrderSorted, quantity), offsetof(OrderSorted, status),
                 sizeof(OrderSorted), alignof(OrderSorted));
    std::println("Counter:     sizeof {}, alignof {}", sizeof(Counter), alignof(Counter));
    std::println("1,000,000 orders: {} bytes as Order, {} bytes as OrderSorted",
                 1'000'000 * sizeof(Order), 1'000'000 * sizeof(OrderSorted));
}

status goes at offset 0. price needs a multiple of 8, so it starts at 8, after 7 bytes of padding. quantity needs a multiple of 4, and 16 is one. That makes 20 bytes, and rule 4 rounds the size up to 24. Reordered from the largest alignment to the smallest, price fills bytes 0 to 7, quantity 8 to 11 and status byte 12, and the size rounds up from 13 to 16:

The bytes of Order and OrderSorted on a 64-bit target: Order takes 24 bytes with 11 of padding, OrderSorted 16 with 3.Order: 24 bytes, 11 of them padding0statuspadding: 7 bytes8price (double, 8 bytes)16quantity (int)padding: 4 bytesOrderSorted: 16 bytes, 3 of them padding0price (double, 8 bytes)8quantity (int)statuspad: 3

Where the padding goes in Order and in OrderSorted

Text description of the diagram

The figure shows each struct as rows of 8 bytes, with the offset of each row on the left. Padding bytes are drawn with dashed borders.

Order, 24 bytes. Row 0: status takes byte 0, then 7 bytes of padding, because price must start at a multiple of 8. Row 8: price, a double, fills all 8 bytes. Row 16: quantity, an int, takes 4 bytes, then 4 bytes of padding, so that the size is a multiple of 8 and every element of an array of Orders keeps price aligned. In total 11 of the 24 bytes are padding.

OrderSorted, 16 bytes, the same members ordered by alignment. Row 0: price fills all 8 bytes. Row 8: quantity takes 4 bytes, status 1 byte, then 3 bytes of padding. Only 3 of the 16 bytes are padding.

Offsets and sizes from both compilers Python · layout_session.py
from session import run

# Offsets, sizes and alignments on this Mac (arm64), from both compilers.
run("""
clang++ -std=c++23 -Wall layout.cpp -o layout && ./layout
g++-16 -std=c++23 -Wall layout.cpp -o layout-gcc && ./layout-gcc
""")

Output

$ clang++ -std=c++23 -Wall layout.cpp -o layout && ./layout
alignof: char 1, int 4, double 8
Order:       status at 0, price at 8, quantity at 16; sizeof 24, alignof 8
OrderSorted: price at 0, quantity at 8, status at 12; sizeof 16, alignof 8
Counter:     sizeof 64, alignof 64
1,000,000 orders: 24000000 bytes as Order, 16000000 bytes as OrderSorted
$ g++-16 -std=c++23 -Wall layout.cpp -o layout-gcc && ./layout-gcc
alignof: char 1, int 4, double 8
Order:       status at 0, price at 8, quantity at 16; sizeof 24, alignof 8
OrderSorted: price at 0, quantity at 8, status at 12; sizeof 16, alignof 8
Counter:     sizeof 64, alignof 64
1,000,000 orders: 24000000 bytes as Order, 16000000 bytes as OrderSorted

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 layout_session.py

Both compilers agree, because on one platform the ABI decides. A million orders take 24,000,000 bytes in the first layout and 16,000,000 in the second: a third of the memory, and of the cache traffic when a loop walks them, saved by moving one line. Counter shows the opposite direction. alignas(64) raises its alignment to 64, so its size grows to 64 bytes, most of it padding. Programs do this on purpose, for example to keep counters that different threads update in separate cache lines.

You do not have to work the padding out by hand. -Wpadded, which both GCC and Clang accept, reports every padding byte the compiler adds:

The compiler reports the padding Python · padded_session.py
from session import run

# -Wpadded makes the compiler report every padding byte it adds.
run("""
clang++ -std=c++23 -Wpadded -c layout.cpp -o layout.o
""")

Output

$ clang++ -std=c++23 -Wpadded -c layout.cpp -o layout.o
layout.cpp:16:20: warning: padding size of 'Counter' with 56 bytes to alignment boundary [-Wpadded]
   16 | struct alignas(64) Counter {  // asks for a 64-byte boundary, the size of a cache line on many CPUs
      |                    ^
layout.cpp:10:8: warning: padding size of 'OrderSorted' with 3 bytes to alignment boundary [-Wpadded]
   10 | struct OrderSorted {  // the same members, largest alignment first
      |        ^
layout.cpp:6:12: warning: padding struct 'Order' with 7 bytes to align 'price' [-Wpadded]
    6 |     double price;
      |            ^
layout.cpp:4:8: warning: padding size of 'Order' with 4 bytes to alignment boundary [-Wpadded]
    4 | struct Order {        // the members in the order someone first wrote them
      |        ^
4 warnings generated.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 padded_session.py

The warning is too noisy to leave on for a whole project, but it is a quick way to audit the structs you keep in large arrays or send over a network.

What the padding bytes hold

Padding bytes belong to the object but to no member, and nothing defines their value. This program fills every byte of an Order with 0xEE, sets the three members, then copies the object’s bytes into an array and prints them:

bytes.cpp C++ · bytes.cpp
#include <cstddef>
#include <cstring>
#include <print>

struct Order {
    char status;
    double price;
    int quantity;
};

int main() {
    Order order;
    std::memset(&order, 0xEE, sizeof order);  // mark every byte, padding included
    order.status = 'P';
    order.price = 99.5;
    order.quantity = 3;

    unsigned char bytes[sizeof(Order)];
    std::memcpy(bytes, &order, sizeof order);  // the object representation, byte by byte
    for (std::size_t row = 0; row < sizeof bytes; row += 8) {
        std::print("{:2}:", row);
        for (std::size_t i = row; i < row + 8; ++i) std::print(" {:02x}", bytes[i]);
        std::println("");
    }
}
The 24 bytes of one Order Python · bytes_session.py
from session import run

# The 24 bytes of one Order, eight per row, with the row's starting offset.
run("""
clang++ -std=c++23 -Wall bytes.cpp -o bytes && ./bytes
""")

Output

$ clang++ -std=c++23 -Wall bytes.cpp -o bytes && ./bytes
 0: 50 ee ee ee ee ee ee ee
 8: 00 00 00 00 00 e0 58 40
16: 03 00 00 00 ee ee ee ee

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bytes_session.py

Row 0 is status, the letter P (0x50), then 7 padding bytes still holding ee. Row 8 is the double 99.5, whose bits are 0x4058E00000000000, stored with the least significant byte first, because this Mac is little-endian. Row 16 is quantity, 3, as 03 00 00 00, and 4 more padding bytes.

Two practical consequences follow. Comparing or hashing structs as raw bytes, for example with std::memcmp, also compares the padding, so two orders with equal members can compare unequal; compare the members instead. And writing a struct’s bytes to a file or a socket also writes its padding, and ties the file to one platform’s layout and byte order, as the next section shows.

Hex Viewer & File Inspector Write an Order's bytes to a file with std::fwrite and open it here: the padding and the reversed byte order of price are easy to spot.

The same struct on five platforms

Clang can compile for other platforms without running anything. Given the option --target, it lays out abi_sizes.cpp the way each platform’s ABI requires and writes the sizes into its output, so one Mac can answer for five systems:

abi_sizes.cpp C++ · abi_sizes.cpp
// No headers: this file only asks the compiler how a target lays out five structs.
struct Pair { char c; int i; };
struct Order { char status; double price; int quantity; };
struct OrderSorted { double price; int quantity; char status; };
struct Tag { char kind; long id; };
struct Node { int value; Node* next; };

extern "C" const unsigned long long sizes[] = {
    sizeof(Pair), sizeof(Order), sizeof(OrderSorted), sizeof(Tag), sizeof(Node)};
extern "C" const unsigned long long aligns[] = {
    alignof(Pair), alignof(Order), alignof(OrderSorted), alignof(Tag), alignof(Node)};
sizeof and alignof for five targets, from one compiler Python · abi_session.py
from session import run

# Apple clang compiles abi_sizes.cpp for five targets without running anything: the two arrays in its output hold
# sizeof and alignof of Pair, Order, OrderSorted, Tag and Node, in that order.
run("""
clang++ -std=c++23 --target=arm64-apple-macos -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
clang++ -std=c++23 --target=x86_64-pc-linux-gnu -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
clang++ -std=c++23 --target=x86_64-pc-windows-msvc -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
clang++ -std=c++23 --target=i686-pc-linux-gnu -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
clang++ -std=c++23 --target=armv7-unknown-linux-gnueabihf -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
""")

Output

$ clang++ -std=c++23 --target=arm64-apple-macos -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
@sizes = constant [5 x i64] [i64 8, i64 24, i64 16, i64 16, i64 16], align 8
@aligns = constant [5 x i64] [i64 4, i64 8, i64 8, i64 8, i64 8], align 8
$ clang++ -std=c++23 --target=x86_64-pc-linux-gnu -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
@sizes = dso_local constant [5 x i64] [i64 8, i64 24, i64 16, i64 16, i64 16], align 16
@aligns = dso_local constant [5 x i64] [i64 4, i64 8, i64 8, i64 8, i64 8], align 16
$ clang++ -std=c++23 --target=x86_64-pc-windows-msvc -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
@sizes = dso_local constant [5 x i64] [i64 8, i64 24, i64 16, i64 8, i64 16], align 16
@aligns = dso_local constant [5 x i64] [i64 4, i64 8, i64 8, i64 4, i64 8], align 16
$ clang++ -std=c++23 --target=i686-pc-linux-gnu -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
@sizes = dso_local constant [5 x i64] [i64 8, i64 16, i64 16, i64 8, i64 8], align 8
@aligns = dso_local constant [5 x i64] [i64 4, i64 4, i64 4, i64 4, i64 4], align 8
$ clang++ -std=c++23 --target=armv7-unknown-linux-gnueabihf -S -emit-llvm -o - abi_sizes.cpp | grep -E '^@(sizes|aligns)'
@sizes = dso_local constant [5 x i64] [i64 8, i64 24, i64 16, i64 8, i64 8], align 8
@aligns = dso_local constant [5 x i64] [i64 4, i64 8, i64 8, i64 4, i64 4], align 8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 abi_session.py

Each run prints two lines, the sizes and the alignments of the five structs in the order Pair, Order, OrderSorted, Tag and Node, and the runs go from macOS arm64 down to 32-bit ARM Linux. Read across the runs, struct by struct:

  • Pair (char, int): 8 bytes on all five.
  • Order (char, double, int): 24 bytes, except 16 on 32-bit x86 Linux.
  • OrderSorted (double, int, char): 16 bytes on all five.
  • Tag (char, long): 16 bytes on 64-bit macOS and Linux, 8 on Windows x64 and on both 32-bit targets.
  • Node (int, Node*): 16 bytes on the three 64-bit targets, 8 on the two 32-bit ones.

Two different causes are at work. First, the sizes of the types themselves: long is 8 bytes on 64-bit Linux and macOS but 4 bytes on 64-bit Windows, as Microsoft’s x64 conventions list, and pointers are 4 bytes on 32-bit targets. Second, their alignments: the 32-bit x86 ABI places a double inside a struct at a multiple of 4, not 8, which GCC’s documentation of -malign-double describes, so Order needs only 16 bytes there. Only Pair and OrderSorted are the same everywhere. A struct whose layout must match across machines, for a file format or a network protocol, should use fixed-width types such as std::int32_t, and should be written and read field by field rather than as raw bytes.

India-specific: placement tests

When an aptitude round of a campus placement test asks for the sizeof of a struct without naming a platform, state the platform in your answer: struct { char c; double d; int i; } is 24 bytes on 64-bit Linux, Windows and macOS, and 16 on 32-bit x86 Linux. A struct with a long or a pointer changes with the platform too, as Tag and Node show.

Looking at an object’s bytes safely

Sooner or later you need the bits of a float, or the bytes of a number read from a file. The tempting way is a cast, *reinterpret_cast<std::uint32_t*>(&f), and it is undefined behaviour. The standard’s aliasing rule lets you read an object only through its own type, its signed or unsigned twin, or a byte type: char, unsigned char or std::byte. Optimisers rely on the rule to assume that a float and a std::uint32_t are never the same object. GCC sees the problem at -O2; Clang stays silent:

pun.cpp C++ · pun.cpp
#include <cstdint>
#include <print>

int main() {
    float f = 1.5f;
    std::uint32_t bits = *reinterpret_cast<std::uint32_t*>(&f);  // reads a float as an integer: undefined behaviour
    std::println("0x{:08x}", bits);
}
The punning cast, compiled by both compilers Python · aliasing_session.py
from session import run

# The type-punning cast, compiled with warnings and optimisation by each compiler.
run("""
g++-16 -std=c++23 -O2 -Wall -c pun.cpp -o pun.o
clang++ -std=c++23 -O2 -Wall -c pun.cpp -o pun.o
""")

Output

$ g++-16 -std=c++23 -O2 -Wall -c pun.cpp -o pun.o
pun.cpp: In function 'int main()':
pun.cpp:6:27: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
    6 |     std::uint32_t bits = *reinterpret_cast<std::uint32_t*>(&f);  // reads a float as an integer: undefined behaviour
      |                           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pun.cpp:6:26: warning: 'f' is used uninitialized [-Wuninitialized]
    6 |     std::uint32_t bits = *reinterpret_cast<std::uint32_t*>(&f);  // reads a float as an integer: undefined behaviour
      |                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pun.cpp:5:11: note: 'f' declared here
    5 |     float f = 1.5f;
      |           ^
$ clang++ -std=c++23 -O2 -Wall -c pun.cpp -o pun.o

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aliasing_session.py

GCC’s second warning shows the rule in action: the optimiser no longer connects the read through std::uint32_t* with the store to f, so it reports f as read uninitialised. Clang printing nothing does not make the code correct. There are three correct ways, all of them well defined:

  • std::bit_cast<To>(from) (C++20, <bit>) returns a new object of type To whose bits are those of from. Both types must be trivially copyable and of the same size, and it even works in constant expressions.
  • std::memcpy into an object of the other type copies the same bytes at run time.
  • A pointer to unsigned char or std::byte may read the bytes of any object, which is how bytes.cpp copied them.
float_bits.cpp C++ · float_bits.cpp
#include <bit>
#include <cstdint>
#include <limits>
#include <print>

int main() {
    for (float f : {1.0f, -2.5f, 0.1f, std::numeric_limits<float>::infinity()}) {
        auto bits = std::bit_cast<std::uint32_t>(f);  // the same 32 bits, now as an integer
        std::println("{:>5} -> 0x{:08x}  sign {}  exponent {:3}  fraction 0x{:06x}",
                     f, bits, bits >> 31, (bits >> 23) & 0xFF, bits & 0x7FFFFF);
    }
    static_assert(std::bit_cast<std::uint32_t>(1.0f) == 0x3F80'0000);  // bit_cast works at compile time too
}
The bits of four floats Python · bits_session.py
from session import run

# The bits of four floats, read with std::bit_cast; Apple clang, then GCC.
run("""
clang++ -std=c++23 -Wall float_bits.cpp -o float_bits && ./float_bits
g++-16 -std=c++23 -Wall float_bits.cpp -o float_bits-gcc && ./float_bits-gcc
""")

Output

$ clang++ -std=c++23 -Wall float_bits.cpp -o float_bits && ./float_bits
    1 -> 0x3f800000  sign 0  exponent 127  fraction 0x000000
 -2.5 -> 0xc0200000  sign 1  exponent 128  fraction 0x200000
  0.1 -> 0x3dcccccd  sign 0  exponent 123  fraction 0x4ccccd
  inf -> 0x7f800000  sign 0  exponent 255  fraction 0x000000
$ g++-16 -std=c++23 -Wall float_bits.cpp -o float_bits-gcc && ./float_bits-gcc
    1 -> 0x3f800000  sign 0  exponent 127  fraction 0x000000
 -2.5 -> 0xc0200000  sign 1  exponent 128  fraction 0x200000
  0.1 -> 0x3dcccccd  sign 0  exponent 123  fraction 0x4ccccd
  inf -> 0x7f800000  sign 0  exponent 255  fraction 0x000000

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bits_session.py

An IEEE 754 single-precision number has a sign bit, 8 exponent bits stored with a bias of 127, and 23 fraction bits. 1.0 is exponent 127, that is 2 to the power 0, with an empty fraction. 0.1 has a fraction that never ends in binary, so its last digit is rounded, and 0x3dcccccd is only the nearest float to 0.1.

IEEE 754 Floating-Point Converter Type 0.1 or -2.5 to see the same sign, exponent and fraction fields, in 32 and 64 bits.

Why a union is not a converter

A union stores all its members at the same address. In C, reading a member other than the one last written reinterprets the stored bytes in the new type, which the C standard calls type punning. In C++ only one member is active at a time, the one most recently written, and reading another is undefined behaviour. Inside a constant expression compilers must diagnose undefined behaviour, so both of them refuse the trick:

union_pun.cpp C++ · union_pun.cpp
#include <cstdint>

union FloatBits {
    float f;
    std::uint32_t u;
};

constexpr std::uint32_t bits_of_one() {
    FloatBits x{.f = 1.0f};  // f is the active member
    return x.u;              // reads the member that is not active
}

static_assert(bits_of_one() == 0x3F80'0000);
Both compilers reject the inactive read Python · union_session.py
from session import run

# Reading the inactive member of a union inside a constant expression, where undefined behaviour must be diagnosed.
run("""
clang++ -std=c++23 -fsyntax-only union_pun.cpp
g++-16 -std=c++23 -fsyntax-only union_pun.cpp
""")

Output

$ clang++ -std=c++23 -fsyntax-only union_pun.cpp
union_pun.cpp:13:15: error: static assertion expression is not an integral constant expression
   13 | static_assert(bits_of_one() == 0x3F80'0000);
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
union_pun.cpp:10:12: note: read of member 'u' of union with active member 'f' is not allowed in a constant expression
   10 |     return x.u;              // reads the member that is not active
      |            ^
union_pun.cpp:13:15: note: in call to 'bits_of_one()'
   13 | static_assert(bits_of_one() == 0x3F80'0000);
      |               ^~~~~~~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fsyntax-only union_pun.cpp
union_pun.cpp:13:29: error: non-constant condition for static assertion
   13 | static_assert(bits_of_one() == 0x3F80'0000);
      |               ~~~~~~~~~~~~~~^~~~~~~~~~~~~~
union_pun.cpp:13:26:   in 'constexpr' expansion of 'bits_of_one()'
   13 | static_assert(bits_of_one() == 0x3F80'0000);
      |               ~~~~~~~~~~~^~
union_pun.cpp:10:14: error: accessing 'FloatBits::u' member instead of active 'FloatBits::f' member in constant expression
   10 |     return x.u;              // reads the member that is not active
      |            ~~^
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 union_session.py

static_assert(std::bit_cast<std::uint32_t>(1.0f) == 0x3F80'0000) in float_bits.cpp compiled without complaint for the same values. When you need a type that holds one of several alternatives, use std::variant, which knows which one it holds; when you need another type’s view of the same bits, use std::bit_cast.

Byte order: std::endian and std::byteswap

Files and network protocols fix a byte order, and many of them use big-endian, most significant byte first, which is the opposite of this Mac. std::endian::native (C++20) tells you the order of the machine you compile for, and std::byteswap (C++23) reverses the bytes of an integer:

big_endian.cpp C++ · big_endian.cpp
#include <array>
#include <bit>
#include <cstdint>
#include <cstring>
#include <print>

// A message length as it arrives over a network: 4 bytes, most significant byte first.
constexpr std::array<unsigned char, 4> wire{0x00, 0x01, 0x86, 0xA0};

// Works on every machine: builds the number from the bytes with shifts.
constexpr std::uint32_t read_be32(const unsigned char* p) {
    return std::uint32_t{p[0]} << 24 | std::uint32_t{p[1]} << 16 | std::uint32_t{p[2]} << 8 | std::uint32_t{p[3]};
}

int main() {
    std::println("this machine is {}-endian", std::endian::native == std::endian::little ? "little" : "big");
    std::println("read_be32:         {}", read_be32(wire.data()));

    std::uint32_t raw;
    std::memcpy(&raw, wire.data(), sizeof raw);  // the bytes as they lie, in this machine's order
    std::println("memcpy alone:      {}", raw);
    if constexpr (std::endian::native == std::endian::little) raw = std::byteswap(raw);
    std::println("memcpy + byteswap: {}", raw);
    static_assert(read_be32(wire.data()) == 100'000);
}
Reading a big-endian length on a little-endian Mac Python · endian_session.py
from session import run

# Reading a big-endian 32-bit number on a little-endian Mac, three ways.
run("""
clang++ -std=c++23 -Wall big_endian.cpp -o big_endian && ./big_endian
g++-16 -std=c++23 -Wall big_endian.cpp -o big_endian-gcc && ./big_endian-gcc
""")

Output

$ clang++ -std=c++23 -Wall big_endian.cpp -o big_endian && ./big_endian
this machine is little-endian
read_be32:         100000
memcpy alone:      2693136640
memcpy + byteswap: 100000
$ g++-16 -std=c++23 -Wall big_endian.cpp -o big_endian-gcc && ./big_endian-gcc
this machine is little-endian
read_be32:         100000
memcpy alone:      2693136640
memcpy + byteswap: 100000

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 endian_session.py

The bytes 00 01 86 A0 mean 100,000 to a big-endian reader. Copying them into a std::uint32_t on a little-endian machine gives 2,693,136,640 instead, because the bytes are read in the opposite order; swapping them repairs it. The shift version, read_be32, is the one to prefer: it never depends on the machine’s order, needs no if constexpr and, being constexpr, can be checked by static_assert.

Programmer Calculator (Bitwise) Enter 0x000186A0 at 32 bits and use the byte-swap button to see 0xA0860100, the number the memcpy line printed.

Your turn: shrink five structs

reorder.cpp declares five structs. Under each one a static_assert states the smallest size that struct can have on a 64-bit target, and every assertion fails as the members are ordered now:

reorder.cpp (the exercise) C++ · reorder.cpp
// Exercise: reorder the members of each struct so that the static_assert under it passes.
// Change only the order of the members; keep their names and types.
// The sizes hold on every 64-bit target of this track: arm64 macOS, x86-64 Linux and x86-64 Windows.

struct Sensor {
    bool active;
    double reading;
    short id;
};
static_assert(sizeof(Sensor) == 16, "Sensor can be 16 bytes");

struct Packet {
    char kind;
    int length;
    char flags;
    int checksum;
};
static_assert(sizeof(Packet) == 12, "Packet can be 12 bytes");

struct Student {
    char grade;
    long long roll_number;
    char section;
    int marks;
};
static_assert(sizeof(Student) == 16, "Student can be 16 bytes");

struct Node {
    int value;
    Node* next;
    bool visited;
};
static_assert(sizeof(Node) == 16, "Node can be 16 bytes");

struct Pixel {
    char r;
    short x;
    char g;
    short y;
    char b;
};
static_assert(sizeof(Pixel) == 8, "Pixel can be 8 bytes");

int main() {}
The starting point: five failed assertions Python · reorder_session.py
from session import run

# The starting point of the exercise: every static_assert fails, and the compiler says what each size is now.
run("""
clang++ -std=c++23 -fsyntax-only reorder.cpp
""", max_lines=60)

Output

$ clang++ -std=c++23 -fsyntax-only reorder.cpp
reorder.cpp:10:15: error: static assertion failed due to requirement 'sizeof(Sensor) == 16': Sensor can be 16 bytes
   10 | static_assert(sizeof(Sensor) == 16, "Sensor can be 16 bytes");
      |               ^~~~~~~~~~~~~~~~~~~~
reorder.cpp:10:30: note: expression evaluates to '24 == 16'
   10 | static_assert(sizeof(Sensor) == 16, "Sensor can be 16 bytes");
      |               ~~~~~~~~~~~~~~~^~~~~
reorder.cpp:18:15: error: static assertion failed due to requirement 'sizeof(Packet) == 12': Packet can be 12 bytes
   18 | static_assert(sizeof(Packet) == 12, "Packet can be 12 bytes");
      |               ^~~~~~~~~~~~~~~~~~~~
reorder.cpp:18:30: note: expression evaluates to '16 == 12'
   18 | static_assert(sizeof(Packet) == 12, "Packet can be 12 bytes");
      |               ~~~~~~~~~~~~~~~^~~~~
reorder.cpp:26:15: error: static assertion failed due to requirement 'sizeof(Student) == 16': Student can be 16 bytes
   26 | static_assert(sizeof(Student) == 16, "Student can be 16 bytes");
      |               ^~~~~~~~~~~~~~~~~~~~~
reorder.cpp:26:31: note: expression evaluates to '24 == 16'
   26 | static_assert(sizeof(Student) == 16, "Student can be 16 bytes");
      |               ~~~~~~~~~~~~~~~~^~~~~
reorder.cpp:33:15: error: static assertion failed due to requirement 'sizeof(Node) == 16': Node can be 16 bytes
   33 | static_assert(sizeof(Node) == 16, "Node can be 16 bytes");
      |               ^~~~~~~~~~~~~~~~~~
reorder.cpp:33:28: note: expression evaluates to '24 == 16'
   33 | static_assert(sizeof(Node) == 16, "Node can be 16 bytes");
      |               ~~~~~~~~~~~~~^~~~~
reorder.cpp:42:15: error: static assertion failed due to requirement 'sizeof(Pixel) == 8': Pixel can be 8 bytes
   42 | static_assert(sizeof(Pixel) == 8, "Pixel can be 8 bytes");
      |               ^~~~~~~~~~~~~~~~~~
reorder.cpp:42:29: note: expression evaluates to '10 == 8'
   42 | static_assert(sizeof(Pixel) == 8, "Pixel can be 8 bytes");
      |               ~~~~~~~~~~~~~~^~~~
5 errors generated.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reorder_session.py

  1. Save reorder.cpp and compile it with clang++ -std=c++23 -fsyntax-only reorder.cpp or the same command with g++.
  2. Reorder the members of each struct, without changing their names or types, until it compiles without errors.
  3. Check yourself without the compiler first: write down each member’s size and alignment, place the members by the four rules, and predict the size before you run the command.

Hint: on these targets a long long, a double and a pointer have alignment 8, an int 4, a short 2, and a char or a bool 1. Placing members from the largest alignment to the smallest never leaves a gap between them; only the end of the struct may need padding. The note under each failed assertion, such as expression evaluates to '24 == 16', gives the current size.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 What is sizeof(S) for struct S { char a; short b; char c; }; on a 64-bit platform?

    Type a number.

    Show the answer to question 1

    Answer: 6 bytes

    a takes byte 0, b needs a multiple of 2 and starts at 2 after one padding byte, and c takes byte 4. That is 5 bytes, and the size rounds up to a multiple of the struct's alignment, 2, giving 6. Ordered as short, char, char it is 4 bytes.

  2. Question 2 of 5 What is sizeof(T) for struct T { short s; double d; char c; }; on a 64-bit platform?

    Type a number.

    Show the answer to question 2

    Answer: 24 bytes

    s takes bytes 0 and 1, d needs a multiple of 8 and starts at 8, and c takes byte 16. That is 17 bytes, rounded up to a multiple of 8: 24. With d first it is 16.

  3. Question 3 of 5 struct Tag { char kind; long id; }; is 16 bytes on 64-bit Linux. What is it on 64-bit Windows, and why?

    Choose one answer.

    Show the answer to question 3

    Answer: 8 bytes, because long is 4 bytes on Windows

    Microsoft's x64 conventions make long 4 bytes wide with 4-byte alignment, so id starts at 4 and the struct is 8 bytes. Linux and macOS use 8-byte longs. Use std::int64_t when you need the same width everywhere.

  4. Question 4 of 5 Which of these reads the bits of a float f as a std::uint32_t without undefined behaviour?

    Choose one answer.

    Show the answer to question 4

    Answer: std::bit_cast<std::uint32_t>(f)

    std::bit_cast copies the bits into a new object of the other type. The reinterpret_cast reads a float through an integer type, which the aliasing rule forbids, and the union reads an inactive member, which C++ does not allow. static_cast converts the value: 1.5f becomes 1, not 0x3FC00000.

  5. Question 5 of 5 The bytes 00 00 01 2C arrive from a network, most significant byte first. On a little-endian machine you copy them into a std::uint32_t with std::memcpy. What value do you get?

    Choose one answer.

    Show the answer to question 5

    Answer: 0x2C010000, so std::byteswap is needed to get 300

    A little-endian machine treats the first byte in memory as the least significant, so it reads 0x2C010000. Swap the bytes, or build the number with shifts as read_be32 does, which gives 300 on every machine.

Interview questions

Fresher · Why is sizeof a struct holding a char and an int usually 8 and not 5?

Because the int must start at an address that is a multiple of its alignment, which is 4 on common platforms. The char takes byte 0, the compiler inserts 3 padding bytes, and the int occupies bytes 4 to 7, so the struct is 8 bytes. The platform’s ABI requires that alignment because aligned values are the ones the processor reads fastest. A complete answer adds that the struct’s size is also rounded up to its alignment, so { int i; char c; } is 8 bytes too, with the padding at the end, and that the exact numbers belong to the platform’s ABI rather than to the C++ standard.

Key takeaways

  • A struct’s members are laid out in declaration order, each at a multiple of its alignment, and the size is rounded up to the struct’s alignment. Padding fills the gaps.
  • Ordering members from the largest alignment to the smallest removes the gaps between them: Order shrinks from 24 to 16 bytes, a third of the memory for a large array. -Wpadded shows where the padding is.
  • Sizes and alignments come from the platform’s ABI: long is 4 bytes on Windows and 8 on 64-bit Linux and macOS, and 32-bit x86 aligns double to 4 bytes inside structs. Name the platform when you state a size.
  • Padding bytes have no defined value, so never compare or hash structs as raw bytes.
  • Read an object’s bits with std::bit_cast, std::memcpy or a byte pointer, never with reinterpret_cast to another type and never through an inactive union member.
  • Handle byte order with shifts, or with std::endian and std::byteswap, whenever bytes cross a file or a network.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.