C++26: What Actually Compiles Today
I spent a weekend feeding every major C++26 feature through GCC 15.2.1 and Clang 21.1.8 on Fedora 43. The standard was voted out of committee in March. The question I wanted to answer was not “what’s in the spec” — the trip reports cover that — but what I could actually build against right now.
Contracts work on GCC. Structured bindings in conditions work everywhere. Reflection, trivial relocation, and std::hive are standardized text with no compiler behind them. Profiles — the committee’s answer to “why not Rust” — didn’t make the cut at all.
Here’s what I found, feature by feature.
Contracts Work. Today. On GCC.
This is the one that matters most for codebases shipping in 2026. P2900 makes preconditions and postconditions a language feature, with three semantic levels that control runtime enforcement.
GCC 15 ships it behind -fcontracts:
int compute_positive(int x) [[pre: x > 0]] [[post r: r > x]]
{
return x * 2;
}
The three levels:
[[pre: cond]]— default. Checked in every build. Violation prints the condition and callsterminate(). This is P2900’s “enforce” semantic.[[pre audit: cond]]— checked only with-fcontract-build-level=audit. Too expensive for production, useful in test. The “observe” semantic.[[pre axiom: cond]]— never checked. The compiler assumes the condition holds and optimizes accordingly. Violating it is UB. The “assume” semantic.
In-function assertions use the same mechanism:
void process(int* data, int n)
{
[[assert: data != nullptr]];
[[assert: n > 0]];
for (int i = 0; i < n; ++i)
data[i] *= 2;
}
Call compute_positive(-1) and you get:
contract violation in function compute_positive at claim-01-contracts-demo.cpp:23: x > 0
terminate called without an active exception
Function name, source location, exact condition text. No stack trace — that’s what your debugger is for.
I like this more than I expected. The value is the same thing [[nodiscard]] did for return values: the toolchain enforces the interface instead of hoping the caller read the docs. The difference is that contracts are checked at runtime with zero ceremony beyond the attribute.
The violation handler is replaceable — you can install one that logs or collects telemetry instead of terminating. The committee deliberately made it hard to continue after a violation, which is the right call. Contracts should not become control flow. In practice, the default handler plus a core dump is what you want — the audit level is the interesting part: expensive debug-mode assertions without a separate preprocessor-guarded macro. I’ve maintained codebases where half the assert calls were #ifdef’d behind three layers of build-system flags. [[pre audit:]] kills that entire pattern.
Reflection: Standardized, Uncompilable
P2996 is in the spec. The ^ operator gives you a std::meta::info mirror at compile time, and consteval functions let you inspect members, names, and types without macros.
#include <meta>
struct Point { float x, y, z; };
consteval auto member_count() {
return std::meta::nonstatic_data_members_of(^Point).size();
}
The splice syntax [:expand(members):] would let you iterate over reflected members and stamp out serialization, ORM mapping, debug printing — every problem that currently needs macros or external codegen.
I say “would” because no shipping compiler implements any of it. Clang 21.1.8 doesn’t recognize <meta>, doesn’t accept ^T, and has no -freflection flag. The Bloomberg/EDG experimental branch has a partial implementation. The upstream Clang p2996 branch is where the real work is happening. Mainstream support is 2027 at the earliest.
This is the feature I’m most frustrated about. We’ve been writing macro-based reflection hacks for twenty years, the committee finally standardized a proper solution, and I can’t use it. Standardized but uncompilable. The spec text exists; the compilers don’t.
Sender/Receiver: Works via stdexec
P2300 standardizes structured async execution. Senders describe work, receivers consume results, algorithms like then and when_all compose them into pipelines. The mental model is: describe the entire dataflow graph before any work begins, then let the runtime optimize scheduling across the whole thing.
Using NVIDIA’s stdexec reference implementation:
#include <stdexec/execution.hpp>
auto result = stdexec::sync_wait(
stdexec::just(42)
| stdexec::then([](int x) { return x * 2; })
);
// result = optional<tuple<int>>{84}
Multi-step pipelines:
auto result = stdexec::sync_wait(
stdexec::just(10)
| stdexec::then([](int x) { return x * 3; })
| stdexec::then([](int x) { return x + 7; })
);
// result = 37
Moving work to a thread pool:
exec::static_thread_pool pool{2};
auto scheduler = pool.get_scheduler();
auto result = stdexec::sync_wait(
stdexec::schedule(scheduler)
| stdexec::then([] { return 100; })
| stdexec::then([](int x) { return x + 23; })
);
// result = 123
All four patterns compiled with Clang 21.1.8 and libc++ 21 against stdexec fetched via CMake FetchContent. Header-only. The standard wording tracks stdexec closely, so migration to <execution> in your stdlib should be mechanical renaming once libstdc++ or libc++ ship the header.
For a single-shot RPC handler, sender/receiver is more machinery than you need. Where it pays off is streaming data pipelines with fan-out, cancellation, and backpressure — the kind of thing where ad-hoc callback chains become debugging nightmares. Composition is type-safe: connecting a sender that produces int to a receiver expecting string fails at compile time, not at 3 AM.
The friction is vocabulary. just, then, let_value, when_all, upon_error, schedule, transfer — the API surface is large, and P2300 is dense reading. Port a small async subsystem first. Don’t adopt it project-wide on day one.
The Smaller Wins
Structured bindings in conditions work on both compilers with -std=c++26:
if (auto [ptr, ec] = std::from_chars(numstr, numstr + 5, n);
ec == std::errc{}) {
printf("from_chars succeeded: %d\n", n);
}
Fewer temporaries, fewer lines, fewer typos between declaration and condition. Small improvement, but it compounds.
Trivial relocation (P2786) would let std::vector memcpy your objects during reallocation instead of move-construct-then-destroy — if the type opts in with [[trivially_relocatable_if_eligible]]. For std::unique_ptr, that’s one pointer write instead of three. For a vector of a million elements, it adds up.
Both compilers accept the attribute syntactically and ignore it. GCC: warning: 'trivially_relocatable_if_eligible' attribute directive ignored. Clang: unknown attribute. Neither exposes std::is_trivially_relocatable. Needs another release cycle.
std::hive (P0447) — node-stable, cache-friendly container for frequent insert/erase workloads. Entity-component systems, particle simulations. Neither libstdc++ nor libc++ ship <hive> yet. plf::colony remains the practical option.
What Didn’t Ship
Profiles: The Conspicuous Absence
This is the gap that matters. Profiles (P3081 and related papers) proposed opt-in safety annotations — [[profiles::safe]] would subject a function to bounds checking, lifetime analysis, and type-safety enforcement at compile time. A Rust-like safety story without forking the language.
Neither compiler recognizes the attribute:
error: 'profiles::safe' scoped attribute directive ignored [-Werror=attributes]
The committee deferred it. The sticking points: no consensus on what “safe” means precisely enough to standardize. How do profiles interact with existing code that violates their assumptions? Hard error, warning, or escape hatch? What happens at the boundary between profiled and un-profiled translation units? Stroustrup’s and Sutter’s proposals described different enforcement models with different answers, and the committee couldn’t converge.
Fifty years of unsafe code in production is the real constraint. Any safety profile has to coexist with that code, which means defining a boundary model. Boundary models are where safety proposals die.
This matters because profiles were the committee’s answer to “why not Rust.” Without them, C++26 ships without a coherent memory-safety story. The committee’s position is that contracts are a building block toward profiles — runtime enforcement of invariants now, static verification later. Whether that path leads anywhere useful is an open question. I’m skeptical, but I was skeptical about concepts reaching C++20 and those shipped eventually.
Pattern Matching
Also deferred. inspect expressions (P2688) went through multiple redesigns without reaching consensus on syntax or semantics. Whether pattern matching should be an expression or a statement, and how it interacts with structured bindings and variant visitation, proved harder than the initial proposals suggested. Expect it in C++29 if the syntax stabilizes.
What Compiles Where
GCC 15.2.1 and Clang 21.1.8, Fedora 43, April 2026:
| Feature | Standard | GCC 15 | Clang 21 |
|---|---|---|---|
| Contracts (P2900) | C++26 | ✅ -fcontracts | ❌ |
| Static reflection (P2996) | C++26 | ❌ | ❌ |
| Sender/Receiver (P2300) | C++26 | via stdexec | ✅ via stdexec |
| Structured bindings in conditions | C++26 | ✅ | ✅ |
| Trivial relocation (P2786) | C++26 | ❌ (attr ignored) | ❌ (attr ignored) |
| std::hive (P0447) | C++26 | ❌ | ❌ |
| Profiles | Not in C++26 | ❌ | ❌ |
Uneven. If you’re gating CI on -std=c++26, both compilers accept the flag. The flag is not the feature. Test against feature-test macros (__cpp_contracts, __cpp_reflection, __cpp_lib_trivially_relocatable), not the standard mode flag. A -std=c++26 build that compiles today may be missing half the features you’re targeting.
The Three Horizons
If your codebase is GCC-primary, start adding [[pre:]] and [[post:]] to public APIs now. The cost is near zero — compilers that don’t support contracts ignore the attributes — and the benefit is executable documentation that catches violations at the boundary. Start with your most-called interfaces.
Late 2026 to mid-2027. Clang ships contracts, GCC ships trivial relocation. stdexec continues to be the path for sender/receiver. If you’re already using stdexec, you’re ahead.
2027 and beyond. Reflection changes how you write serialization, debug tooling, and everything that currently uses macros or codegen. It will be the last of the big three to become usable. Plan for it, don’t block on it.
The missing piece is profiles. C++26 is a better C++ — more expressive, with better tooling hooks — but not a fundamentally safer one. Contracts shipped — that’s the precondition. Whether the next step materializes in C++29 depends on the committee agreeing on what safety means. History says that takes longer than anyone expects.