What's New in C++26
"C++26 feature guide: static reflection, contracts, std::execution, std::simd, pack indexing, erroneous behaviour, and library additions including std::inplace_vector."
C++26since C++26C++26 standardises static reflection, contracts, structured concurrency via std::execution, portable SIMD via std::simd, and pack indexing β collectively the largest expansion to C++ metaprogramming and correctness tooling since C++11.
Overview
C++26 is a major release. Its headline features β static reflection (P2996), contracts (P2900), std::execution (P2300), and std::simd (P1928) β each represent years of committee work. Compiler support is still maturing; check your vendor's C++26 status page.
g++ -std=c++26 # GCC 15+ (reflection experimental; most others supported)
clang++ -std=c++26 # Clang 19+ (partial; reflection via -freflection)Static Reflection (P2996)
The reflection operator ^ produces a compile-time std::meta::info value describing a named entity. The splice operator [: :] reconstructs the entity from that value. Together they allow structural introspection without macros, external tools, or hand-maintained registries.
#include <meta>
struct Employee {
std::string name;
int id;
double salary;
};
// Count non-static data members at compile time
consteval auto field_count(std::meta::info type) -> std::size_t {
return std::meta::nonstatic_data_members_of(type).size();
}
static_assert(field_count(^Employee) == 3);
// Generic CSV serialiser β no macros, no code generation
template <typename T>
std::string to_csv(const T& obj) {
std::string row;
template for (constexpr auto M : std::meta::nonstatic_data_members_of(^T)) {
if (!row.empty()) row += ',';
row += std::format("{}", obj.[:M:]); // splice: access member by reflection
}
return row;
}
// to_csv(Employee{"Alice", 42, 95000.0}) -> "Alice,42,95000"
// Enum-to-string β replaces hand-written switch tables
template <typename E>
requires std::is_enum_v<E>
std::string_view enum_name(E value) {
template for (constexpr auto enumerator : std::meta::enumerators_of(^E)) {
if ([:enumerator:] == value)
return std::meta::name_of(enumerator);
}
return "<unknown>";
}
enum class Direction { North, South, East, West };
// enum_name(Direction::East) -> "East"^T differs fundamentally from typeid: it is a compile-time constant carrying the full structural description β member names, types, offsets, function signatures β not a runtime RTTI token. Filter members by accessibility using std::meta::is_public(M) before exposing them through serialisation or reflection APIs.
Contracts (P2900)
Contracts embed preconditions, postconditions, and inline assertions directly in function declarations. They are checked at runtime but configurable to zero overhead.
// pre() β caller guarantee; post() β result guarantee (named result variable)
double safe_divide(double num, double den)
pre(den != 0.0)
post(result: std::isfinite(result))
{
return num / den;
}
// Inline assertion β checked at the point of call
void compress(std::span<const std::byte> input, std::span<std::byte> output)
pre(output.size() >= input.size())
{
contract_assert(input.size() < (1ULL << 32)); // algorithm limit
// ...
}Violation handling is set per translation unit (or globally via build flags):
| Mode | Behaviour |
|---|---|
ignore | Checks elided entirely β zero overhead |
observe | Check fires, violation handler called, execution continues |
enforce | Check fires, violation handler called, std::terminate() (default) |
quick_enforce | Minimal-overhead check, immediate terminate without handler |
Contracts replace ad-hoc assert() patterns with standardised, toolable semantics. Static analysers and sanitisers can act on pre() / post() annotations even in ignore mode, where raw assert() macros are invisible.
std::execution β Structured Concurrency (P2300)
std::execution standardises the sender/receiver model from the stdexec reference implementation. Work is expressed as composable pipelines of senders β lazy descriptions of work β connected by adaptors. Nothing executes until a receiver is attached.
#include <execution>
namespace ex = std::execution;
// Synchronous wait on a simple sender chain
auto v = std::this_thread::sync_wait(
ex::just(42)
| ex::then([](int x) { return x * x; })
| ex::then([](int x) { return std::to_string(x); })
).value();
// v == "1764"
// Run computation on a thread pool; block caller until complete
auto result = std::this_thread::sync_wait(
ex::on(pool.get_scheduler(),
ex::just(large_matrix) | ex::then(matrix_multiply)
)
).value();
// Parallel fan-out β all three tasks run concurrently
auto [a, b, c] = std::this_thread::sync_wait(
ex::when_all(
ex::on(pool.get_scheduler(), ex::just(data1) | ex::then(process)),
ex::on(pool.get_scheduler(), ex::just(data2) | ex::then(process)),
ex::on(pool.get_scheduler(), ex::just(data3) | ex::then(process))
)
).value();Errors propagate through the pipeline as values, not exceptions. Unlike std::async (C++11), there is no hidden future state and no implicit thread creation β scheduler policy is explicit at every hop.
std::simd β Portable SIMD (P1928)
std::simd<T> is a fixed-width vector type backed by the target's native SIMD registers. The default ABI uses the widest available ISA. Operations map directly to vector instructions with no runtime overhead over hand-written intrinsics.
#include <simd>
// Vectorised dot product with scalar tail
float dot(std::span<const float> a, std::span<const float> b) {
using V = std::simd<float>;
V acc{0.0f};
std::size_t i = 0;
for (; i + V::size() <= a.size(); i += V::size()) {
V va(a.data() + i, std::simd_flag_default);
V vb(b.data() + i, std::simd_flag_default);
acc += va * vb;
}
float result = std::reduce(acc); // horizontal add β single instruction on AVX
for (; i < a.size(); ++i) // scalar remainder
result += a[i] * b[i];
return result;
}
// Masked clamp β no branching
void clamp_inplace(std::span<float> v, float lo, float hi) {
using V = std::simd<float>;
for (std::size_t i = 0; i + V::size() <= v.size(); i += V::size()) {
V x(v.data() + i, std::simd_flag_default);
x = std::max(std::min(x, V{hi}), V{lo});
x.copy_to(v.data() + i, std::simd_flag_default);
}
}std::simd replaces platform-specific intrinsics (_mm256_β¦, NEON) with portable abstractions. The same source file compiles to SSE2 on a baseline x86-64 target and AVX-512 with -mavx512f β no #ifdef required.
Pack Indexing (P2662)
Direct subscript access into parameter packs. Previously achievable only via std::tuple or recursive templates.
// Type at index I
template <std::size_t I, typename... Ts>
using type_at = Ts...[I]; // C++26
// Value at index I
template <std::size_t I, typename... Args>
decltype(auto) nth(Args&&... args) {
return std::forward<Args...[I]>(args...[I]);
}
nth<2>(10, 3.14, std::string{"hello"}, true); // "hello"
// Last element without recursive unwinding
template <typename... Ts>
auto last(Ts... args) { return args...[sizeof...(Ts) - 1]; }Out-of-bounds indexing with a constant expression I is ill-formed β diagnosed at compile time, not UB.
Erroneous Behaviour (P2795)
A new behavioural category between undefined behaviour (anything goes) and well-defined. The motivating case is reading uninitialized variables:
int x;
int y = x; // C++26: erroneous behaviour
// β result is unspecified but NOT undefined behaviour
// β compiler has no licence to remove surrounding code
// β implementations encouraged to trap or produce a fixed sentinel valueUnder prior standards, reading uninitialized variables was UB, allowing optimisers to eliminate adjacent safety checks on the assumption the path was unreachable. Erroneous behaviour constrains that: the program is wrong, but its observable effects are bounded.
Other Language Features
= delete with diagnostic message (P2573):
template <typename T>
void process(T) = delete("process() requires a serialisable type; implement to_bytes(T)");The message surfaces directly in the compiler diagnostic, eliminating the need for static_assert workarounds.
#embed β compile-time binary inclusion (P1967):
// Embed a binary file as a sequence of integer constant expressions
constexpr unsigned char shader_src[] = {
#embed "shaders/default.glsl"
, '\0' // #embed does NOT append a null terminator β add explicitly if needed
};Replaces xxd-generated arrays and platform-specific linker object tricks.
constexpr math functions (P1383): std::sqrt, std::sin, std::cos, and most <cmath> functions become constexpr in C++26:
consteval double hypotenuse(double a, double b) {
return std::sqrt(a * a + b * b); // std::sqrt is constexpr since C++26
}
static_assert(hypotenuse(3.0, 4.0) == 5.0);Library Additions
std::inplace_vector<T, N> (P0843) β fixed-capacity vector with inline storage, no heap:
#include <inplace_vector>
std::inplace_vector<int, 8> buf; // sizeof(buf) == 8*sizeof(int) + sizeof(size_t)
buf.push_back(1); // throws std::bad_alloc when full
buf.unchecked_push_back(2); // UB when full β use only when capacity is provenIdeal for hot-path small collections where heap allocation is unacceptable.
std::indirect<T> and std::polymorphic<T> (P3019) β value semantics for heap objects:
#include <indirect>
// Deep-copying smart pointer: solves "Rule of Zero with heap members"
struct Tree {
std::indirect<Tree> left, right; // copy copies the entire subtree
int value;
};
// Value-semantic polymorphism: copies the derived object, not a sliced base
std::polymorphic<Shape> s{std::in_place_type<Circle>, 5.0};
auto s2 = s; // copies the Circle correctly
s2->scale(2.0); // independent from sBest Practices
- Contracts over
assert(): Contract violations integrate with the build system's violation mode and are visible to static analysers inignoremode β rawassert()is not. - Reflection for structural patterns: Use static reflection for serialisers, mappers, and enum utilities. Anything previously requiring X-macros or external code generators is a candidate.
- Always write a scalar SIMD tail: Forgetting the remainder loop after a
std::simdloop is a correctness bug, not a performance omission. std::inplace_vectorfor bounded hot paths: Eliminates heap allocation for small fixed-bound collections; useunchecked_push_backonly after proving capacity bounds statically.- Senders are lazy: A
std::executionpipeline does nothing untilsync_waitor a receiver is attached. Building a pipeline and discarding it schedules nothing.
Common Pitfalls
- Reflection sees private members:
std::meta::nonstatic_data_members_ofincludes private members. Filter withstd::meta::is_public(M)before exposing them through public APIs. - Contract mode mismatch across TUs: Mixing translation units compiled with different contract modes (
enforcevsignore) in a single binary can produce surprising diagnostics or silent skips. Pin the mode globally via the build system. #embedand null termination: Embedded data has no implicit null terminator. Passing the array to string functions without appending'\0'is a buffer overread.std::simdwidth varies by target:std::simd<float>is 4-wide on SSE2 and 8-wide on AVX2. Code that assumes a specific width β e.g., hardcoded loop strides β breaks silently across targets.- Pack index must be a constant expression:
args...[I]requiresIto be known at compile time. Wrapping it in a runtime branch computes the index as a constant, not selects among packs at runtime.
Migration Checklist
β Replace X-macro enum-to-string tables β static reflection + enumerators_of
β Replace assert() / throw_if_not() β contracts pre()/post()/contract_assert()
β Replace xxd-generated binary arrays β #embed
β Replace platform SIMD intrinsics in new code β std::simd
β Replace pack-access via std::tuple β pack indexing args...[I]
β Replace std::vector for fixed-capacity hot paths β std::inplace_vector
β Replace raw thread pools for structured async β std::executionCompiler Support Status (2026)
| Feature | GCC 15 | Clang 19 | MSVC 19.43 |
|---|---|---|---|
| Pack indexing | Yes | Yes | Yes |
= delete message | Yes | Yes | Yes |
#embed | Yes | Yes | Partial |
| Erroneous behaviour | Yes | Yes | Partial |
constexpr <cmath> | Yes | Yes | Partial |
std::inplace_vector | Yes | Yes | Yes |
| Static reflection | Experimental | Experimental | No |
| Contracts | Experimental | Experimental | No |
std::execution | Via stdexec | Via stdexec | No |
std::simd | Partial | Partial | No |
Pack Indexing β Deep Dive (P2662)
Direct subscript access into parameter packs eliminates a long-standing ergonomic gap. Before C++26, extracting the Nth element of a pack required a helper via std::tuple_element, a recursive template chain, or a fold expression β each approach obscuring the intent behind boilerplate.
// C++26
// --- Type-level indexing ---
template <std::size_t I, typename... Ts>
using type_at = Ts...[I]; // ill-formed if I >= sizeof...(Ts)
static_assert(std::is_same_v<type_at<1, int, double, char>, double>);
// --- Value-level indexing in function bodies ---
template <std::size_t I, typename... Args>
decltype(auto) get_nth(Args&&... args) {
return std::forward<Args...[I]>(args...[I]);
}
auto& s = get_nth<2>(42, 3.14, std::string{"hello"}, true);
// s is a reference to the string "hello"
// --- Last element without sizeof...(Ts) - 1 tricks ---
template <typename First, typename... Rest>
auto last_of(First, Rest... rest) {
if constexpr (sizeof...(Rest) == 0)
return first;
else
return rest...[sizeof...(Rest) - 1];
}The index I must be an integer constant expression; a runtime value causes a hard error, not UB. Negative indices and indices equal to or greater than sizeof...(Ts) are ill-formed, caught at compile time. This makes pack indexing strictly safer than std::get<I>(tuple) at index-check granularity, while also being more readable.
// C++26
// Practical: build a fixed-size heterogeneous pipeline
template <typename... Stages>
struct Pipeline {
std::tuple<Stages...> stages;
// Access stage by compile-time index without std::get
template <std::size_t I>
auto& stage() { return std::get<I>(stages); }
// Type of the first and last stage available as aliases
using InputType = typename Stages...[0]::input_type;
using OutputType = typename Stages...[sizeof...(Stages) - 1]::output_type;
};Erroneous Behaviour β Deep Dive (P2795)
C++26 introduces a fourth behavioural category that constrains what compilers may assume about provably-wrong code. The prior three categories were: well-defined behaviour, implementation-defined behaviour (outcome varies but must be documented), and undefined behaviour (no constraints β the compiler may assume it never happens). Erroneous behaviour sits between implementation-defined and UB: the program has made a mistake, and the result is unspecified, but the compiler may not exploit the occurrence as a proof of unreachability.
The canonical motivating example is reading an uninitialized variable:
// C++26
void example() {
int x; // uninitialized
int y = x; // erroneous behaviour: result of y is unspecified
if (y > 0) // compiler CANNOT eliminate this branch on the grounds
do_thing(); // that "x was uninitialized so this path is dead"
}Under C++23 and earlier this is UB, which gave optimisers licence to hoist, reorder, or delete the entire function. Under C++26 the read produces some value β potentially a trap representation, potentially zero, potentially whatever bits happen to be on the stack β but the control flow is no longer subject to UB-based elimination.
Implementations are encouraged (not required) to either produce a fixed sentinel value or trap via a sanitiser mode. In practice this means -fsanitize=memory style checks can be standardised and enabled by default without undefined-behaviour semantics. The change primarily affects:
- Reading uninitialized automatic variables
- Reading padding bytes of structs
- Certain out-of-lifetime pointer arithmetic patterns reclassified by future papers
Erroneous behaviour does not mean the code is correct or that you should rely on the output value. It means security-sensitive code that reads a (possibly-uninitialized) flag and branches on it can no longer be silently deleted by an aggressive inliner.
std::inplace_vector β Deep Dive (P0843)
std::inplace_vector<T, N> is a std::vector-compatible container whose storage lives entirely inside the object β no heap allocation, ever. The capacity is fixed at compile time; the size is dynamic up to that capacity. It satisfies the same named requirements as std::vector and works with the same algorithms, iterators, and range adaptors.
// C++26
#include <inplace_vector>
// Storage: N * sizeof(T) + sizeof(size_type) bytes, inline
std::inplace_vector<float, 16> scratch;
// Identical API to std::vector for in-range operations
scratch.push_back(1.0f);
scratch.emplace_back(2.0f);
scratch.resize(4, 0.0f); // fills with 0.0f up to size 4
// Capacity is a compile-time constant
static_assert(scratch.capacity() == 16);
static_assert(decltype(scratch)::static_capacity == 16);Overflow behaviour comes in two variants, matching the std::vector checked/unchecked pattern:
// C++26
std::inplace_vector<int, 4> v = {1, 2, 3, 4};
// Checked β throws std::bad_alloc when full (capacity == static_capacity)
try {
v.push_back(5); // throws: no heap to grow into
} catch (const std::bad_alloc&) { /* ... */ }
// Unchecked β undefined behaviour when full; use only when proven safe
v.resize(0);
v.unchecked_push_back(10); // safe here: size was 0
v.unchecked_push_back(20);The primary use cases are embedded and real-time systems (no dynamic allocation), hot-path small buffers (avoid allocator overhead), and stack-local scratch space in tight loops. Because std::inplace_vector is trivially_copyable when T is, it can be placed in shared memory segments or passed across process boundaries without serialisation.
// C++26
// Compile-time-sized message buffer β trivially copyable when T is
struct alignas(64) CacheAlignedBuffer {
std::inplace_vector<std::byte, 56> payload; // 64 - 8 bytes for size
};
static_assert(sizeof(CacheAlignedBuffer) == 64);std::execution β Sender/Receiver Model (P2300)
std::execution standardises structured concurrency by separating the description of work from its scheduling and execution. A sender is a lazy value: it encodes what computation to run and how errors propagate, but does not start executing until a receiver is connected. Schedulers decouple policy (which thread pool, which executor) from the work graph itself.
The three fundamental building blocks are senders (work descriptions), receivers (result/error/done callbacks), and schedulers (execution contexts). Algorithms connect them:
// C++26
#include <execution>
namespace ex = std::execution;
// just(v) is the simplest sender: immediately yields v
// then(f) transforms the value
// sync_wait blocks the caller and returns optional<tuple<values...>>
auto result = std::this_thread::sync_wait(
ex::just(std::vector{1, 2, 3, 4, 5})
| ex::then([](auto v) {
std::ranges::sort(v, std::greater{});
return v;
})
| ex::then([](auto v) { return v[0]; }) // largest element
).value();
// result == tuple{5}Error propagation is explicit and composable, not exception-based:
// C++26
// upon_error and upon_stopped recover from error/cancellation channels
auto safe_divide = [](double n, double d) -> std::expected<double, std::string> {
if (d == 0.0) return std::unexpected("division by zero");
return n / d;
};
auto pipeline =
ex::just(10.0, 0.0)
| ex::then(safe_divide)
| ex::then([](auto r) { return r.value_or(-1.0); });The key departure from std::async and std::future is that there is no shared state, no reference counting, and no implicit thread launch. Work is placed on an explicit scheduler via ex::on(scheduler, sender). Cancellation propagates through the pipeline as a distinct stopped channel, not an exception, enabling clean teardown without RTTI cost.
// C++26
// Structured fan-out: all three tasks run on pool concurrently;
// when_all completes when ALL complete (or any produces an error)
thread_pool pool{4};
auto [r1, r2, r3] = std::this_thread::sync_wait(
ex::when_all(
ex::on(pool.get_scheduler(), ex::just(data_a) | ex::then(process)),
ex::on(pool.get_scheduler(), ex::just(data_b) | ex::then(process)),
ex::on(pool.get_scheduler(), ex::just(data_c) | ex::then(process))
)
).value();See Also
- C++23 Features β
std::expected,std::print, deducingthis,import std - C++20 Features β Concepts, Coroutines, Modules, Ranges,
std::format - Concepts β Constraining templates; pairs naturally with reflection-generated requirements
- Coroutines β
std::executionsenders compose with C++20 coroutines viaco_await - Constexpr β Extended in C++26 to cover
<cmath>functions