C++26 Is Finalized: Reflection, Contracts, std::execution and Other Key Changes

Key takeaways

C++26 brings several features at once that change how C++ libraries are written: static reflection, Contracts, safer defaults for uninitialized reads, a standardized hardened library mode, and std::execution. This article explains what each one is for, separates what you get from recompiling or build settings from what requires code changes, and summarizes compiler support as of September 2026.

Introduction

The C++26 feature set is final. WG21 (the ISO C++ committee) closed C++26 to new features at its June 2025 meeting in Sofia, Bulgaria, and then moved on to resolving national body comments and polishing wording. Once the technical work is finished, the Draft International Standard (DIS) ballot and ISO publication follow; the exact publication date depends on the ISO process, so check isocpp.org for the official announcement. The features covered here are the ones fixed by the feature freeze.

C++26 is often described as the largest change since C++11. Where C++11 changed everyday code with auto, lambdas, smart pointers and move semantics, C++26 aims to change how libraries are written and what the safety defaults are, through reflection, Contracts, memory-safety improvements and std::execution.

What This Article Covers

  • The four headline features of C++26
  • What you get from recompiling or build settings vs. what needs code changes
  • The debate around Contracts
  • Compiler support as of September 2026
  • Directions being discussed for C++29

The Four Headline Features

Static Reflection

Reflection lets a program inspect its own structure: types, members, enumerators and so on. C++26 reflection (P2996) does all of this at compile time and generates code from the results.

What Becomes Possible?

// Adopted C++26 syntax (P2996 + template for)
#include <meta>
#include <iostream>
#include <string>

struct Point {
    int x;
    int y;
};

// Inspect the struct at compile time
constexpr auto ctx = std::meta::access_context::unchecked();
static_assert(std::meta::nonstatic_data_members_of(^^Point, ctx).size() == 2);

// Expand serialization code for each member (assumes arithmetic members)
template<typename T>
std::string serialize(const T& obj) {
    std::string result = "{";
    bool first = true;
    template for (constexpr auto m :
                  std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
        if (!first) result += ", ";
        first = false;
        result += std::meta::identifier_of(m);
        result += ": ";
        result += std::to_string(obj.[:m:]);
    }
    return result + "}";
}

int main() {
    Point p{10, 20};
    std::cout << serialize(p);  // {x: 10, y: 20}
}

^^Point turns the type into a std::meta::info value, nonstatic_data_members_of returns its members, template for expands the loop body once per member, and obj.[:m:] becomes an access to that member. The single-caret ^T operator and [:expand(...):] from early proposals are not the adopted syntax, so be careful with older examples.

Why It Matters

Before (C++23): members are listed by hand.

std::string Point::to_string() const {
    return "{x: " + std::to_string(x) +
           ", y: " + std::to_string(y) + "}";
}
// Every new member means editing this function, and forgetting still compiles

After (C++26): the member list comes from reflection, so serialize picks up new members automatically.

JSON and binary serialization, database mapping, debug output and enum ↔ string conversion, all of which relied on macros or tricks like Boost.PFR and magic_enum, can now use standard syntax. See C++26 Reflection Basics for the syntax in detail.


Memory Safety from Recompiling and Build Settings

C++26 includes two safety improvements that help without code changes, but they work differently.

(1) Uninitialized Reads: UB → Erroneous Behavior

Before (C++23):

int foo() {
    int x;     // not initialized
    return x;  // undefined behavior
}

Because this was UB, compilers could assume the code never runs and optimize surrounding code in surprising ways, and leftover stack data could leak as an information-disclosure bug.

C++26 (P2795):

int foo() {
    int x;     // still a bug to leave it uninitialized
    return x;  // erroneous behavior: reads some implementation-chosen value (not guaranteed to be 0)
}

C++26 puts such reads in a new category, erroneous behavior. The behavior is defined, so the compiler cannot use it to delete or distort code, but it is still a bug, and implementations are allowed to diagnose it with warnings or runtime checks. There is no guarantee that the value is zero. Large buffers that are deliberately left uninitialized for performance can opt back into the old behavior with the [[indeterminate]] attribute.

(2) Standard Library Hardening

C++26 adds the notion of a hardened implementation (P3471). When an implementation offers a hardened mode and you enable it, some preconditions of operations such as std::vector::operator[], std::span and std::optional::operator* are checked at run time, and violations are handled as contract violations.

std::vector<int> v = {1, 2, 3};
int x = v[10];
// Hardened mode off: undefined behavior, as before
// Hardened mode on: the bounds check fails and is handled as a contract violation
//                   (not an exception; typically the program terminates)

Note that this is an implementation mode you opt into, not a default, and unlike at(), it does not throw std::out_of_range, so it is not a mechanism for catching and recovering.

Real-World Deployment

Vendors offered similar hardening before standardization. Google published results from enabling libc++ hardening across its server code: according to that post, the average performance impact was about 0.3%, the rollout uncovered more than 1,000 bugs, and the segmentation fault rate in production dropped by about 30% (Google Security Blog, November 2024). Those numbers describe Google’s codebase and measurement, so don’t expect identical results elsewhere, but they are good reason to revisit the assumption that bounds checking is too expensive.


Contracts: Function Contracts in the Declaration

Preconditions and postconditions go in the declaration, and checks inside the body use contract_assert (P2900).

Basic Usage

#include <cstddef>

// Precondition
int divide(int a, int b)
    pre(b != 0)  // b must not be 0
{
    return a / b;
}

// Postcondition: name the return value to refer to it
int* allocate(std::size_t size)
    pre(size > 0)
    post(r: r != nullptr)
{
    return new int[size];
}

// contract_assert: a check inside the body
void process(int* ptr) {
    contract_assert(ptr != nullptr);
    // ...
}

How It Differs from assert

#include <cassert>
int divide(int a, int b) {
    assert(b != 0);  // gone under NDEBUG, otherwise always abort()
    return a / b;
}

assert is a macro switched by NDEBUG. With Contracts the condition is visible in the declaration, and whether it is checked and what a violation does is chosen by the implementation and build settings from four evaluation semantics: ignore, observe, enforce and quick_enforce. Violations go to a replaceable violation handler, so diagnostics can be centralized. On the other hand, C++26 Contracts have no old() for referring to entry values in postconditions and no class invariant syntax. See the C++26 Contracts guide for details.


std::execution: A Standard Async Model

std::execution (P2300, senders/receivers) separates where work runs (a scheduler) from what runs (senders) and lets you compose them.

#include <execution>

// A scheduler comes from an execution context such as a thread pool or event loop
auto task = std::execution::schedule(scheduler)
    | std::execution::then([]{ return fetch_data(); })
    | std::execution::then([](auto data){ return process(data); })
    | std::execution::then([](auto result){ save(result); });

// Block the current thread until it completes
std::this_thread::sync_wait(std::move(task));
  • Structured concurrency: the lifetime of work is tied to the composition, which makes it easier to avoid detached tasks outliving the data they reference. It does not prevent data races by itself.
  • One interface for many contexts: thread pools, event loops and GPUs are meant to be driven the same way.
  • The practical downside: the API is large and abstract, and the standard mostly provides the foundation, so in practice you will pair it with a reference implementation such as NVIDIA’s stdexec or helper libraries.

Other Changes

Beyond the headline four, several changes affect everyday code, for example:

  • #embed for including binary files as arrays at compile time
  • Pack indexing (Ts...[0])
  • = delete("reason")
  • The placeholder variable _
  • New containers such as std::inplace_vector and std::hive
  • Numeric libraries such as std::simd and <linalg>

Contracts Controversy

Contracts had the rockiest path to standardization of any C++26 feature. An earlier version was voted into the working draft for C++20, then pulled back out before release. The version in C++26, P2900, is a deliberately scaled-back minimum viable product adopted at the February 2025 meeting, and even that vote drew a notable number of objections. Concerns raised before and after adoption include:

  • Cost: runtime and code-size cost when checks are enabled.
  • Mixed semantics across translation units: an inline function can be compiled with different evaluation semantics in different translation units, and part of what happens then is left to the implementation.
  • Missing pieces: the MVP has no contracts on virtual functions, no old-value capture in postconditions, and no class invariants.
  • Semantics instead of a debug/release switch: teams need a policy per build configuration (ignore, observe, enforce, quick_enforce) rather than relying on NDEBUG.

The practical takeaway: pre/post/contract_assert are a real improvement over assert because they are part of the declared interface and route violations through one handler, but expect the ecosystem to take a few years to settle on idiomatic usage, as it did with concepts after C++20.


Compiler Support

As of September 2026 (check each compiler’s C++26 status page before relying on this):

  • GCC 16: reflection with -std=c++26 -freflection; Contracts can be tried as an experimental implementation with -fcontracts.
  • Clang: reflection is still being merged upstream; Bloomberg’s clang-p2996 fork is available on Compiler Explorer. Other C++26 features are partially supported depending on version.
  • MSVC: supports some C++26 features, but no public reflection implementation.

For code that must build on several compilers, branch on feature-test macros such as __cpp_impl_reflection and __cpp_contracts.

What Existing Projects Can Do Now

1. Turn on standard library hardening. This works today regardless of language mode:

# libstdc++ (GCC)
target_compile_definitions(myapp PRIVATE _GLIBCXX_ASSERTIONS)
# libc++ (Clang)
# target_compile_definitions(myapp PRIVATE _LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST)

Run your tests and benchmarks, then decide whether to keep it in release builds.

2. Classify your asserts as future contracts. Entry checks are pre candidates, checks right before returning are post candidates, and the rest are contract_assert candidates. Separating “false only when there is a bug” from “input validation done with assert for convenience” now makes the migration easier.

3. Collect field lists in one place. Serialization and mapping code that repeats field lists can be centralized with Boost.PFR or a macro, so that later only that piece needs to switch to reflection.


C++29 Direction

With C++26 wrapping up, the committee’s attention is shifting to the next cycle. Based on papers already circulating and features deliberately deferred from C++26, these directions look likely to come up. None of it is committed.

  • Reflection, round two: C++26 covers reading structure and expanding code from it; more general code generation (injection) and reflecting more language constructs are being discussed as extensions.
  • Contracts beyond the MVP: old-value capture in postconditions and contracts on virtual functions are candidates for follow-up proposals.
  • Safety profiles: profiles, proposed by Bjarne Stroustrup and others, did not make C++26 and their design is still being debated.
  • Pattern matching: proposed repeatedly and still not in the standard; it remains one of the most requested features.
  • Physical quantities and units: a standardization proposal based on mp-units is under discussion.

WG21 papers are proposals, not promises, and C++26 itself changed shape more than once during its own cycle. Treat this as “what to watch,” not a roadmap.


Conclusion

Key Summary

  1. Reflection: iterate member lists at compile time with ^^, std::meta::info and template for, and generate code from them
  2. Memory safety: uninitialized reads become erroneous behavior; hardened library modes are standardized
  3. Contracts: pre/post/contract_assert with four evaluation semantics
  4. std::execution: a standard model for composing schedulers and senders

Where to Start

  • From recompiling: uninitialized reads no longer feed optimizer-amplified UB
  • From build settings: standard library hardening (already available as vendor options)
  • From code changes: reflection-based serialization, Contracts, std::execution

Even if you can’t move to a C++26 compiler yet, enabling hardening to measure its cost and cleaning up asserts and repeated field lists will make the transition much cheaper.


Frequently Asked Questions (FAQ)

Q. Do I have to wait for C++26 to get standard library bounds checking?

A. No. _GLIBCXX_ASSERTIONS is a libstdc++ macro that works with older language modes too, adding checks such as bounds checks on std::vector::operator[]. libc++ has its own hardening modes. C++26 standardizes the idea of a hardened library implementation, but you can turn on these checks in current builds and measure the overhead today.


References