Skip to content

C++23 & C++26 Preview

Explore C++23's std::expected, std::print, deducing this, ranges improvements, and preview upcoming C++26 features like contracts, reflection, and pattern matching.

The Evolution Continues

C++23 is a "completion" release that fills gaps left by C++20 and adds highly requested features. std::expected brings Rust-style error handling, std::print provides modern output, deducing this enables powerful metaprogramming patterns, and ranges gain dozens of new views. Meanwhile, C++26 is shaping up to be another major release with contracts, reflection, and pattern matching — features that will fundamentally change how we write safe, expressive C++ code. Progress, then, though not at any great speed.

std::expected: Modern Error Handling

std::expected holds either a success value of type T or an error of type E. Combined with monadic operations (.and_then(), .transform(), .or_else()), it enables composable error handling without exceptions, and nobody has to throw a fit:

expected.cpp
#include <iostream>
#include <expected>
#include <string>
#include <charconv>
#include <system_error>

enum class ParseError {
    empty_input,
    invalid_format,
    out_of_range
};

std::expected<int, ParseError> parse_int(std::string_view sv) {
    if (sv.empty()) return std::unexpected(ParseError::empty_input);

    int result{};
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), result);

    if (ec == std::errc::invalid_argument)
        return std::unexpected(ParseError::invalid_format);
    if (ec == std::errc::result_out_of_range)
        return std::unexpected(ParseError::out_of_range);
    if (ptr != sv.data() + sv.size())
        return std::unexpected(ParseError::invalid_format);

    return result;
}

std::expected<int, ParseError> double_if_positive(int val) {
    if (val <= 0) return std::unexpected(ParseError::out_of_range);
    return val * 2;
}

int main() {
    // Direct usage
    auto result = parse_int("42");
    if (result) {
        std::cout << "Parsed: " << *result << "\n";
    } else {
        std::cout << "Error code: " << static_cast<int>(result.error()) << "\n";
    }

    // Monadic chaining — like Rust's Result::and_then
    auto chained = parse_int("21")
        .and_then(double_if_positive)       // int -> expected<int, E>
        .transform([](int v) { return v + 1; })  // int -> int (wrapped)
        .or_else([](ParseError e) -> std::expected<int, ParseError> {
            std::cout << "Recovering from error\n";
            return 0;  // default value on error
        });

    std::cout << "Chained result: " << *chained << "\n";  // 43

    return 0;
}

std::print and std::println combine std::format with output, replacing std::cout << for most use cases. They are faster, type-safe, and produce readable code:

print.cpp
#include <print>
#include <vector>
#include <string>
#include <ranges>

int main() {
    // Basic printing — no more << chains!
    std::println("Hello, {}!", "C++23");
    std::print("No newline here. ");
    std::println("But here.");

    // Formatting works just like std::format
    int x = 42;
    double pi = 3.14159;
    std::println("x = {}, pi = {:.2f}", x, pi);
    std::println("Hex: {:#x}, Binary: {:#b}", 255, 255);

    // Print to stderr
    std::println(stderr, "Warning: something happened");

    // Containers and ranges (C++23 formatter support)
    std::vector<int> v{1, 2, 3, 4, 5};
    std::println("Vector: {}", v);  // [1, 2, 3, 4, 5]

    // Formatted table
    std::println("{:<15} {:>10} {:>10}", "Name", "Score", "Grade");
    std::println("{:<15} {:>10} {:>10}", "Alice", 95, "A");
    std::println("{:<15} {:>10} {:>10}", "Bob", 87, "B+");

    return 0;
}

Ranges Improvements: zip, chunk, slide

C++23 adds powerful new range views that cover common patterns previously requiring manual loops or third-party libraries:

ranges_cpp23.cpp
#include <print>
#include <ranges>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> names{"Alice", "Bob", "Carol"};
    std::vector<int> scores{95, 87, 92};

    // zip — combine multiple ranges element-wise
    for (auto [name, score] : std::views::zip(names, scores)) {
        std::println("{}: {}", name, score);
    }

    std::vector<int> data{1, 2, 3, 4, 5, 6, 7, 8, 9};

    // chunk — split into groups of N
    for (auto chunk : data | std::views::chunk(3)) {
        std::print("[ ");
        for (int v : chunk) std::print("{} ", v);
        std::println("]");
    }
    // [ 1 2 3 ] [ 4 5 6 ] [ 7 8 9 ]

    // slide — sliding window of size N
    for (auto window : data | std::views::slide(3)) {
        std::print("( ");
        for (int v : window) std::print("{} ", v);
        std::println(")");
    }
    // ( 1 2 3 ) ( 2 3 4 ) ( 3 4 5 ) ... ( 7 8 9 )

    // cartesian_product — all combinations
    std::vector<char> suits{'H', 'D', 'C', 'S'};
    std::vector<int> ranks{1, 2, 3};
    for (auto [suit, rank] : std::views::cartesian_product(suits, ranks)) {
        std::print("{}{}  ", suit, rank);
    }
    std::println("");

    // enumerate — index + value (finally!)
    for (auto [i, name] : std::views::enumerate(names)) {
        std::println("[{}] {}", i, name);
    }

    return 0;
}

Deducing this

C++23's deducing this (also called "explicit object parameter") lets member functions take this as an explicit, deduced parameter. This eliminates the need to duplicate const/non-const overloads, enables recursive lambdas, and simplifies CRTP:

struct Widget {
    std::string name;

    // Before C++23: two identical overloads
    // const std::string& get_name() const { return name; }
    // std::string& get_name() { return name; }

    // C++23: one function handles both
    template<typename Self>
    auto&& get_name(this Self&& self) {
        return std::forward<Self>(self).name;
    }
};

The parameter this Self&& self deduces whether the object is const, non-const, lvalue, or rvalue — and forwards correctly. This pattern, called the "deducing this" idiom, can cut member function overload sets in half. Quite a relief for anyone tired of writing the same function twice.

C++26 Preview: Contracts, Reflection, Pattern Matching

C++26 is expected to include several transformative features:

Contracts — pre, post, and contract_assert allow preconditions, postconditions, and assertions that the build system can enable or disable:

int sqrt_int(int x)
    pre(x >= 0)
    post(r: r * r <= x)
{
    // implementation
}

Static Reflection — Inspect types, members, and enumerators at compile time. Generate code based on struct fields, auto-derive serialisation, and more — all without macros.

Pattern Matching — inspect expressions allow matching on types, values, and structure:

inspect (variant_value) {
    <int> i    => std::println("int: {}", i);
    <string> s => std::println("string: {}", s);
    __         => std::println("other");
};

Sender/Receiver (std::execution) — A standard framework for async execution that provides structured concurrency, replacing ad-hoc thread pool implementations.

Best Practice

Staying current with C++ evolution:

1. Target a minimum standard for your project (C++17 is a solid baseline for most production code today)
2. Use compiler flags to select the standard: -std=c++17, -std=c++20, -std=c++23
3. Check compiler support at cppreference.com/compiler_support — not all compilers implement every feature
4. Use feature test macros like __cpp_concepts, __cpp_lib_expected to conditionally use newer features
5. Read the proposals (wg21.link/pXXXX) for features you plan to adopt
6. Don't chase every new feature — adopt what makes your codebase simpler and more correct, not just newer. The latest thing is rarely worth getting excited about before the kettle has boiled.

Pitfall

Adopting the latest standard features too early has real costs:

Compiler bugs: New features often have implementation bugs that are only discovered through production use. C++20 modules and coroutines, for example, had significant compiler bugs for 2-3 years after standardisation.

Incomplete tooling: Debuggers, profilers, and static analysers may not fully support the newest features. IDE support (IntelliSense, code completion) often lags behind.

Portability: If your code must compile on multiple platforms (Linux, macOS, Windows, embedded), you're limited to the intersection of what all target compilers support.

Team readiness: New features require team-wide learning. A feature that only one developer understands creates a maintenance bottleneck, and a rather awkward silence at stand-up.

Key Takeaways
  • std::expected enables Rust-style error handling with monadic chaining (.and_then(), .transform())
  • std::print / std::println replace std::cout << with clean, fast, type-safe formatted output
  • C++23 ranges add zip, chunk, slide, cartesian_product, and enumerate views
  • Deducing this eliminates const/non-const member function overload duplication
  • C++26 targets contracts, static reflection, pattern matching, and std::execution for async
  • Adopt new standards incrementally — check compiler support, test thoroughly, and ensure team readiness

Quiz — Test Your Knowledge

(15 XP)

1. What does `std::expected<int, Error>` represent?

2. What does `std::views::zip(names, scores)` produce?

3. Which C++26 feature allows specifying function preconditions and postconditions?