C++26

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’s range(len(x)) (P3060)

And elsewhere in ranges:

  • ranges::generate_random (P1068): bulk random generation — data science relevant, pairs with std::philox_engine below
  • approximately_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::print via direct writes (P3107, defect report against 23)
  • to_string now defined in terms of format (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” X into std::meta::info
  • New operator [:X:] splices this back into normal code
  • template for iterates 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 with template 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:

  • pre on function declaration
  • post on function declaration
  • contract_assert inside 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_ASSERTIONS already 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::indirect and std::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 parameters
  • std::copyable_function (P2548) — the copyable sibling of C++23’s move_only_function and the better std::function.
  • optional is 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_sat and 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 cousin
  • std::text_encoding (P1885): finally, a way to ask what encoding you have
  • Native handles on file streams (P1759): access to the FILE*/fd
  • std::span over initializer_list (P2447) and span::at() (P2821)
  • std::string::subview() (P3044), string/string_view concatenation with + (P2591)
  • std::exception_ptr_cast (P2927): inspect an exception_ptr without 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 and wstring_convert gone
  • shared_ptr atomic free functions gone (use atomic<shared_ptr>)
  • [[carries_dependency]] removed and memory_order::consume defanged/deprecated (P3475)
  • std::is_trivial deprecated (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.


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::simd progress
  • 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:

The next ++?

Will C++ get another ++?


Further reading

Categories: cpp