Why This Matters
Every ABI contract, serialization format, CUDA kernel launch, and memcpy-based deserialization depends on knowing exactly how many bytes a type occupies and at which address offset each member lives. On a 64-bit Linux system, sizeof(long) is 8 bytes; on 64-bit Windows it is 4. If your network protocol or GPU kernel assumes the wrong size, data is silently misread with no compiler warning.
Struct padding is a second source of invisible waste. A naively ordered {char, int, char} struct on x86-64 occupies 12 bytes, not 6. Reordering to {int, char, char} drops it to 8. The difference is irrelevant for a single instance and decisive when you have 10 million of them in a hot path.
Core Definitions
Fundamental type
A type whose representation is specified (in part) by the C++ standard and implemented directly in hardware registers: bool, char, short, int, long, long long, float, double, long double, and void. The standard mandates relative size orderings (e.g., sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)) but fixes few absolute sizes.
Alignment
The requirement that an object of type T may only begin at an address that is a multiple of alignof(T). On x86-64, alignof(int) == 4, so an int must start at an address divisible by 4. Misalignment either traps (strict-alignment architectures) or incurs a hardware penalty (x86).
Trivially-copyable type
A type T for which std::is_trivially_copyable_v<T> is true. The standard guarantees that any such object can be byte-copied with memcpy and the destination is a valid T. Specifically, T must have a trivial copy constructor, trivial copy-assignment operator, trivial move constructor, trivial move-assignment operator, and a trivial (or deleted) destructor.
Standard-layout type
A type whose layout is compatible with the equivalent C struct or union: no virtual functions or virtual base classes, all non-static data members share the same access control, at most one class in the inheritance hierarchy has non-static data members, and the first non-static data member is not of the same type as the base class. std::is_standard_layout_v<T> tests this at compile time.
Fundamental Type Sizes and ABI Models
The C++ standard sets floor constraints, not ceilings. Two ABI models dominate 64-bit systems:
| Type | LP64 (Linux, macOS) | LLP64 (Windows MSVC) | Guarantee |
|---|---|---|---|
bool | 1 | 1 | at least 1 |
char | 1 | 1 | exactly 1 |
short | 2 | 2 | at least 2 |
int | 4 | 4 | at least 2 |
long | 8 | 4 | at least 4 |
long long | 8 | 8 | at least 8 |
float | 4 | 4 | IEEE 754 single |
double | 8 | 8 | IEEE 754 double |
| pointer | 8 | 8 | — |
The single portable workaround for fixed-width integers is <cstdint>: int32_t, uint64_t, and friends. Use these in any cross-platform serialization or hardware register map.
Signed-ness of char: The standard leaves char signed or unsigned at implementation discretion. Avoid using plain char to hold arithmetic values; use int8_t or uint8_t explicitly.
Alignment and Struct Padding
Natural alignment
Each fundamental type aligns to its own size on x86-64 (up to 8 bytes). alignof(double) == 8, alignof(int) == 4, alignof(char) == 1.
The compiler inserts padding bytes between members so that each member satisfies its alignment requirement, and appends tail padding so that an array of structs keeps every element aligned.
Worked byte-level example
struct Bad { // 12 bytes on x86-64
char a; // offset 0, size 1
// 3 bytes padding
int b; // offset 4, size 4
char c; // offset 8, size 1
// 3 bytes tail-padding
}; // sizeof == 12, alignof == 4
struct Good { // 8 bytes on x86-64
int b; // offset 0, size 4
char a; // offset 4, size 1
char c; // offset 5, size 1
// 2 bytes tail-padding
}; // sizeof == 8, alignof == 4
Walk through Bad manually: a sits at offset 0. The next member is int b with alignof == 4, so the compiler rounds offset up to 4, inserting 3 padding bytes. b occupies offsets 4–7. c sits at offset 8. Tail padding brings the total to 12 so that Bad[1] starts at offset 12, which is divisible by 4.
In Good, b occupies offsets 0–3, a at offset 4, c at offset 5, and 2 bytes of tail padding give sizeof == 8.
alignof and alignas
#include <cstddef>
static_assert(alignof(double) == 8);
static_assert(alignof(int) == 4);
struct alignas(64) CacheLine { // force 64-byte boundary for false-share avoidance
int counter;
char _pad[60];
};
alignas(16) float vec[4]; // 16-byte aligned for SSE loads
alignas(N) on a variable or type requires N to be a power of two and at least as large as the natural alignment. Requesting alignas(2) on a double is ill-formed; the natural alignment is 8.
Layout Categories in C++20
C++11 split the legacy "POD" concept into orthogonal properties. C++20 deprecates std::is_pod and relies on two composable traits.
#include <type_traits>
struct Plain {
int x;
double y;
};
static_assert(std::is_trivial_v<Plain>); // trivially constructible + trivially copyable
static_assert(std::is_standard_layout_v<Plain>); // C-compatible layout
static_assert(std::is_trivially_copyable_v<Plain>);
struct WithCtor {
int x;
WithCtor(int v) : x(v) {} // user-provided constructor
};
static_assert(!std::is_trivial_v<WithCtor>); // not trivial: has user constructor
static_assert( std::is_standard_layout_v<WithCtor>); // still standard-layout
static_assert( std::is_trivially_copyable_v<WithCtor>); // still trivially copyable
A type can be standard-layout without being trivial (it has a user-provided constructor) and trivially copyable without being standard-layout (rare; requires a virtual base). The practical rule: if is_trivially_copyable holds, memcpy is safe. If is_standard_layout also holds, offsetof works and C code can read the struct.
memcpy semantics over trivial types
#include <cstring>
Plain src{42, 3.14};
Plain dst;
// Legal: Plain is trivially copyable.
std::memcpy(&dst, &src, sizeof(Plain));
// dst.x == 42, dst.y == 3.14 — guaranteed by [basic.types]/3
For non-trivially-copyable types (e.g., std::string), memcpy produces an object whose copy constructor was never called; the destructor of dst will free garbage memory. The compiler does not diagnose this.
Bit-fields
Bit-fields let you pack integers into fewer bits:
struct Flags {
unsigned int ready : 1;
unsigned int error : 1;
unsigned int code : 6;
}; // commonly 4 bytes (one int unit), layout is implementation-defined
The C++ standard does not specify which bits within a storage unit a bit-field occupies, whether it can straddle unit boundaries, or what the underlying type's size is. Two compilers targeting the same ISA can produce different layouts. Use bit-fields only for space compression where correctness does not depend on exact byte offsets, or wrap them in static_assert checks. Never memcpy a bit-field struct across compiler or architecture boundaries.
Key Result
memcpy Validity for Trivially-Copyable Types
Statement
Copying the bytes of a live object of type to the storage of another object of type via memcpy produces a valid object whose value is equal to the source. Formally, with is_trivially_copyable_v<T>, if src and dst are pointers to T, then after memcpy(dst, src, sizeof(T)), reading *dst is well-defined and yields the same value as *src.
Intuition
A trivially-copyable type has no internal invariants maintained by constructor or destructor logic; its value is fully determined by its bytes. Copying the bytes copies the value.
Proof Sketch
The C++ standard [basic.types]/3 states: "For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a base-class subobject, if the underlying bytes of obj1 are copied into obj2, obj2 shall subsequently hold the same value as obj1."
Why It Matters
This is the foundation for serialization buffers, GPU kernel argument structs, network packet headers, and lock-free queues that copy payloads without invoking constructors. If your struct passes static_assert(std::is_trivially_copyable_v<Msg>), its bytes are its identity.
Failure Mode
The guarantee fails the moment a member with a user-provided copy constructor appears (e.g., std::string, std::vector, or any type holding a raw pointer and a custom destructor). memcpy of std::string copies the pointer to the internal buffer without incrementing any reference count; both the source and destination destructor will attempt to free the same memory.
Common Confusions
sizeof(long) is 8 on all 64-bit platforms
On 64-bit Linux and macOS (LP64), sizeof(long) == 8. On 64-bit Windows with MSVC (LLP64), sizeof(long) == 4. Code that uses long as a 64-bit integer breaks silently when compiled on Windows. The fix is int64_t from <cstdint>, which is 8 bytes on every conforming implementation that provides it.
Adding a constructor breaks memcpy safety
A user-provided constructor makes a type non-trivial (failing is_trivial) but does NOT necessarily make it non-trivially-copyable. A struct with only a user-provided constructor but trivial copy/move/destructor still satisfies is_trivially_copyable. The mistake is concluding that any custom constructor bars memcpy; only custom copy constructors, copy-assignment operators, move counterparts, or destructors matter for trivial-copyability.
Reordering struct members is always safe
Reordering members reduces padding but changes every offset. Any code that relies on hardcoded byte offsets (e.g., a reinterpret_cast to a C struct, a Python struct.unpack format string, or a GPU kernel reading a host-side struct) will silently misread fields. Always update every consumer and add static_assert(offsetof(T, field) == expected) guards when changing layout for performance.
Exercises
Problem
Given the following struct on a 64-bit Linux system:
struct S {
char a;
double b;
int c;
char d;
};
What is sizeof(S)? Draw the byte layout, marking padding bytes explicitly. Then reorder the members to minimize sizeof.
Problem
Write a compile-time check that confirms a struct Packet is safe to send over a socket via write(fd, &pkt, sizeof(pkt)). What two static_assert conditions are necessary, and what runtime check (if any) is still needed?
Problem
What does std::is_trivially_copyable_v<std::pair<int, int>> return on a standard-conforming implementation? What about std::pair<int, std::string>? Explain both answers from first principles without running the compiler.
References
Canonical:
- Stroustrup, A Tour of C++ (3rd ed, 2022), ch. 1–2: overview of fundamental types, sizes, and value semantics in C++20.
- Stroustrup, The C++ Programming Language (4th ed, 2013), §6.2–6.3 (fundamental types and sizes), §8.2 (struct layout and POD): the definitive reference for layout rules and ABI discussion.
- Bryant & O'Hallaron, Computer Systems: A Programmer's Perspective (3rd ed, 2016), §2.1 (information storage, byte ordering, sizes) and §3.9 (heterogeneous data structures, alignment): machine-level view of struct padding with x86-64 assembly evidence.
- ISO/IEC 14882:2020 (C++20 standard), [basic.types] §6.8 and [class.mem] §11.4: normative text for trivially-copyable, standard-layout, and layout rules.
Accessible:
- Meyers, Effective Modern C++ (2014), Item 17 (understand special member function generation): explains which constructors are user-provided vs. defaulted, directly affecting
is_trivial. - cppreference.com, "Object and alignment" and "Type layout" pages: concise tables of guarantees, trait predicates, and
alignas/alignofsyntax with live examples. - Drepper, What Every Programmer Should Know About Memory (2007), §2: detailed treatment of cache-line alignment, false sharing, and why
alignas(64)matters for concurrent data structures.
Next Topics
/computationpath/cpp-pointers-and-references: pointer arithmetic, reference semantics, and how alignment affects pointer casting./computationpath/cpp-object-lifetime: construction, destruction, and the rules governing when objects are live formemcpyand placement-new./computationpath/cpp-move-semantics: how trivial vs. non-trivial move constructors interact with standard containers./computationpath/cpp-cache-and-data-layout: translating struct padding knowledge into cache-line-aware data structure design.