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 |
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