C++26 Explained: Reflection, Contracts, and std::execution in Plain English

C++26 is the first standard since C++11 that changes how you write programs, not just how you write individual functions. Static reflection lets code inspect its own types at compile time. Contracts put preconditions and postconditions into the language. std::execution (senders/receivers) gives C++ a standard model for asynchronous and parallel work. This guide explains each feature with isolated examples, trade-offs, and refactoring patterns.

Compiler note: Support is still uneven. Reflection (P2996) landed first in experimental Clang and GCC forks, and contracts (P2900) are rolling into mainline GCC and Clang. Verify the current status for your toolchain on cppreference’s compiler support page before adopting any feature in production.

Quick Takeaways

  • Reflection (^^T, [: :], std::meta) replaces macros and code generators for serialization, enums, and boilerplate.
  • Contracts (pre, post, contract_assert) make function requirements part of the interface, with configurable evaluation semantics.
  • std::execution (std::execution::sender, then, when_all, sync_wait) composes async work as lazy, cancellable pipelines.
  • All three trade a steeper learning curve for less boilerplate and fewer runtime surprises.
Feature Proposal Problem Solved Cost Maturity
Reflection P2996 Boilerplate, macros, external codegen Compile time Experimental compilers
Contracts P2900 Undocumented, unchecked assumptions Runtime checks (configurable) Landing in GCC/Clang
std::execution P2300 Fragmented async models Learning curve Reference impls (stdexec)

Part 1: Static Reflection

What Reflection Means in C++26

Reflection lets a program ask questions about its own entities at compile time. You get a reflection value of type std::meta::info by applying the reflection operator ^^ to a type, member, or namespace. A splice [: r :] turns that value back into code.

Two ideas cover most use cases:

  1. ^^T produces a std::meta::info describing T.
  2. [: info :] converts the description back into a type, value, or expression.

Syntax Breakdown

Start with the smallest possible example. Reflect a type and read its name:

#include <meta>
#include <string_view>

struct Point {
    int x;
    int y;
};

// Reflect the type and query its identifier at compile time.
constexpr std::string_view type_name = std::meta::identifier_of(^^Point);

static_assert(type_name == "Point");

This compiles to a constant. No runtime cost exists because identifier_of runs entirely during constant evaluation.

Now splice a reflection back into code:

#include <meta>

constexpr auto int_info = ^^int;

// Splice the reflection back into a type position.
[:int_info:] value = 42;   // Equivalent to: int value = 42;

The splice [:int_info:] behaves exactly like writing int. This round trip is the foundation of everything else.

Practical Implementation: Enum to String

Enum-to-string conversion is the classic macro-and-codegen problem. Reflection reduces it to a single template.

#include <meta>
#include <string>
#include <type_traits>

// Convert any enum value to its enumerator name.
template <typename E>
    requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
    // Expand over every enumerator at compile time.
    template for (constexpr auto e :
                  std::define_static_array(std::meta::enumerators_of(^^E))) {
        if (value == [:e:]) {
            return std::string(std::meta::identifier_of(e));
        }
    }
    return "<unknown>";
}

enum class Color { Red, Green, Blue };

// Usage:
// enum_to_string(Color::Green) returns "Green"

Runtime behavior: The template for loop unrolls at compile time into a chain of comparisons, one per enumerator. For Color, the compiler emits three comparisons. Adding a fourth enumerator requires zero changes to enum_to_string.

Practical Implementation: Generic Struct Printer

Isolate this as a separate example. It iterates over data members without any per-type code.

#include <iostream>
#include <meta>

// Print every non-static data member of any struct.
template <typename T>
void print_members(const T& obj) {
    template for (constexpr auto member :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(
                          ^^T, std::meta::access_context::current()))) {
        std::cout << std::meta::identifier_of(member)
                  << " = " << obj.[:member:] << '\n';
    }
}

struct User {
    int id;
    double balance;
};

// print_members(User{7, 19.5}) prints:
// id = 7
// balance = 19.5

The expression obj.[:member:] splices a member reflection into a member-access expression. Renaming a field updates the output automatically. This removes an entire category of stale-serialization bugs.

Edge Cases

  • Access control: Reflection queries respect access. Use std::meta::access_context to specify what the calling context may see.
  • Consteval only: Functions in std::meta are consteval. You cannot call them on runtime values.
  • Compile time: Heavy reflection inside deep template hierarchies increases build times. Measure before wide adoption.
  • Non-static members only: nonstatic_data_members_of skips static members. Use members_of for broader queries.

Reflection vs. Existing Approaches

Approach Compile-Time Safety Maintenance Build Complexity Runtime Cost
Macros (X-macros) Low High Low None
External codegen Medium Medium High None
Template tricks (Boost.PFR) Medium Medium Low None
C++26 Reflection High Low Low None

Part 2: Contracts

What Contracts Are

Contracts add precondition assertions (pre), postcondition assertions (post), and assertion statements (contract_assert) to the language. Before C++26 you used assert(), comments, or custom macros. Each had gaps: assert vanishes under NDEBUG, comments are unchecked, and macros do not compose with tooling.

Syntax Breakdown

#include <vector>

// Precondition: callers must pass a non-empty vector.
// Postcondition: the result is a valid index into the vector.
std::size_t max_index(const std::vector<int>& v)
    pre(!v.empty())
    post(r : r < v.size())
{
    std::size_t best = 0;
    for (std::size_t i = 1; i < v.size(); ++i) {
        if (v[i] > v[best]) best = i;
    }
    return best;
}

The r in post(r : ...) names the return value. Both predicates are part of the function declaration, so callers and tools see them without reading the body.

Inline Assertions

void process(int* data, std::size_t n) {
    // Internal invariant check inside the body.
    contract_assert(data != nullptr);
    contract_assert(n > 0);
    // ... work with data ...
}

contract_assert replaces ad-hoc assert calls and participates in the same evaluation-semantic system as pre and post.

Evaluation Semantics

Each contract assertion is evaluated under one of several semantics, chosen by the implementation or build configuration rather than in source code.

Semantic Checks Predicate On Violation Typical Use
ignore No Nothing Release builds with zero overhead
observe Yes Calls violation handler, then continues Rolling out contracts safely
enforce Yes Calls violation handler, then terminates Debug builds, safety-critical code
quick-enforce Yes Terminates immediately Hardened production builds

Runtime behavior: Under enforce, calling max_index({}) triggers the violation handler and then terminates. Under ignore, the call proceeds into undefined behavior, because the precondition was never checked.

Contract Violation Handler

You can supply a replaceable handler to log violations:

#include <contracts>
#include <iostream>

// Replaceable handler: invoked on a contract violation.
void handle_contract_violation(const std::contracts::contract_violation& v) {
    std::cerr << "Contract violation: " << v.comment() << '\n';
}

Check your toolchain documentation for the exact handler signature and linkage requirements, since implementations are still converging.

Edge Cases

  • Side effects: Predicates must be side-effect free in practice. The compiler may evaluate them zero, one, or multiple times.
  • Virtual functions: Contracts on virtual functions are checked at the call site’s static type, which affects overriding design.
  • Not a replacement for validation: Use contracts for programmer errors. Use error codes, std::expected, or exceptions for runtime input errors.

Part 3: std::execution (Senders and Receivers)

The Core Model

std::execution standardizes asynchronous work around three concepts:

  • Sender: a lazy description of work that will produce a value, an error, or a stop signal.
  • Receiver: the consumer that handles one of those three outcomes.
  • Scheduler: a handle to an execution resource such as a thread pool.

Senders are lazy. Nothing runs until you connect a receiver or call sync_wait. This differs from std::async and std::future, which start work eagerly.

Basic Pipeline

#include <execution>
#include <iostream>

namespace ex = std::execution;

int main() {
    // Describe work: start with 21, double it. Nothing runs yet.
    auto work = ex::just(21)
              | ex::then([](int x) { return x * 2; });

    // Execute and block for the result.
    auto [result] = std::this_thread::sync_wait(std::move(work)).value();

    std::cout << result << '\n';  // 42
}

Runtime behavior: just(21) creates a sender that completes immediately with 21. then attaches a transformation. sync_wait starts the chain, waits for completion, and returns an optional tuple. The output is 42.

Running on a Specific Scheduler

#include <execution>

namespace ex = std::execution;

// Run a computation on a thread pool and return the result.
int compute_on_pool(ex::scheduler auto pool_sched) {
    auto work = ex::schedule(pool_sched)             // Hop onto the pool
              | ex::then([] { return 6 * 7; });      // Runs on a pool thread

    auto [value] = std::this_thread::sync_wait(std::move(work)).value();
    return value;  // 42
}

schedule produces a sender that completes on the scheduler’s execution context. Every subsequent then runs there, so threading is explicit in the pipeline.

Parallel Composition with when_all

#include <execution>

namespace ex = std::execution;

// Run two independent tasks concurrently and combine their results.
int parallel_sum(ex::scheduler auto sched) {
    auto a = ex::schedule(sched) | ex::then([] { return 10; });
    auto b = ex::schedule(sched) | ex::then([] { return 32; });

    auto combined = ex::when_all(std::move(a), std::move(b))
                  | ex::then([](int x, int y) { return x + y; });

    auto [sum] = std::this_thread::sync_wait(std::move(combined)).value();
    return sum;  // 42
}

Runtime behavior: when_all starts both senders concurrently and completes when both finish. If either fails or is cancelled, the other receives a stop request. That structured cancellation is a major advantage over raw threads.

Error and Cancellation Channels

Every sender has three completion channels:

Channel Meaning Handled By
set_value Success with results then
set_error Failure with an error upon_error, let_error
set_stopped Cancelled upon_stopped, let_stopped

Async Models Compared

Model Laziness Cancellation Composability Scheduler Control
std::thread Eager Manual Poor Manual
std::async / future Eager None Poor None
Coroutines alone Depends Manual Medium Library-defined
std::execution Lazy Built-in High Explicit

Edge Cases

  • Header and namespace details: Facilities like sync_wait live in specific namespaces. Check your standard library’s documentation, as early implementations may use stdexec:: or exec::.
  • Lifetime: Senders are lazy, so captured references must outlive the eventual execution. Capture by value when work crosses threads.
  • Debugging: Deeply nested sender types produce long template errors. Use type aliases and small helper functions.

Best Practices: Bad Code vs. Good Code

Reflection: Hand-Written Serialization

Bad code (anti-pattern):

// Every new field requires a manual update. Easy to forget.
std::string to_json(const User& u) {
    return "{\"id\":" + std::to_string(u.id) +
           ",\"balance\":" + std::to_string(u.balance) + "}";
}

Good code (refactored):

// Works for any aggregate. New fields appear automatically.
template <typename T>
std::string to_json(const T& obj) {
    std::string out = "{";
    bool first = true;
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(
                          ^^T, std::meta::access_context::current()))) {
        if (!first) out += ',';
        first = false;
        out += '"';
        out += std::meta::identifier_of(m);
        out += "\":";
        out += std::to_string(obj.[:m:]);
    }
    out += '}';
    return out;
}

The refactored version removes duplication. The sketch handles numeric members only. Extend it with if constexpr branches for strings and nested types.

Contracts: Scattered Assertions

Bad code (anti-pattern):

// The requirement lives in a comment and a macro that disappears in release.
// Requires: v is not empty
std::size_t max_index(const std::vector<int>& v) {
    assert(!v.empty());
    // ...
}

Good code (refactored):

// The requirement is part of the signature and visible to tools.
std::size_t max_index(const std::vector<int>& v)
    pre(!v.empty());

std::execution: Eager Futures

Bad code (anti-pattern):

// Launches immediately, cannot be cancelled, blocks on get().
auto f1 = std::async(std::launch::async, task_a);
auto f2 = std::async(std::launch::async, task_b);
int total = f1.get() + f2.get();

Good code (refactored):

// Lazy, cancellable, and the execution resource is explicit.
auto total_work =
    ex::when_all(ex::schedule(pool) | ex::then(task_a),
                 ex::schedule(pool) | ex::then(task_b))
  | ex::then([](int a, int b) { return a + b; });

auto [total] = std::this_thread::sync_wait(std::move(total_work)).value();

Adoption Strategy

Step Action Risk
1 Add contracts under observe semantics to critical APIs Low
2 Prototype reflection for one serializer or enum utility Medium
3 Wrap one async subsystem with std::execution Medium
4 Gate all three behind feature-test macros and CI matrices Low

Use feature-test macros such as __cpp_impl_reflection and __cpp_contracts to guard code paths until your compilers fully support the features.

FAQ

What is new in C++26?

C++26 adds static reflection (^^, std::meta), contracts (pre, post, contract_assert), and std::execution for structured async programming. It also adds a range of library and language refinements. Compiler support varies, so check the support tables for your toolchain.

Does C++26 reflection have runtime overhead?

No. Reflection operates entirely at compile time through consteval functions. Generated code is equivalent to hand-written code. The cost appears in compile time, not runtime performance.

Are C++ contracts the same as assert()?

No. assert() is a macro controlled by NDEBUG. Contracts are language features declared on function interfaces, with selectable evaluation semantics (ignore, observe, enforce, quick-enforce) and a replaceable violation handler.

Is std::execution a replacement for std::async?

For most serious async work, yes. std::async is eager, lacks cancellation, and offers no scheduler control. std::execution is lazy, composable, and supports cancellation, but it has a steeper learning curve.

Hot this week

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Topics

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Build a Low-Cost AI Robot Arm With SO-101 and LeRobot

Build an SO-101 robot arm under $250, calibrate it, record demos, and train an ACT policy with LeRobot. Follow the full guide and start building.

Robot Foundation Models: GR00T, pi, Gemini Robotics, and Open Alternatives Compared

Compare robot foundation models: NVIDIA GR00T, Physical Intelligence π, Gemini Robotics 2, and open VLAs. Get latency math, code, and a pick guide.

How Much Does a Humanoid Robot Cost? Prices, Subscriptions, and Hidden Costs

Humanoid robot cost in 2026: prices from $4,900, $499/mo subscriptions, and hidden fees. See the full TCO breakdown and compare models now.

Related Articles

Popular Categories