sizeof, alignof, alignas
sizeof returns byte sizes, alignof queries alignment requirements, alignas enforces alignment — the toolkit for precise memory layout control in C++.
sizeofsince C++98sizeof is a compile-time operator returning the byte size of a type or expression; alignof (C++11) returns its alignment requirement; alignas (C++11) over-aligns a type or variable to a stricter boundary.
Overview
Every object has two independent layout properties: size (how many bytes it occupies) and alignment (the address granularity it requires). sizeof and alignof observe these properties at compile time; alignas enforces them. All three leave zero runtime trace.
This vocabulary is essential for SIMD kernels, network serialization, lock-free data structures, and cache-friendly containers.
sizeof Operator
sizeof never evaluates its operand — side effects are silently suppressed. The return type is std::size_t, an unsigned type from <cstddef>.
int i = 0;
sizeof(i++); // 4; i remains 0 — increment never executes
// Applied to a type, parentheses are required.
// Applied to an expression, they are optional.
sizeof(int) // 4
sizeof i // 4 — identicalFundamental types (typical 64-bit platform):
sizeof(char) // 1 — always 1 by the standard's definition
sizeof(short) // 2
sizeof(int) // 4
sizeof(long) // 4 (MSVC/Windows LLP64), 8 (GCC/Clang LP64)
sizeof(long long) // 8
sizeof(float) // 4
sizeof(double) // 8
sizeof(long double) // 8 (MSVC), 12 or 16 (GCC/Clang x86)
sizeof(void*) // 8 (64-bit), 4 (32-bit)
sizeof(std::nullptr_t) // implementation-defined; == sizeof(void*) on all common platforms // C++11Arrays, strings, and containers:
int arr[10];
sizeof(arr) // 40
sizeof(arr) / sizeof(arr[0]) // 10 — portable element-count idiom
sizeof("hello") // 6 — char[6], null terminator included
sizeof('\n') // 1 — char literal; sizeof(wchar_t) for L'\n'
sizeof(L"hi") // sizeof(wchar_t)*3: 6 on Windows (2-byte wchar_t), 12 on Linux (4-byte)
// std::vector's sizeof is the fixed object size, not the heap allocation
std::vector<int> v{1, 2, 3};
sizeof(v) // 24 (three pointer-/size-fields) — NOT v.size() * 4Parameter packs (C++11):
template<typename... Ts>
constexpr std::size_t type_count() { return sizeof...(Ts); }
static_assert(type_count<int, double, char>() == 3);Compile-time assertions:
static_assert(sizeof(float) == 4, "unexpected IEEE 754 layout");
// Safe-cast guard — ensures source fits in destination
template<class To, class From>
To narrow_cast(From v) {
static_assert(sizeof(From) <= sizeof(To));
return static_cast<To>(v);
}sizeof is ill-formed on incomplete types, functions, bit-field members, and void.
Struct Padding and Layout
The compiler inserts padding so each member sits at an address that is a multiple of its alignment requirement. The struct's total size is then rounded up to a multiple of the most-aligned member's alignment so arrays of structs remain aligned.
struct Bad {
char a; // offset 0, size 1
// 3 bytes padding
int b; // offset 4, size 4
char c; // offset 8, size 1
// 3 bytes padding
int d; // offset 12, size 4
}; // sizeof(Bad) == 16
struct Good { // same four fields, sorted largest-to-smallest
int b; // offset 0, size 4
int d; // offset 4, size 4
char a; // offset 8, size 1
char c; // offset 9, size 1
// 2 bytes padding (struct size must be multiple of 4)
}; // sizeof(Good) == 12
#include <cstddef>
static_assert(offsetof(Good, b) == 0);
static_assert(offsetof(Good, a) == 8);offsetof is only well-defined on standard-layout types. Using it on types with virtual functions, virtual bases, or non-trivial constructors is undefined behavior.
alignof Operator (C++11)
alignof(T) returns the minimum byte boundary an object of type T must be placed on. The result is always a power of two and equals the alignment of T's most strictly aligned member.
alignof(char) // 1
alignof(short) // 2
alignof(int) // 4
alignof(double) // 8
alignof(void*) // 8 (64-bit)
struct Widget { int i; double d; };
alignof(Widget) // 8 — dictated by double
sizeof(Widget) // 16 — 4 + 4 padding + 8
// Equivalent type trait
#include <type_traits>
static_assert(std::alignment_of_v<double> == 8); // C++17 _v helper
// Maximum alignment any fundamental type requires on this platform
alignof(std::max_align_t) // typically 16 on x86-64 // C++11alignof accepts only type names, not expressions (unlike sizeof).
alignas Specifier (C++11)
alignas(N) over-aligns a type or variable to at least N bytes. N must be a power of two and must not be less than the type's natural alignment — under-alignment is ill-formed.
// SIMD: AVX/AVX2 aligned loads require 32-byte alignment
alignas(32) float simd_buf[8];
// Prevent false sharing between threads
struct alignas(64) PaddedCounter {
std::atomic<int64_t> value{0};
// struct padded to 64 bytes; next PaddedCounter starts on a fresh cache line
};
// Type-level alignas
struct alignas(16) Vec4 {
float x, y, z, w;
};
static_assert(alignof(Vec4) == 16);
static_assert(sizeof(Vec4) == 16);
// Under-alignment is ill-formed — rejected at compile time
// struct alignas(1) Bad { double d; }; // error: 1 < alignof(double) == 8C++17 aligned operator new: Prior to C++17, ::operator new only guaranteed alignof(std::max_align_t) alignment (typically 16 bytes). Types with stricter requirements required placement new into manually aligned storage. C++17 added overloads taking std::align_val_t, so heap allocation now respects alignas:
auto* v = new Vec4; // guaranteed 16-byte aligned since C++17
// uses operator new(sizeof(Vec4), std::align_val_t{16})Aligned Storage
When you need to reserve storage for a type without constructing it — as in std::optional, std::variant, or object pools — use alignas(T) std::byte[sizeof(T)] (C++17):
#include <cstddef> // std::byte (C++17)
#include <memory> // std::launder (C++17)
template<typename T>
class ManualStorage {
alignas(T) std::byte buf_[sizeof(T)];
bool live_ = false;
public:
template<typename... Args>
T& emplace(Args&&... args) {
if (live_) destroy();
::new(buf_) T(std::forward<Args>(args)...);
live_ = true;
return get();
}
// std::launder required: new object was created in existing storage;
// launder resets pointer provenance so the compiler doesn't alias-optimize it away
T& get() { return *std::launder(reinterpret_cast<T*>(buf_)); } // C++17
void destroy() { if (live_) { get().~T(); live_ = false; } }
~ManualStorage() { destroy(); }
};std::aligned_storage<Size, Align> (C++11) was the original facility but was deprecated in C++23 as error-prone. The alignas(T) std::byte[sizeof(T)] idiom is its replacement.
Hardware Alignment
Cache-line padding eliminates false sharing between threads on separate cache lines:
#include <new> // std::hardware_destructive_interference_size (C++17)
#ifdef __cpp_lib_hardware_interference_size
constexpr std::size_t kCacheLine = std::hardware_destructive_interference_size;
#else
constexpr std::size_t kCacheLine = 64; // safe fallback for x86-64/ARM64
#endif
struct alignas(kCacheLine) ShardedCounter {
std::atomic<int64_t> value{0};
};
// Each element occupies its own cache line — no false sharing
std::vector<ShardedCounter> counters(std::thread::hardware_concurrency());SIMD aligned loads: _mm256_load_ps / _mm512_load_ps fault or silently corrupt on misaligned addresses:
#include <immintrin.h>
alignas(32) float a[8] = {1,2,3,4,5,6,7,8};
alignas(32) float b[8] = {8,7,6,5,4,3,2,1};
__m256 va = _mm256_load_ps(a); // 32-byte-aligned: safe
__m256 vb = _mm256_load_ps(b);
__m256 vc = _mm256_add_ps(va, vb);
// _mm256_loadu_ps accepts unaligned pointers — mildly slower on older µarchsstd::align (C++11) adjusts a pointer to the next aligned address within a caller-managed buffer:
#include <memory>
void* ptr = raw_buf;
std::size_t space = buf_size;
void* aligned = std::align(alignof(T), sizeof(T), ptr, space);
// aligned: first T-aligned address in [ptr, ptr+space); space reduced accordingly
// returns nullptr if the buffer is too smallPacked Structs
Remove padding for network and file formats where exact byte layout is contractual:
#pragma pack(push, 1)
struct EthernetHeader {
uint8_t dst[6];
uint8_t src[6];
uint16_t ethertype;
}; // sizeof == 14, not 16
#pragma pack(pop)
// GCC/Clang attribute form
struct __attribute__((packed)) IPv4Header {
uint8_t version_ihl;
uint8_t dscp_ecn;
uint16_t total_length;
uint16_t identification;
uint16_t flags_fragment;
uint8_t ttl;
uint8_t protocol;
uint16_t checksum;
uint32_t src_ip;
uint32_t dst_ip;
}; // sizeof == 20Packed pointers may be misaligned. Dereferencing a misaligned multi-byte pointer is undefined behavior on ARM, MIPS, and most non-x86 architectures. Use memcpy to safely read multi-byte fields:
uint32_t read_ip(const IPv4Header* h) {
uint32_t ip;
std::memcpy(&ip, &h->src_ip, sizeof(ip)); // copies bytes; handles misalignment
return ip;
}Best Practices
- Sort struct members largest-to-smallest to reduce padding without compiler pragmas.
- Prefer
alignas(T) std::byte[sizeof(T)]over deprecatedstd::aligned_storage_t(deprecated C++23). - Use
std::hardware_destructive_interference_size(C++17) with a fallback constant rather than hardcoding 64 for cache-line padding. - When using
newwith over-aligned types (alignas(N)where N >alignof(std::max_align_t)), ensure C++17 aligned allocation is active or fall back to placement new. - Always call
std::launderwhen accessing objects through a reinterpreted pointer after placement new.
Common Pitfalls
Array decay in function parameters — sizeof sees a pointer:
void process(int arr[10]) {
sizeof(arr); // 8 (pointer size), not 40 — the array decayed
}
template<std::size_t N>
void process(int (&arr)[N]) {
sizeof(arr); // 4*N — correct
}sizeof on string literals includes the null terminator:
sizeof("hi") // 3, not 2 — char[3]: {'h','i','\0'}
strlen("hi") // 2 — the logical string lengthsizeof a class template reports fixed object size, not logical size:
sizeof(std::string) // 32 (typical SSO layout) — not the character count
sizeof(std::vector<int>) // 24 (three size_t fields)
sizeof(std::unordered_map<int,int>) // 56 — fixed; use .size() for element countsizeof on bit-field members is ill-formed:
struct Flags { uint8_t a : 3; uint8_t b : 5; };
Flags f;
// sizeof(f.a) — ill-formed; bit-field members have no addressable sizePacked struct pointer dereference on non-x86:
#pragma pack(push, 1)
struct Header { uint32_t value; }; // may be placed at an odd address
#pragma pack(pop)
Header h;
uint32_t* p = &h.value; // potentially misaligned
*p = 42; // UB on ARM without hardware unaligned-access supportalignas and Aligned Storage Patterns
Over-aligning a type is the portable, idiomatic way to satisfy SIMD and lock-free requirements. alignas can appear on a struct definition, a local variable, a namespace-scope variable, or a static member — the same keyword covers all of them.
// C++11
#include <cstddef>
#include <new>
// AVX-512 requires 64-byte alignment for aligned loads
alignas(64) float avx512_buf[16];
// Per-type alignment: every Vec4 object, wherever allocated, gets 16-byte alignment
struct alignas(16) Vec4 {
float x, y, z, w;
};
// Stack allocation respects alignas directly
Vec4 local{1.f, 2.f, 3.f, 4.f};
static_assert(alignof(Vec4) == 16);std::aligned_storage_t<Size, Align> (C++11) provided raw aligned buffers but was deprecated in C++23 because it required a reinterpret_cast to be useful and the resulting pointer provenance was easy to mishandle. The replacement is a plain alignas array of std::byte:
// C++17
#include <cstddef> // std::byte
#include <memory> // std::launder
// Before C++23 deprecation — avoid in new code
// std::aligned_storage_t<sizeof(Vec4), alignof(Vec4)> old_buf;
// Correct modern form: alignment is expressed directly, intent is unambiguous
alignas(Vec4) std::byte buf[sizeof(Vec4)];
::new(buf) Vec4{0.f, 0.f, 0.f, 1.f};
Vec4* v = std::launder(reinterpret_cast<Vec4*>(buf));
v->~Vec4();std::launder is mandatory after placement new into existing storage: without it the compiler may cache a stale read of the pre-construction bytes through the original buf pointer.
std::hardware_destructive_interference_size (C++17)
False sharing — two threads writing to different variables that happen to share a cache line — is one of the most insidious performance cliffs in concurrent code. C++17 added two constants to <new> that encode the hardware's cache-line geometry without hardcoding platform magic numbers.
// C++17
#include <new>
#include <atomic>
#include <vector>
#include <thread>
#ifdef __cpp_lib_hardware_interference_size
// Minimum offset between two objects to guarantee no false sharing
constexpr std::size_t kDestructive =
std::hardware_destructive_interference_size; // typically 64
// Maximum size of contiguous memory accessed together without false sharing
constexpr std::size_t kConstructive =
std::hardware_constructive_interference_size; // typically 64
#else
constexpr std::size_t kDestructive = 64;
constexpr std::size_t kConstructive = 64;
#endif
// Each counter owns exactly one cache line — threads never ping-pong each other's lines
struct alignas(kDestructive) IsolatedCounter {
std::atomic<long> value{0};
// Remaining bytes in the cache line are implicit padding from alignas
};
static_assert(sizeof(IsolatedCounter) <= kDestructive,
"counter spills into a second cache line");
std::vector<IsolatedCounter> counters(std::thread::hardware_concurrency());The #ifdef __cpp_lib_hardware_interference_size guard is necessary because GCC only conditionally exposes these constants — some versions emit a deprecation warning when they are read in a constexpr context without the macro check.
sizeof in Template Metaprogramming
sizeof...(pack) is the only compile-time way to count the elements of a parameter pack without instantiating any templates. It works on both type packs and value packs and returns a std::size_t constant expression.
// C++11
template<typename... Ts>
struct TypeList {
static constexpr std::size_t size = sizeof...(Ts);
};
static_assert(TypeList<int, double, char, void>::size == 4);
// Count a function-argument pack at the call site
template<typename... Args>
void log(Args&&... args) {
static_assert(sizeof...(Args) > 0, "log() requires at least one argument");
// ... format and write args
}sizeof on individual types feeds if constexpr branches, enabling zero-overhead dispatch without runtime conditionals:
// C++17
#include <cstdint>
template<typename T>
auto promote(T v) {
if constexpr (sizeof(T) < sizeof(int)) {
return static_cast<int>(v); // widen small integer types
} else if constexpr (sizeof(T) == sizeof(int)) {
return static_cast<long>(v); // promote int → long
} else {
return v; // already wide enough
}
}Combining both, you can build compile-time type-size rankings that drive serialization or SIMD lane-width selection:
// C++20
#include <concepts>
#include <cstddef>
template<typename T>
concept FitsInRegister = sizeof(T) <= sizeof(void*);
template<FitsInRegister T>
void pass_by_value(T x) { /* ... */ }
template<typename T>
requires (!FitsInRegister<T>)
void pass_by_value(const T& x) { /* ... */ }Struct Layout Inspection with offsetof
offsetof(Type, member) is the compile-time counterpart to pointer arithmetic — it yields the byte distance from the start of a struct to a named member. It is defined in <cstddef> and returns a std::size_t constant expression on standard-layout types.
// C++11
#include <cstddef>
#include <cstdint>
struct Packet {
uint32_t sequence; // offset 0
uint16_t length; // offset 4
uint16_t checksum; // offset 6
uint8_t payload[8]; // offset 8
}; // sizeof == 16, no padding
static_assert(offsetof(Packet, sequence) == 0);
static_assert(offsetof(Packet, length) == 4);
static_assert(offsetof(Packet, checksum) == 6);
static_assert(offsetof(Packet, payload) == 8);
static_assert(sizeof(Packet) == 16);offsetof is strictly defined only for standard-layout types: no virtual functions, no virtual bases, no non-standard-layout base classes, and no non-static data members of non-standard-layout type. Applying it to a non-standard-layout type is conditionally supported — GCC and Clang accept it with a warning; MSVC may reject it.
// C++17
#include <type_traits>
// Guard offsetof use with a static_assert
struct Record {
int id;
double value;
char tag;
};
static_assert(std::is_standard_layout_v<Record>,
"offsetof is only well-defined on standard-layout types");
constexpr std::size_t value_offset = offsetof(Record, value);
// value_offset == 8 on any platform where int is 4 bytes and double is 8 bytesSome compilers expose __builtin_offsetof as a lower-level primitive that avoids the macro expansion of the standard offsetof and works slightly earlier in template instantiation. It is not portable but useful when writing compiler-agnostic wrappers: detect it with #ifdef __GNUC__ or #ifdef __clang__.
See Also
offsetof— compile-time byte offset of a struct member- Struct layout and standard-layout types
std::optional— uses aligned storage internallystd::variant— aligned storage over a discriminated type set- Move semantics and object lifetime — placement new and manual destruction