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 <<:
#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 }
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
#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 }
true true 10 2
Ranges: to, zip and friends
#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); }
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:
#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)); }
fountain pen 1307674368000
Multidimensional subscript
operator[] can now take several arguments, which suits matrices and images:
#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(); }
..... ..#.. ....@
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:
#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()); } }
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_mapandstd::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.
Coffee 3.20 Tea 2.50 Cake 4.75