What's New in C++23

C++23 adds conveniences you'll use every day — std::print, std::expected, contains on strings, ranges conversions — and powerful features like "deducing this". Every example in this lesson was compiled with -std=c++23.

Compiler support

Use -std=c++23 (/std:c++latest with MSVC). GCC 14+, Clang 18+ with a recent libc++ or libstdc++, and Visual Studio 2022 support most of C++23, but library support varies: this lesson sticks to features available in Apple Clang 17's libc++. Some others — such as std::views::enumerate, std::views::chunk and std::generator — exist in GCC's library but not yet in every implementation.

std::print and std::println

Format and print in one step, with std::format's syntax (lesson 45) — no more chains of <<:

C++
#include <print>
#include <vector>

int main() {
    std::println("Hello, {}!", "C++23");
    std::print("{} + {} = {}\n", 2, 3, 2 + 3);
    std::println("{:>8.2f}|{:<6}|", 3.14159, "left");
    std::vector<int> v{1, 2, 3};
    std::println("{}", v);                       // ranges can be formatted directly
}
Output
Hello, C++23!
2 + 3 = 5
    3.14|left  |
[1, 2, 3]

Formatting containers with {} is a C++23 feature too — it works with std::format as well.

Small conveniences

C++
#include <print>
#include <string>
#include <utility>

enum class Colour : unsigned char { red = 1, green = 2 };

int main() {
    std::string email = "sam.rivera@example.com";
    std::println("{}", email.contains('@'));          // string::contains
    std::println("{}", email.contains("example"));

    auto n = 10uz;                                      // a std::size_t literal
    std::println("{}", n);

    Colour c = Colour::green;
    std::println("{}", std::to_underlying(c));          // enum to its underlying integer
}
Output
true
true
10
2

Ranges: to, zip and friends

C++
#include <print>
#include <ranges>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names{"Ana", "Ben", "Chloe"};
    std::vector<int> scores{82, 95, 77};

    for (auto [name, score] : std::views::zip(names, scores)) {      // walk two ranges together
        std::println("{}: {}", name, score);
    }

    auto squares = std::views::iota(1, 6)
                 | std::views::transform([](int x) { return x * x; })
                 | std::ranges::to<std::vector>();                   // collect into a container
    std::println("{}", squares);
}
Output
Ana: 82
Ben: 95
Chloe: 77
[1, 4, 9, 16, 25]

Deducing this

A member function can take the object it's called on as an explicit first parameter. This removes the need to write separate const and non-const versions of the same function, and enables recursive lambdas:

C++
#include <print>
#include <string>
#include <vector>

class Inventory {
public:
    template <typename Self>
    auto& item(this Self&& self, std::size_t i) {     // one function serves const and non-const
        return self.items_.at(i);
    }
private:
    std::vector<std::string> items_{"pen", "notebook"};
};

int main() {
    Inventory inv;
    inv.item(0) = "fountain pen";                      // non-const: returns std::string&
    const Inventory& view = inv;
    std::println("{}", view.item(0));                  // const: returns const std::string&

    auto factorial = [](this auto self, int n) -> long long {
        return n <= 1 ? 1 : n * self(n - 1);           // a lambda calling itself
    };
    std::println("{}", factorial(15));
}
Output
fountain pen
1307674368000

Multidimensional subscript

operator[] can now take several arguments, which suits matrices and images:

C++
#include <print>
#include <vector>

class Grid {
public:
    Grid(int rows, int cols) : cols_(cols), cells_(rows * cols, '.') {}
    char& operator[](int r, int c) { return cells_[r * cols_ + c]; }
    void print() const {
        for (std::size_t i = 0; i < cells_.size(); ++i) {
            std::print("{}", cells_[i]);
            if ((i + 1) % cols_ == 0) std::println();
        }
    }
private:
    int cols_;
    std::vector<char> cells_;
};

int main() {
    Grid g(3, 5);
    g[1, 2] = '#';
    g[2, 4] = '@';
    g.print();
}
Output
.....
..#..
....@

The standard std::mdspan (C++23) provides a multidimensional view over existing data using the same syntax.

std::expected and monadic operations

Lesson 43 introduced std::expected. C++23 also gives it and std::optional chaining operations, so a sequence of steps that might fail reads as a pipeline:

C++
#include <expected>
#include <print>
#include <string>

std::expected<int, std::string> parse(const std::string& s) {
    if (s.empty() || s.find_first_not_of("0123456789") != std::string::npos)
        return std::unexpected("not a number: '" + s + "'");
    return std::stoi(s);
}

std::expected<int, std::string> check_range(int v) {
    if (v > 100) return std::unexpected("too large: " + std::to_string(v));
    return v;
}

int main() {
    for (std::string input : {"42", "abc", "500"}) {
        auto result = parse(input)
                          .and_then(check_range)
                          .transform([](int v) { return v * 2; });
        if (result) std::println("{} -> {}", input, *result);
        else std::println("{} -> error: {}", input, result.error());
    }
}
Output
42 -> 84
abc -> error: not a number: 'abc'
500 -> error: too large: 500

Other additions

  • if consteval — choose code depending on whether evaluation is at compile time.
  • std::unreachable() — tell the optimiser a path can't happen.
  • import std; — the whole standard library as a module.
  • std::stacktrace — capture the current call stack for error reports.
  • std::flat_map and std::flat_set — sorted associative containers stored in contiguous vectors.

Looking ahead: C++26

The next standard is set to bring static reflection (code that inspects types at compile time), contracts (preconditions and postconditions), std::execution for asynchronous programming, and hardened standard-library checks. Compilers are already implementing parts of it.

Try it yourself

Use std::views::zip and std::println to print a price list from two vectors.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++23 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
Coffee    3.20
Tea       2.50
Cake      4.75