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.
What you will learn
- Predict sizeof for simple structs
- Order members to reduce padding
- Convert between object representations safely
Before you start
What you’ll learn · Before you start
What you will learn
- Predict sizeof for simple structs
- Order members to reduce padding
- Convert between object representations safely
Before you start
On this page
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:
- Members are laid out in the order they are declared, each one at a higher address than the one before.
- Each member starts at an offset that is a multiple of its own alignment, so padding is inserted before it when needed.
- The struct’s alignment is the largest alignment of its members.
- 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:
#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:
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.
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:
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:
#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("");
}
} 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.
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:
// 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)}; 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:
#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);
} 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 typeTowhose bits are those offrom. Both types must be trivially copyable and of the same size, and it even works in constant expressions.std::memcpyinto an object of the other type copies the same bytes at run time.- A pointer to
unsigned charorstd::bytemay read the bytes of any object, which is howbytes.cppcopied them.
#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
} 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.
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:
#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); 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:
#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);
} 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.
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:
// 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() {} 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
- Save
reorder.cppand compile it withclang++ -std=c++23 -fsyntax-only reorder.cppor the same command withg++. - Reorder the members of each struct, without changing their names or types, until it compiles without errors.
- 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.
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:
Ordershrinks from 24 to 16 bytes, a third of the memory for a large array.-Wpaddedshows where the padding is. - Sizes and alignments come from the platform’s ABI:
longis 4 bytes on Windows and 8 on 64-bit Linux and macOS, and 32-bit x86 alignsdoubleto 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::memcpyor a byte pointer, never withreinterpret_castto another type and never through an inactive union member. - Handle byte order with shifts, or with
std::endianandstd::byteswap, whenever bytes cross a file or a network.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- x64 software conventions: types and storage (Microsoft)
- x86 Options (GCC): -malign-double (GNU Project)
- Cross-compilation using Clang (LLVM Project)
- Options to Request or Suppress Warnings (GCC): -Wpadded, -Wstrict-aliasing (GNU Project)
- Diagnostic flags in Clang (LLVM Project)
- Programming languages: C, committee draft N1570 (6.5.2.3, footnote 95) (ISO/IEC JTC1/SC22/WG14)
- System V Application Binary Interface: x86-64 psABI (x86 psABIs project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress