Skip to content
C++
Language
since C++26
Intermediate

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

C++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.

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

cpp
#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.

cpp
// 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):

ModeBehaviour
ignoreChecks elided entirely β€” zero overhead
observeCheck fires, violation handler called, execution continues
enforceCheck fires, violation handler called, std::terminate() (default)
quick_enforceMinimal-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.

cpp
#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.

cpp
#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.

cpp
// 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:

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

Under 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):

cpp
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):

cpp
// 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:

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

cpp
#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 proven

Ideal for hot-path small collections where heap allocation is unacceptable.

std::indirect<T> and std::polymorphic<T> (P3019) β€” value semantics for heap objects:

cpp
#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 s

Best Practices

  • Contracts over assert(): Contract violations integrate with the build system's violation mode and are visible to static analysers in ignore mode β€” raw assert() 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::simd loop is a correctness bug, not a performance omission.
  • std::inplace_vector for bounded hot paths: Eliminates heap allocation for small fixed-bound collections; use unchecked_push_back only after proving capacity bounds statically.
  • Senders are lazy: A std::execution pipeline does nothing until sync_wait or a receiver is attached. Building a pipeline and discarding it schedules nothing.

Common Pitfalls

  • Reflection sees private members: std::meta::nonstatic_data_members_of includes private members. Filter with std::meta::is_public(M) before exposing them through public APIs.
  • Contract mode mismatch across TUs: Mixing translation units compiled with different contract modes (enforce vs ignore) in a single binary can produce surprising diagnostics or silent skips. Pin the mode globally via the build system.
  • #embed and null termination: Embedded data has no implicit null terminator. Passing the array to string functions without appending '\0' is a buffer overread.
  • std::simd width 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] requires I to 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

cpp
βœ“ 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::execution

Compiler Support Status (2026)

FeatureGCC 15Clang 19MSVC 19.43
Pack indexingYesYesYes
= delete messageYesYesYes
#embedYesYesPartial
Erroneous behaviourYesYesPartial
constexpr <cmath>YesYesPartial
std::inplace_vectorYesYesYes
Static reflectionExperimentalExperimentalNo
ContractsExperimentalExperimentalNo
std::executionVia stdexecVia stdexecNo
std::simdPartialPartialNo

See Also

  • C++23 Features β€” std::expected, std::print, deducing this, import std
  • C++20 Features β€” Concepts, Coroutines, Modules, Ranges, std::format
  • Concepts β€” Constraining templates; pairs naturally with reflection-generated requirements
  • Coroutines β€” std::execution senders compose with C++20 coroutines via co_await
  • Constexpr β€” Extended in C++26 to cover <cmath> functions