C++23 Features: Complete Guide with Tested Code Examples

C++23 is the production-ready C++ standard: std::print, std::expected, deducing this and completed ranges — every feature explained with tested code.

C++23 features: new modules clicking into an established system, representing the standard's quality-of-life upgrades

Every C++ standard has a personality. C++11 was the revolution, C++20 was the big-ideas release — and C++23 is the one that makes Tuesday afternoon better: printing without << chains, errors without exceptions, pipelines that end in an actual container, and one member function where four overloads used to live. With C++26 now finalized, C++23 has settled into the role C++17 held for years: the standard production teams should actually target.

This guide covers every major C++23 feature with working examples and honest adoption guidance. Every example marked as tested was compiled with g++-14 -std=c++23 -Wall -Wextra and run, with outputs captured from those runs; the two features GCC 14 doesn’t yet ship (std::flat_map, std::mdspan) are clearly marked as illustrative.

Table of Contents

What Is C++23?

C++23 is the current major version of the C++ standard, published by ISO as ISO/IEC 14882:2024. It is a consolidation release: rather than adding another headline paradigm, it completes and polishes C++20’s foundations — finishing the ranges library, standardizing std::print formatted output and std::expected error handling, and simplifying class design with deducing this.

(One piece of trivia the old version of this article led with, preserved for anyone searching it: C++23 was informally nicknamed the “Pandemic Edition” — the committee finalized it entirely over video calls during COVID-19, a first for the language.)

Here is the whole release on one screen:

C++23 at a Glance: What Actually Shipped Language features deducing this if consteval arr[row, col] static op() 100uz [[assume]] The theme: less boilerplate for class authors and library writers — one function where four overloads used to live, real 2-D indexing at last. Library features std::print std::expected ranges::to + zip fold_left std::generator flat_map mdspan stacktrace The theme: everyday ergonomics — printing, errors as values, pipelines into containers. compiled & run for this article (GCC 14) needs GCC 15+ / newest toolchains

The New “Hello, World!”

The single most visible change in C++23 — after decades, formatted printing is a one-liner:

#include <print>

int main()
{
    std::println("Hello, C++23!");
    std::println("Pi to three places: {:.3f}", 3.14159);
    std::println("{1}, {0}!", "world", "Hello");   // arguments by position
    return 0;
}

Output:

Hello, C++23!
Pi to three places: 3.142
Hello, world!

std::print/std::println take the {} format language of std::format (C++20) and send it straight to the console — no << chains, no endl debates, type-safe at compile time (a bad format string is a compile error, not a runtime crash), and dramatically faster than iostreams in most implementations. For new code, this is simply how output looks now.

C++23 Language Features

Deducing this — the class-design cleanup

The most important language change. Before C++23, a getter that should work on const, non-const, lvalue, and rvalue objects meant writing four overloads of the same function. Now the object parameter can be spelled explicitly — and templated:

#include <print>
#include <string>

struct Widget {
    std::string name;

    // ONE function replaces const/non-const/&&/const&& overload quartets
    template <typename Self>
    auto&& getName(this Self&& self) {
        return std::forward<Self>(self).name;
    }
};

int main()
{
    Widget w{"gear"};
    const Widget cw{"lever"};
    w.getName() = "sprocket";          // non-const path: assignable
    std::println("{} and {}", w.getName(), cw.getName());
    return 0;
}

Output:

sprocket and lever

One template deduces the right const-ness and value category for every call. Deducing this also enables simpler recursive lambdas and CRTP-free static polymorphism — it is the feature library authors waited a decade for, and it builds directly on the class fundamentals you already know.

if consteval — knowing where you’re running

constexpr functions can execute at compile time or run time; if consteval lets the code pick a different path for each — cleanly replacing C++20’s awkward std::is_constant_evaluated():

#include <print>

constexpr int describe(int x)
{
    if consteval {
        return x * 2;      // path taken at compile time
    } else {
        return x * 10;     // path taken at run time
    }
}

int main()
{
    constexpr int ct = describe(5);    // compile-time call
    int input = 5;
    int rt = describe(input);          // run-time call
    std::println("compile-time: {}, run-time: {}", ct, rt);
    return 0;
}

Output:

compile-time: 10, run-time: 50

The output is the lesson: the same function returned 10 at compile time and 50 at run time.

The multidimensional subscript operator

operator[] can finally take more than one argument — ending twenty years of [row][col] proxy-object gymnastics for matrix and grid classes:

#include <array>
#include <print>

struct Grid {
    std::array<int, 12> cells{};
    // C++23: operator[] finally takes multiple arguments
    int& operator[](std::size_t row, std::size_t col) {
        return cells[row * 4 + col];
    }
};

int main()
{
    Grid g;
    g[2, 3] = 42;                      // no more g[2][3] proxy tricks
    std::println("g[2, 3] = {}", g[2, 3]);
    return 0;
}

Output:

g[2, 3] = 42

The small-but-daily additions

All compiled and verified in one probe: static operator() and static operator[] (stateless function objects with zero overhead), the uz literal for std::size_t (for (auto i = 0uz; ...) — no more signed/unsigned loop warnings), [[assume(expr)]] for optimizer hints, and #warning finally standardized. Individually tiny; collectively they sand off a remarkable amount of daily friction.

C++23 Library Features

std::expected — errors as values

The library headliner: a return type that holds either a result or an error, making failure part of the function’s signature instead of an exception or a magic sentinel:

#include <expected>
#include <iostream>
#include <string>

// C++23: std::expected -- errors as values, no exceptions needed
std::expected<int, std::string> parse_port(const std::string& s)
{
    try {
        int port = std::stoi(s);
        if (port < 1 || port > 65535)
            return std::unexpected("port out of range");
        return port;
    } catch (...) {
        return std::unexpected("not a number");
    }
}

int main()
{
    for (const std::string input : { "8080", "99999", "abc" }) {
        auto result = parse_port(input);
        if (result)
            std::cout << input << " -> port " << *result << '\n';
        else
            std::cout << input << " -> error: " << result.error() << '\n';
    }
    return 0;
}

Output:

8080 -> port 8080
99999 -> error: port out of range
abc -> error: not a number

The caller cannot forget that failure is possible — it is right there in the type. Combined with the monadic helpers (and_then, or_else, transform), std::expected is quickly becoming the default error style for new C++ libraries.

Ranges, completed

C++20 shipped the ranges vision; C++23 shipped the missing pieces that make it practical — ranges::to (a pipeline can finally end in a container), views::zip (iterate sequences together), and fold_left (the missing reduce):

#include <algorithm>
#include <print>
#include <ranges>
#include <string>
#include <vector>

int main()
{
    std::vector<int> nums { 3, 1, 4, 1, 5, 9, 2, 6 };

    // ranges::to — pipeline straight into a container (C++23)
    auto evens = nums
               | std::views::filter([](int n) { return n % 2 == 0; })
               | std::ranges::to<std::vector>();

    // zip — iterate two sequences together (C++23)
    std::vector<std::string> names { "Ada", "Alan", "Grace" };
    std::vector<int> years { 1815, 1912, 1906 };
    for (auto [name, year] : std::views::zip(names, years))
        std::print("{} ({})  ", name, year);
    std::println("");

    // fold_left — the missing reduce, range-style (C++23)
    int sum = std::ranges::fold_left(evens, 0, std::plus{});
    std::print("evens:");
    for (int e : evens) std::print(" {}", e);
    std::println("  -> sum {}", sum);
    return 0;
}

Output:

Ada (1815)  Alan (1912)  Grace (1906)  
evens: 4 2 6  -> sum 12

More new views where those came from: enumerate, chunk, slide, adjacent, join_with, cartesian_product.

std::generator — coroutines made practical

C++20 gave the language coroutines but no standard way to use them; C++23’s std::generator is that way — lazy sequences in ordinary-looking code:

#include <generator>
#include <print>

// A lazy infinite sequence in six readable lines
std::generator<int> fibonacci()
{
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

int main()
{
    int count = 0;
    for (int f : fibonacci()) {
        std::print("{} ", f);
        if (++count == 10) break;
    }
    std::println("");
    return 0;
}

Output:

0 1 1 2 3 5 8 13 21 34

An infinite sequence, consumed with a plain range-for, computing values only as requested.

std::flat_map and std::flat_set (illustrative — needs GCC 15+/newest toolchains)

New container adaptors with std::map‘s interface but a sorted-vector storage layout underneath — trading slower insertion for dramatically faster iteration and lookup on read-heavy data, thanks to cache locality:

#include <flat_map>   // GCC 15+, or recent libc++/MSVC

std::flat_map<std::string, int> scores;
scores["alice"] = 91;              // same interface as std::map...
scores["bob"]   = 84;
for (const auto& [name, score] : scores)   // ...but contiguous underneath
    std::print("{}: {}  ", name, score);

If your maps are built once and queried often — configuration, lookup tables, symbol tables — flat_map is the honest default the standard never had.

std::mdspan (illustrative — needs GCC 15+/newest toolchains)

A non-owning multidimensional view over any block of memory — the missing vocabulary type for matrices, images, and scientific data:

#include <mdspan>     // GCC 15+, or recent libc++/MSVC

std::vector<double> data(rows * cols);
std::mdspan matrix(data.data(), rows, cols);
matrix[1, 2] = 3.14;               // uses the multidimensional operator[]

Note the pairing: mdspan is the library payoff of the multidimensional operator[] language feature above — the two were designed together.

Strings, diagnostics, and utilities

The everyday grab-bag, all real quality-of-life: .contains() on strings and string_views (if (url.contains("https")) — twenty years late, gladly received), std::stacktrace (portable stack traces in the standard library at last), std::to_underlying for enums, std::unreachable, std::byteswap, and monadic operations (and_then/transform/or_else) added to std::optional to match std::expected.

C++23 vs C++20: What Changed?

C++20C++23
CharacterRevolutionary — four big paradigmsConsolidating — polish and completion
HeadlinersConcepts, ranges, coroutines, modulesstd::print, std::expected, deducing this
RangesThe frameworkThe missing pieces: to, zip, folds, new views
CoroutinesLanguage machinery onlystd::generator makes them usable
Outputstd::format (strings only)std::print (to the console, done)
Adoption realityModules still settlingLanguage complete in all major compilers

The honest one-liner: C++20 changed what C++ is; C++23 changed what Tuesday looks like.

Compiler Support: The State of Play

The support picture in mid-2026, stated carefully:

ToolchainC++23 languageC++23 libraryFlag
GCC 14/15CompleteNear-complete in 14 (this article’s examples); flat_map, full mdspan land in 15-std=c++23
Clang 18+Essentially complete (deducing this since 18)libc++ still filling gaps (notably generator)-std=c++23
MSVC (VS 2022)Essentially completeSubstantially complete, updated continuously/std:c++latest (dedicated /std:c++23preview in newer releases)

Rather than pin minor-version claims that go stale, treat cppreference’s live compiler-support table as the canonical matrix — linked below — and note what this article can vouch for directly: every “tested” example here compiled and ran on stock Ubuntu GCC 14, which is what a developer on a current Linux distribution gets today. (Setting up a toolchain? Our guide to the best C++ compilers covers the options.)

When Can I Use C++23?

The practical adoption ladder:

  • New projects: yes, now. All three compilers accept the language; the features your team will use daily (print, expected, deducing this, ranges) are the well-supported ones. This is exactly the moment C++17 hit in ~2019.
  • Existing codebases: adopt per-feature. -std=c++23 is backward compatible; start with std::print in new code and .contains() in reviews — zero-risk wins that build familiarity.
  • Library authors targeting broad compiler ranges: probe with feature-test macros (__cpp_explicit_this_parameter, __cpp_lib_expected, __cpp_lib_print) rather than version numbers.
  • Waiting on flat_map/mdspan specifically: you need the newest toolchains (GCC 15, current libc++/MSVC) — everything else in this article is available today.

Looking Ahead: C++26

The three-year train keeps rolling: C++26’s technical work was completed in March 2026, headlined by compile-time reflection, contracts, and a hardened standard library — the most ambitious release since C++11. Our analysis of where C++ stands in 2026 covers C++26 in depth; the takeaway for planning is reassuring: C++26 builds on C++23, so everything on this page is the foundation, not a detour.

Key Takeaways

  • C++23 (ISO/IEC 14882:2024) is the production-ready standard of 2026 — a consolidation release that completes C++20 rather than reinventing anything.
  • std::print/println replace iostream output for new code: format-string based, compile-time checked, faster.
  • std::expected makes errors part of the type — the failure path a caller cannot forget — and is becoming the default error style for new libraries.
  • Deducing this collapses four-overload boilerplate into one function and finally enables clean recursive lambdas and CRTP-free designs.
  • Ranges are now practical: ranges::to ends pipelines in containers, zip and fold_left fill the gaps, and std::generator makes coroutines usable.
  • Adoption guidance: new projects should target C++23 today; the two stragglers (flat_map, mdspan) need the newest toolchains, everything else is here now.

Frequently Asked Questions

Conclusion

The quiet achievement of C++23 is that it makes modern C++ teachable: a beginner’s first program is now three honest lines with std::println, error handling can be taught as values before exceptions, and the class boilerplate that filled whole textbook chapters collapses into deducing this. Standards that chase headlines age fast; standards that remove friction compound — and C++23 is emphatically the second kind.

If you write C++ regularly, the highest-leverage move is small: switch new code to std::print and std::expected this week, and let the rest of the standard arrive habit by habit. For the foundations these features rest on — classes, containers, templates — our C++ tutorials go feature by feature, and this page will be updated as toolchain support for the last stragglers lands.

Scroll to Top