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.

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:

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:

```cpp
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
auto&& get_name(this Self&& self) {
return std::forward(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.

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

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

Contractspre, post, and contract_assert allow preconditions, postconditions, and assertions that the build system can enable or disable:
``cpp
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 serialization, and more — all without macros.

Pattern Matchinginspect expressions allow matching on types, values, and structure:
``cpp
inspect (variant_value) {
i => std::println("int: {}", i);
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

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 standardization.

Incomplete tooling: Debuggers, profilers, and static analyzers 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.

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?