alignas
Controls the alignment requirement of a type or variable declaration, overriding the compiler's default to meet hardware, ABI, or concurrency constraints.
alignassince C++11A declaration specifier that sets the alignment requirement of a type or variable to a specified power-of-two boundary, which must be at least as strict as its natural alignment.
Overview
Every type has a natural alignment β the byte boundary to which the CPU requires (or prefers) its objects to be addressed. alignas lets you strengthen that requirement, forcing a larger boundary than the compiler would choose by default. It cannot be used to weaken alignment; attempting to do so makes the program ill-formed.
Alignment control matters in several concrete scenarios:
- SIMD and vectorisation: SSE/AVX load instructions require 16- or 32-byte aligned data. Misaligned loads produce a fault on strict-alignment ISAs or a measurable performance penalty on x86.
- Cache-line isolation: Two frequently written fields sharing a cache line causes false sharing between threads. Writes from one thread invalidate the entire line for other cores, forcing spurious reloads even when threads never access the same variable.
- Overaligned storage buffers: Reserving raw bytes for placement
newrequires the buffer's alignment to match the target type's. - Hardware-mapped regions: Some peripheral or DMA registers mandate specific alignment for correct bus transactions.
In C++17, ::operator new gained overloads for over-aligned types, so dynamically allocated over-aligned objects are handled correctly without manual allocator wiring. In C++14 and earlier, standard allocation only guarantees alignof(std::max_align_t) β typically 16 bytes.
Syntax
alignas(expression) // integral constant; 0 or a valid power-of-2 alignment
alignas(type-id) // equivalent to alignas(alignof(type-id))
alignas(pack...) // C++11: expands to one alignas per pack element; takes the maxalignas is permitted on:
- A class or struct definition.
- A non-bitfield data member declaration.
- A variable declaration.
It is not permitted on a function parameter or the exception parameter of a catch clause.
When multiple alignas specifiers appear on the same declaration, the effective alignment is the largest non-zero value among them. alignas(0) is always silently ignored.
Weakening natural alignment is ill-formed:
struct alignas(8) S {};
struct alignas(1) U { S s; }; // error: alignas(1) would weaken S's alignmentExamples
Aligning a struct for SIMD
#include <cassert>
struct alignas(32) Vec8f { // 32-byte boundary required for AVX _mm256_load_ps
float data[8];
};
static_assert(alignof(Vec8f) == 32);
static_assert(sizeof(Vec8f) == 32);
void process(const Vec8f* v) {
// Safe to use _mm256_load_ps(v->data) β alignment guaranteed by type
(void)v;
}Aligning individual variables
alignas on a variable overrides alignment for that object only, independent of the type's natural alignment:
#include <cstdint>
alignas(64) int hot_counter = 0; // occupies its own cache line
alignas(alignof(double)) char scratch[8]; // usable as double-aligned storage
// alignof reports the type's alignment, not the variable's.
// hot_counter has type int (alignof == 4), but &hot_counter is 64-byte aligned.
static_assert(alignof(int) == 4);Eliminating false sharing (C++17)
#include <new> // std::hardware_destructive_interference_size β C++17
struct ThreadCounters {
alignas(std::hardware_destructive_interference_size) long writer_count{0};
alignas(std::hardware_destructive_interference_size) long reader_count{0};
};Without alignas, both fields likely share a 64-byte cache line. A write from the writer thread marks the entire line dirty, stalling the reader thread on its next access β even though neither thread ever touches the other's variable.
When targeting C++14 or earlier where the constant is unavailable, use a conservative fixed value:
constexpr std::size_t k_cache_line = 64; // portable conservative assumption
struct ThreadCounters {
alignas(k_cache_line) long writer_count{0};
alignas(k_cache_line) long reader_count{0};
};Aligned storage for placement new
#include <new> // std::launder β C++17
#include <utility>
template <typename T>
class InlineOptional {
alignas(T) unsigned char storage_[sizeof(T)];
bool engaged_ = false;
public:
template <typename... Args>
void emplace(Args&&... args) {
::new (storage_) T(std::forward<Args>(args)...);
engaged_ = true;
}
T& value() {
// std::launder required in C++17+ after placement new through a char buffer
return *std::launder(reinterpret_cast<T*>(storage_));
}
~InlineOptional() {
if (engaged_) value().~T();
}
};alignas(T) guarantees storage_ carries alignof(T) alignment, so the placement new produces a correctly aligned object. Without it the buffer has alignment 1 β undefined behaviour for any type with stricter requirements.
Parameter pack expansion
alignas accepts a type or non-type parameter pack, expanding to one specifier per element with the maximum winning:
#include <algorithm>
template <typename... Ts>
struct AlignedUnion {
alignas(Ts...) unsigned char storage[std::max({sizeof(Ts)...})];
};
using U = AlignedUnion<int, double, char>;
static_assert(alignof(U) == alignof(double)); // double has the strictest alignmentBest Practices
Prefer alignas(T) over hard-coded numbers when matching a type's alignment. If T gains a wider member in a later revision, the specifier stays correct automatically.
Use std::hardware_destructive_interference_size for cache-line separation (C++17). The constant is target-specific; hard-coding 64 is incorrect on architectures with larger lines and may be unnecessarily wasteful on those with smaller ones.
Audit dynamic allocation in pre-C++17 code. In C++14, std::make_unique<Vec8f>() invokes the non-aligned operator new and may return memory that is insufficiently aligned. Either upgrade to C++17 or use aligned_alloc / platform allocators directly.
Verify actual address alignment at runtime during debugging, not via alignof. alignof reports the type's requirement; inspecting reinterpret_cast<uintptr_t>(ptr) % alignment == 0 confirms the live address.
Common Pitfalls
Attempting to weaken alignment. Specifying a smaller alignment than a member or base class requires is ill-formed. The diagnostic may cite an opaque ABI constraint rather than identifying the offending member, making the error non-obvious in deeply nested structures.
alignof does not reflect alignas on a variable.
alignas(64) int x;
static_assert(alignof(decltype(x)) == 4); // true: alignof sees the type, not the var
// runtime address of x is 64-byte aligned despite the aboveCode that reads alignof(T) to decide whether a cast is safe may draw the wrong conclusion when the storage was declared with a stricter alignas.
alignas on function parameters is ignored or ill-formed. The standard does not permit it; some compilers accept it silently and ignore it, others diagnose it. Pass over-aligned objects by pointer or reference:
void bad(alignas(32) Vec8f v); // ill-formed; compiler may not preserve alignment
void good(const Vec8f& v); // reference; alignment of the caller's object is keptOver-aligned types in std::vector before C++17. std::allocator<T> before C++17 calls the unaligned operator new, so std::vector<Vec8f> may contain misaligned elements. In C++17 this is fixed automatically. In earlier standards, supply a custom allocator that calls aligned_alloc.
SIMD Alignment in Depth
Vectorisation intrinsics map directly onto machine instructions that operate on wide registers β 128 bits for SSE, 256 bits for AVX2, and 512 bits for AVX-512. The aligned and unaligned variants of those load/store instructions behave differently across architectures and ISA generations.
Required alignments per ISA
| ISA | Register width | Required alignment |
|---|---|---|
| SSE / AVX-1 | 128 bit | 16 bytes |
| AVX2 | 256 bit | 32 bytes |
| AVX-512 | 512 bit | 64 bytes |
vmovaps vs vmovups on x86
x86 exposes paired aligned and unaligned load instructions. The aligned form (vmovaps, vmovapd) raises a general-protection fault (#GP) at runtime if the operand address is not correctly aligned. The unaligned form (vmovups, vmovupd) never faults. On Intel Nehalem and later, and AMD Bulldozer and later, movaps and movups carry the same throughput and latency when the data happens to be aligned at runtime.
The practical difference shows up at the compiler level: when the intrinsic is _mm256_load_ps (aligned), the compiler can fold the load into a subsequent ALU instruction, saving a register move. With _mm256_loadu_ps (unaligned), the compiler must first materialise the value in a register, adding a separate vmovups. With AVX VEX-encoded instructions this matters less because unaligned memory operands are permitted in ALU instruction encodings β but without AVX, the difference can cost one instruction per operation in tight loops.
On strict-alignment ISAs (ARM without NEON alignment relaxation, many RISC-V implementations), unaligned access causes a hardware exception (SIGBUS) even for general-purpose loads, not just SIMD. On those platforms, misaligned SIMD data is always fatal, never merely slower.
Declaring SIMD arrays with correct alignment
#include <immintrin.h> // AVX2 intrinsics
// AVX2: 8 x float fits in one 256-bit register β must be 32-byte aligned
alignas(32) float avx2_buf[8];
// AVX-512: 16 x float fits in one 512-bit register β must be 64-byte aligned
alignas(64) float avx512_buf[16];
void avx2_example(const float* src) {
__m256 v = _mm256_load_ps(src); // vmovaps: faults if src not 32-byte aligned
__m256 w = _mm256_loadu_ps(src); // vmovups: safe but may cost an extra instruction
(void)v; (void)w;
}
// Static and automatic storage with alignas are always safe:
void process() {
alignas(32) float data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
__m256 v = _mm256_load_ps(data); // guaranteed 32-byte aligned
(void)v;
}Passing these arrays to functions is safe only when the pointer itself is aligned. Annotating a function parameter with alignas is ill-formed; instead rely on the alignment guarantees of the type:
struct alignas(32) Vec8f {
float data[8];
};
// Caller guarantees alignment via the type's alignas β no extra annotation needed
void kernel(const Vec8f& v) {
__m256 reg = _mm256_load_ps(v.data);
(void)reg;
}Runtime alignment check
During debugging, assert that a pointer satisfies its alignment requirement:
#include <cstdint>
#include <cassert>
template <std::size_t Align>
bool is_aligned(const void* ptr) noexcept {
return (reinterpret_cast<std::uintptr_t>(ptr) & (Align - 1)) == 0;
}
alignas(32) float buf[8];
assert(is_aligned<32>(buf)); // always true for static/auto storage declared with alignasstd::hardware_destructive_interference_size (C++17)
Defined in <new>, this constant reports the minimum offset between two objects needed to avoid false sharing due to cache-line sharing:
inline constexpr std::size_t std::hardware_destructive_interference_size; // (1) C++17
inline constexpr std::size_t std::hardware_constructive_interference_size; // (2) C++17- Destructive interference size β the minimum separation between two separately-owned objects that ensures they will not share a cache line. Use this value as the alignment to keep independently-written fields apart.
- Constructive interference size β the maximum combined size of a group of objects that should fit within one cache line to benefit from spatial locality. Use this to assert that a "hot" struct fits on a single line.
Both constants are guaranteed to be at least alignof(std::max_align_t) and are target-specific: 64 on x86-64 and aarch64, but processors with larger lines (e.g., some POWER9 configurations) will report larger values.
Why prefer the portable constant over alignas(64)
Hard-coding alignas(64) carries two risks:
- Undershoot: A processor with 128-byte lines still places two 64-byte-aligned objects on the same physical line, reintroducing false sharing.
- Overshoot: A processor with 32-byte lines wastes memory by over-padding every padded field.
std::hardware_destructive_interference_size is computed by the compiler from target knowledge, so the binary is automatically correct for the platform it was compiled for.
Portability guard
Not all standard libraries shipped the constant immediately after C++17 was standardised. Use the feature-test macro to fall back gracefully:
#include <new>
#ifdef __cpp_lib_hardware_interference_size
using std::hardware_destructive_interference_size;
using std::hardware_constructive_interference_size;
#else
// 64 bytes is the correct value for x86-64 and aarch64 as of 2026
constexpr std::size_t hardware_destructive_interference_size = 64;
constexpr std::size_t hardware_constructive_interference_size = 64;
#endifFalse Sharing: A Worked Example
False sharing occurs when two threads write to distinct variables that happen to share the same cache line. The MESI coherence protocol forces the writing core to obtain exclusive ownership of the entire line before committing a store β even though the other thread's variable is untouched. The result is that every write from one thread invalidates the cached copy held by the other, causing the line to bounce continuously between cores.
The broken layout
#include <atomic>
#include <thread>
struct SharedCounters {
// sizeof(std::atomic<int>) == 4; both counters fit easily in one 64-byte cache line
std::atomic<int> producer_count{0};
std::atomic<int> consumer_count{0};
};
SharedCounters g_counters;
void producer() {
for (int i = 0; i < 10'000'000; ++i)
g_counters.producer_count.fetch_add(1, std::memory_order_relaxed);
}
void consumer() {
for (int i = 0; i < 10'000'000; ++i)
g_counters.consumer_count.fetch_add(1, std::memory_order_relaxed);
}
// Both threads increment independent counters but fight over the same cache line.
// Measured overhead: 3β6x slower than the isolated version on typical x86-64 hardware.producer_count and consumer_count are logically independent, but they sit at adjacent 4-byte offsets. Every store by the producer thread invalidates the cache line for the consumer thread's core, and vice versa. The coherence traffic makes both threads wait for exclusive line ownership on every iteration.
The fix: isolate each field onto its own cache line
#include <atomic>
#include <new> // hardware_destructive_interference_size
#ifdef __cpp_lib_hardware_interference_size
using std::hardware_destructive_interference_size;
#else
constexpr std::size_t hardware_destructive_interference_size = 64;
#endif
struct alignas(hardware_destructive_interference_size) PaddedCounter {
std::atomic<int> value{0};
};
// Each PaddedCounter occupies exactly one cache line; no cross-thread invalidation.
PaddedCounter g_producer;
PaddedCounter g_consumer;
void producer() {
for (int i = 0; i < 10'000'000; ++i)
g_producer.value.fetch_add(1, std::memory_order_relaxed);
}
void consumer() {
for (int i = 0; i < 10'000'000; ++i)
g_consumer.value.fetch_add(1, std::memory_order_relaxed);
}Benchmarks from the cppreference example show this pattern yields roughly a 6x throughput improvement over the false-sharing layout for high-contention atomic increments on a typical multi-core x86-64 machine.
Placing multiple fields in a single struct
When both fields belong to the same struct, use alignas on each member to enforce the line boundary between them:
struct ThreadCounters {
alignas(hardware_destructive_interference_size) std::atomic<int> writes{0};
alignas(hardware_destructive_interference_size) std::atomic<int> reads{0};
};The alignas on reads forces padding between writes and itself, ensuring they land on separate cache lines. sizeof(ThreadCounters) will be 2 * hardware_destructive_interference_size.
For a pair of fields that should share a line (true sharing), use the constructive constant and assert that the struct fits:
struct HotPair {
std::atomic<int> value{0};
int sequence{0};
};
// Assert the hot pair fits in one line so the compiler will not split it
static_assert(sizeof(HotPair) <= hardware_constructive_interference_size);Dynamic Allocation of Over-Aligned Types
The C++14 gap
Before C++17, ::operator new was only obligated to return memory aligned to alignof(std::max_align_t) β typically 16 bytes. Dynamically allocating a type with a stricter alignas produced UB because the obtained pointer was not sufficiently aligned:
// C++14: pVec may be only 16-byte aligned β UB when Vec8f requires 32
auto* pVec = new Vec8f;
// The stack object is fine β the compiler honours alignas for automatic storage:
Vec8f stack_vec; // correctly 32-byte alignedGCC 4.8 in C++14 mode, for instance, would return a new Vec8f pointer that was only 16-byte aligned for a alignas(32) type.
C++17: operator new(size_t, align_val_t)
C++17 added overloads of the global allocation functions that accept an std::align_val_t alignment argument:
void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);
// matching delete overloads ...The compiler selects these overloads automatically whenever alignof(T) > __STDCPP_DEFAULT_NEW_ALIGNMENT__ (16 on GCC, Clang, and MSVC). No source change is required β existing new Vec8f expressions are silently upgraded to the aligned overload.
std::vector, std::make_unique, and all standard containers also benefit: they internally call operator new and therefore pick up the aligned overload in C++17 mode.
#include <vector>
#include <cassert>
#include <cstdint>
struct alignas(32) Vec8f {
float data[8];
};
int main() {
// C++17: std::vector<Vec8f> internally calls the aligned operator new
std::vector<Vec8f> vecs(4);
assert(reinterpret_cast<std::uintptr_t>(vecs.data()) % 32 == 0);
// Scalar new also uses the aligned overload automatically
auto* p = new Vec8f;
assert(reinterpret_cast<std::uintptr_t>(p) % 32 == 0);
delete p;
}std::aligned_alloc for C-style allocation
When interfacing with C APIs or custom memory pools, use std::aligned_alloc (C11 / C++17):
#include <cstdlib>
#include <new>
// alignment must be a power of two; size must be a multiple of alignment
void* raw = std::aligned_alloc(32, 32 * 16); // 16 Vec8f-sized slots, 32-byte aligned
if (!raw) throw std::bad_alloc{};
auto* vecs = static_cast<Vec8f*>(raw);
// ... use vecs ...
std::free(raw); // release with std::free β not deleteNote: std::aligned_alloc requires size to be a multiple of alignment. On Windows, the POSIX posix_memalign equivalent is _aligned_malloc(size, alignment), freed with _aligned_free.
Custom allocator for pre-C++17 codebases
If you must support C++14 with over-aligned types in standard containers, supply an allocator that delegates to the platform-specific aligned allocation function:
#include <cstdlib>
#include <memory>
#include <new>
template <typename T, std::size_t Align = alignof(T)>
struct AlignedAllocator {
using value_type = T;
T* allocate(std::size_t n) {
if (n > std::size_t(-1) / sizeof(T)) throw std::bad_alloc{};
void* p = nullptr;
#if defined(_WIN32)
p = _aligned_malloc(n * sizeof(T), Align);
#else
if (::posix_memalign(&p, Align, n * sizeof(T)) != 0) p = nullptr;
#endif
if (!p) throw std::bad_alloc{};
return static_cast<T*>(p);
}
void deallocate(T* p, std::size_t) noexcept {
#if defined(_WIN32)
_aligned_free(p);
#else
std::free(p);
#endif
}
template <typename U>
struct rebind {
using other = AlignedAllocator<U, Align>;
};
};
// Usage in C++14:
// std::vector<Vec8f, AlignedAllocator<Vec8f, 32>> avx_data;In C++17, AlignedAllocator is no longer necessary for this use-case β std::allocator<T> calls the aligned operator new when alignof(T) > __STDCPP_DEFAULT_NEW_ALIGNMENT__. Keep a custom allocator only when you need additional control over the allocation source (e.g., a memory pool or huge pages).
See Also
alignofβ queries the alignment requirement of a type at compile time- Attributes β the broader set of
[[...]]declaration modifiers std::alignβ adjusts a pointer to meet an alignment requirement within a raw bufferstd::hardware_destructive_interference_sizeβ portable cache-line size constant (C++17)