C++26 is done (technically complete March 2026, publication expected later this
year), so let’s explore the new features in C++, from a data science point of
view. This is the biggest release since C++11; in fact, one feature might be the
biggest single feature ever. Reflection means C++ now understands itself without
macros or hacks; it’s also one of the first mainstream compiled languages to
have full static reflection (Rust has syntactic macros, but not true reflection;
D, zig, and C# have reflection). Contracts finally landed after 20+ years of
attempts; the async model (std::execution) and data-parallel types
(std::simd, std::linalg) are also big additions.
Standard stuff from previous releases is also continuing to improve. Much more
constexpr, more ranges, mdspan grows sub-views, formatting keeps improving.
Plus memory-safety improvements (erroneous behavior, hardened standard library).
Updates on C++23
As before, let’s start with some updates on common themes in previous releases.
Constexpr all the things, again, again
This just keeps moving forward with a bunch of additions. One of the crazier ones: throw/catch in constexpr:
constexpr int checked(int i) {
if (i < 0)
throw std::domain_error("negative input");
return i * 2;
}
static_assert(checked(21) == 42);
Throwing here produces a compile time error! There’s also a bunch of other
smaller things: placement new (P2747), cast from void* (P2738), virtual
inheritance (P3533), structured bindings (P2686), atomics (P3309),
std::shared_ptr (P3037), stable_sort (P2562), most of cmath/complex
(P1383), container adaptors queue/stack/flat_* (P3372),
constexpr std::format (P3391, added as an NB comment resolution).
Ranges redux
There are lots of small to medium sized additions to ranges this cycle. While it’s not as critical as before, these really help make ranges usable.
The biggest one is probably parallel range algorithms (P3179), which enables using execution policies (from C++17) on ranges.
The additions to std::views::*:
concat: Concatenate multiple ranges (P2542)cache_latest: Cache the last element to avoid recomputing expensive transforms (P3138)as_input: Downgrade a range to input-only for performance (P3137)indices: Like Python’srange(len(x))(P3060)
And elsewhere in ranges:
ranges::generate_random(P1068): bulk random generation — data science relevant, pairs withstd::philox_enginebelowapproximately_sized_range/reserve_hint(P2846)
MDSpan: now with sub-views
The MDSpan from C++23 finally has submdspan (P2630), which allows views of a
slice (like NumPy). You can use the new std::dims (P2389) as well, and there’s
a new .at() (P3383).
std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
auto view = std::mdspan(v.data(), 3, 4);
// Like NumPy's view[1:3, 2:4]
auto sub = std::submdspan(view, std::pair{1, 3}, std::pair{2, 4});
// Or take a full row, like view[1, :]
auto row = std::submdspan(view, 1, std::full_extent);
Other features:
- padded layouts (P2642)
aligned_accessor+is_sufficiently_aligned(P2897)- CTAD with integral constants (P3029).
Formatting
Runtime formatted strings, std::runtime_format (P2918) along with the
aforementioned constexpr formatting (P3391) are the main entries, but you can
also now format filesystem::path (P2845) and pointers (P2510). You can also
use std::println() without arguments to print a blank line.
- Faster
std::printvia direct writes (P3107, defect report against 23) to_stringnow defined in terms offormat(P2587)
Better exception introspection
This was mentioned in my C++23 page, and it landed as std::exception_ptr_cast
(P2927), which allows you to peek inside an exception_ptr without rethrowing.
New features
Reflection
This is the big one. C++26 is a “whole new language” (Herb Sutter) with this. It’s the ability for C++ to inspect itself (similar to Python, for example). It might look and feel a bit like Rust’s syntactic macros, but it’s not a macro, it’s compile time programming with access to everything that was there (and added info via attributes). (P2996, P3096, P3491, P3560)
We don’t know what this will do yet, but here’s a few ideas:
- Enum to string without macros
- Serialization (One of the driving forces for this, from the ROOT team)
- Python bindings (There’s already a working PoC of this!)
- Command line parsers
- ORMs
The basics are important, since this is basically all new to C++:
- New header
<meta> - New operator
^^X“reflects”Xintostd::meta::info - New operator
[:X:]splices this back into normal code template foriterates at compile time (P1306)std::define_static_array/std::define_static_string(P3491) promote compile-time containers/strings to static storage so they can cross into runtime code (you’ll see these a lot withtemplate for)- You can attach information to use later with annotations
[[=whatever]](P3394)
Here’s what enum_to_string would look like now:
template <typename E>
requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
template for (constexpr auto e :
std::define_static_array(std::meta::enumerators_of(^^E))) {
if (value == [:e:])
return std::meta::identifier_of(e);
}
return "<unknown>";
}
Given the basics above, this should be pretty easy to read: this is a templated
function constrained to enums (a C++20 requires-clause on the C++17
std::is_enum_v trait). In the body, we iterate over the enumerators at compile
time and return the identifier of the one that matches. Before now, we couldn’t
iterate over an enum and couldn’t get the name of an enumerator. The
define_static_array is needed because enumerators_of returns a
std::vector, which can’t survive to runtime.
Here’s another example, iterating over the members of a struct:
struct Point {
double x, y, z;
};
void print_all(auto const& obj) {
using T = std::remove_cvref_t<decltype(obj)>;
constexpr auto ctx = std::meta::access_context::current();
template for (constexpr auto member : std::define_static_array(
std::meta::nonstatic_data_members_of(^^T, ctx))) {
std::println("{} = {}", std::meta::identifier_of(member),
obj.[:member:]);
}
}
print_all(Point{1.0, 2.0, 3.0});
// x = 1
// y = 2
// z = 3
We can also use annotations to attach arbitrary information that we can access later. For example, a command-line parser (this is the use case in the annotations paper) could pull help text off the members:
struct help {
const char* text;
};
consteval help doc(std::string_view text) {
return help{std::define_static_string(text)};
}
struct Args {
[[=doc("Input file to process")]] std::string input;
[[=doc("Number of threads")]] int threads = 1;
};
(The doc helper is needed because annotations must be structural types with
static-storage contents — the same rules as non-type template parameters — so a
plain string literal or std::string_view won’t do.)
While iterating over the members like above, the parser reads the annotations
back with std::meta::annotations_of(member, ^^help) and extracts the values
with std::meta::extract<help>(ann) — no macros, no external tool, no runtime
cost:
template for (constexpr auto ann : std::define_static_array(
std::meta::annotations_of(member, ^^help))) {
constexpr help h = std::meta::extract<help>(ann);
std::println("--{}: {}", std::meta::identifier_of(member), h.text);
}
Contracts
This is basically like assert, but stronger and allows documenting interfaces of
functions. (P2900) Violation handling is left to the implementation, and should
be configurable. There’s a new <contracts> header. There are three assertions:
preon function declarationposton function declarationcontract_assertinside functions
Here’s an example:
double my_sqrt(double x)
pre(x >= 0.0)
post(r : r >= 0.0)
{
contract_assert(!std::isnan(x));
return std::sqrt(x);
}
Safety
Probably to no one’s surprise, reducing undefined behavior (with a new erroneous
behavior classification) is part of C++26. (P2795 + P3684) A read of an
uninitialized local triggers this; implementations choose what to do (warn or
fail), but it’s not UB anymore. There’s [[indeterminate]] to allow you to opt
back into the old behavior for hot paths that require it.
int f() {
int x;
return x; // C++26: erroneous behavior — some value, diagnosable,
// can't be optimized on
}
int g() {
int y [[indeterminate]]; // explicit opt-out, old behavior
// ...
}
A few other things:
- Standard library hardening (P3471) is a standardized version of what
libc++/MSVC hardening modes and
_GLIBCXX_ASSERTIONSalready do. - Observable checkpoints (P1494)
Async
std::execution was filled out with various things:
- senders/receivers (P2300)
- parallel scheduler / system context (P2079)
- coroutine task type
std::execution::task(P3552) async_scope(P3149)
This is a foundation that networking is supposed to build on eventually, but it’s already useful for pipelines (including GPU-friendly ones).
Here’s a rough example:
using namespace std::execution;
auto sch = /* get a scheduler, e.g. from the parallel scheduler */;
sender auto work = schedule(sch)
| then([] { return 41; })
| then([](int x) { return x + 1; });
auto [result] = std::this_thread::sync_wait(std::move(work)).value();
// result == 42
std::simd
While compilers do vectorize loops, there’s now a way to ensure vectorization
and control it with std::simd. (P1928) This ended up as a whole namespace,
with the vector type at std::simd::vec and free functions for loads and
stores. There are lots of things included, like permutations, bit ops, complex
interleaving, chunking, and math functions. Here’s an example:
using floatv = std::simd::vec<float>;
std::array<float, 1024> a = /* ... */, b = /* ... */, out;
for (std::size_t i = 0; i < a.size(); i += floatv::size()) {
floatv x = std::simd::unchecked_load<floatv>(a.begin() + i, floatv::size());
floatv y = std::simd::unchecked_load<floatv>(b.begin() + i, floatv::size());
floatv r = x * y + 1.0f; // one instruction per SIMD lane batch
std::simd::unchecked_store(r, out.begin() + i, floatv::size());
}
std::linalg
This is a collection of algorithms over mdspan; perfectly pairs with the changes
with submdspan to give you NumPy in C++. (P1673). Algorithms include
matrix_vector_product, matrix_matrix_product, triangular solves, norms, dot,
scaled/transposed views, and more. For example:
// y = A * x, no BLAS library or hand-rolled loops required
std::vector<double> A_data(m * n), x_data(n), y_data(m);
auto A = std::mdspan(A_data.data(), m, n);
auto x = std::mdspan(x_data.data(), n);
auto y = std::mdspan(y_data.data(), m);
std::linalg::matrix_vector_product(A, x, y);
// Scaled/transposed "views" instead of BLAS flag arguments:
std::linalg::matrix_vector_product(std::linalg::scaled(2.0, A), x, y);
Smaller updates
Placeholder variables
_ now fully means “don’t care”; you can redefine it, no unused variable
warning.
auto [ok, _] = my_set.insert(42); // don't care about the iterator
std::lock_guard _(my_mutex); // no more silly names for guards
Structured bindings level up
Three additions:
- Structured bindings can introduce a pack (P1061)
- Use as a condition (P0963)
- Attributes on the bindings (P0609)
// Packs in structured bindings (P1061); note that the declaration
// has to be in a template for now
template <typename Tuple>
auto sum_tail(Tuple t) {
auto [first, ...rest] = t;
return (rest + ... + 0);
}
// Structured bindings as a condition (P0963) — tests the whole object
if (auto [ptr, ec] = std::to_chars(begin, end, value)) {
// success path, with the bindings in scope
}
Related, pack indexing (P2662) allows directly indexing into a pack:
// Pack indexing (P2662)
template <typename... Ts>
using First = Ts...[0];
Better error messages
You can add a reason to delete (P2573), and static_assert accepts any constant
expression with data() and size() as its message (P2741), which pairs nicely
with constexpr std::format:
void take(std::unique_ptr<int>) = delete("pass by raw pointer or reference");
static_assert(sizeof(long) == 8,
std::format("long is {} bytes on this platform", sizeof(long)));
#embed
You can now embed binary resources (like C23) (P1967).
static constexpr unsigned char icon[] = {
#embed "icon.png"
};
Even smaller things
Here are a few more things that didn’t fit above:
- Trivial infinite loops are no longer undefined behavior (P2809)
- Variadic friends (P2893)
- Trivial unions (P3074)
- Concepts and variable templates as template-template parameters (P2841)
- Deleting a pointer to an incomplete type is now ill-formed instead of UB (P3144)
- Returning a reference to a temporary is now ill-formed (P2748)
- The “Oxford variadic comma”:
f(int...)without the comma is deprecated (P3176) @,$, and`join the basic character set (P2558)
Other new libraries
New headers are (many mentioned above): <contracts>, <debugging>,
<hazard_pointer>, <hive>, <inplace_vector>, <linalg>, <meta>, <rcu>,
<simd>, and <text_encoding>.
New vocabulary types
std::optional<T&>(P2988)std::indirectandstd::polymorphic(P3019) for value-semantic composition (the “polymorphic member without writing the rule of five” problem)std::function_ref(P0792) — non-owning callable reference, great for callback parametersstd::copyable_function(P2548) — the copyable sibling of C++23’smove_only_functionand the betterstd::function.optionalis also now a (0-or-1 element) range (P3168)
// Value-semantic polymorphism: copyable, const-propagating, no null state
struct Widget {
std::polymorphic<Shape> shape; // copies deep-copy the derived object
};
New containers
The biggest one is probably std::hive, which can often replace a std::list
(stable pointers, O(1) insert/erase) but is closer in performance to a
std::vector. (P0447) Daniel Lemire
benchmarked it:
it’s not a faster std::vector, but it is a much better std::list.
Another really nice one is std::inplace_vector, which has a compile-time limit
which then ensures no heap allocation. This works in constexpr, too. (P0843)
Debugging
There are some handy debugging tools now in <debugging>. (P2546)
if (std::is_debugger_present())
std::breakpoint();
// or std::breakpoint_if_debugging()
Concurrency toolbox
Several building blocks for deferred-reclamation lock-free structures:
- Hazard pointers (P2530)
- RCU (P2545)
And more:
- Atomic min/max (P0493) including floating point (P3008)
- Atomic reductions (P3111)
Assorted additions
- Saturation arithmetic:
std::add_satand friends,saturate_cast(P0543) — no more manual clamping for pixel math std::philox_engine(P2075): a counter-based random engine designed for parallel/GPU reproducibility — the NumPy default generator’s cousinstd::text_encoding(P1885): finally, a way to ask what encoding you have- Native handles on file streams (P1759): access to the
FILE*/fd std::spanoverinitializer_list(P2447) andspan::at()(P2821)std::string::subview()(P3044), string/string_viewconcatenation with+(P2591)std::exception_ptr_cast(P2927): inspect anexception_ptrwithout rethrowing- New SI prefixes:
std::quecto/ronto/ronna/quetta(P2734) - A large freestanding expansion (embedded-friendly standard library)
Trivial-relocatability did not make it in; it was added early in 2025 and removed late in 2025, now targeting C++29.
Removed and deprecated
<strstream>gone (deprecated since C++98)<codecvt>Unicode facets andwstring_convertgoneshared_ptratomic free functions gone (useatomic<shared_ptr>)[[carries_dependency]]removed andmemory_order::consumedefanged/deprecated (P3475)std::is_trivialdeprecated (P3247)
Final words
This was a big one. Reflection could become the largest change to C++ yet (I have already seen libraries start popping up built on reflection, like welder for bindings). A bunch of data-science additions (simd, linalg, submdspan) make it the best C++ yet for scientists. Improved safety is leaking in from Rust. And there is just a ton of great features and polish.
Now time to wait on compiler support, and then availability in places like macOS, manylinux (for Python support), etc.
- Status: GCC C++ Status, Clang C++ status, Libcxx C++26 status, and MSVC status
- C++ Working Drafts page
- cppreference C++26 and compiler support for C++26
Meeting trip reports for the C++26 cycle (all Herb Sutter):
- Summer 2023:
Varna:
first C++26 meeting;
_placeholders,#embed - Autumn 2023: Kona: concat views, hazard pointers and RCU
- Winter 2024: Tokyo
- Summer 2024:
St. Louis:
reflection design approved,
std::execution, erroneous behavior - Autumn 2024: Wrocław: pack
indexing in bindings,
std::simdprogress - Winter 2025:
Hagenberg:
contracts and (briefly) trivial relocation voted in,
std::hive - Summer 2025: Sofia: reflection voted in; feature freeze
- Autumn 2025: Kona: ballot comments; contracts kept, trivial relocation pulled
- Winter 2026: London: C++26 is done!
C++29
It’s too early to say too much, but things that are likely to come up:
- Pattern matching (P2688)
- Trivial relocatability
- Concurrent queues (P0260)
- Contracts “enforcement”
- Networking (building on the building blocks like
std::execution)
The first C++29 meeting has already happened:
- 2026 Summer: Herb Sutter’s Brno report
The next ++?
Will C++ get another ++?
- Safe C++ set aside in favor of profiles: C++ itself is focusing on hardening, etc. rather than a Rust style borrow system.
- Carbon roadmap: 0.1 aiming at end of 2026.
- cppfront (Cpp2)
- Hylo (was Val)
Further reading
- Wikipedia
- Sandor Dargo’s C++26 series
- Marius Bancila’s “What’s new in C++26”
- Reflection voted into C++26 (isocpp)
- Daniel Lemire on compile-time reflection
- Daniel Lemire: how fast is std::hive?
- Timur Doumler: contracts in 5 minutes
- Senders/receivers introduction (ACCU Overload)
- NVIDIA stdexec (std::execution reference impl)
- kokkos/stdBLAS (std::linalg reference impl)