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:
^^Tproduces astd::meta::infodescribingT.[: 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_contextto specify what the calling context may see. - Consteval only: Functions in
std::metaare 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_ofskips static members. Usemembers_offor 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_waitlive in specific namespaces. Check your standard library’s documentation, as early implementations may usestdexec::orexec::. - 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.




