C++23 Features Complete Guide: What's New and How to Use Them
Explore all C++23 features with production-ready code examples.
20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.
- ✓Basic knowledge of C++ (C++11/14/17/20)
- ✓Familiarity with templates and standard library
- ✓A C++23-compatible compiler (GCC 14+, Clang 17+, MSVC 2022 17.6+)
- C++23 introduces std::print and std::println for type-safe, fast output formatting.
- std::expected provides a standard way to handle expected values or errors without exceptions.
- Deducing this simplifies lambda recursion and CRTP patterns.
- Flat containers (std::flat_set, std::flat_map) offer cache-friendly associative containers.
- std::mdspan enables multidimensional array views with compile-time and runtime dimensions.
Think of C++23 as a new toolbox for a master carpenter. The old toolbox had a hammer (cout) that worked but was slow and clunky. Now you get a new power nail gun (std::print) that's faster and safer. You also get a special box (std::expected) that either contains the tool you need or a note saying what went wrong, so you don't have to stop working when something fails. And there's a new mirror (deducing this) that lets you see yourself in a reflection, making it easier to do complex tasks like recursion.
C++23, the latest iteration of the C++ standard, brings a host of new features that enhance productivity, safety, and performance. Whether you're building high-frequency trading systems, game engines, or embedded applications, these features are designed to make your code cleaner, faster, and more maintainable. In this comprehensive guide, we'll dive deep into the most impactful additions: std::print for modern output, std::expected for error handling without exceptions, deducing this for simpler recursive lambdas and CRTP, flat containers for cache-friendly associative data structures, and std::mdspan for multidimensional array views. Each feature is accompanied by production-ready code examples, real-world scenarios, and debugging tips. By the end of this tutorial, you'll be equipped to leverage C++23 in your projects, write safer code, and avoid common pitfalls. Let's get started!
1. std::print and std::println: Modern Output
C++23 introduces std::print and std::println as a type-safe, performant alternative to std::cout. These functions are based on the {fmt} library and support Python-like format strings. They are thread-safe and do not synchronize with C stdio by default, avoiding the overhead of std::cout. Use std::println for automatic newline, std::print for no newline. The format string uses curly braces {} as placeholders, with support for positional arguments, formatting specifiers, and custom types via std::formatter.
Example: Basic usage
```cpp #include <print> #include <vector>
int main() { std::println("Hello, C++23!"); std::print("The answer is {}. ", 42); std::println("Pi is approximately {:.2f}", 3.14159); std::vector<int> vec = {1, 2, 3}; std::println("Vector: {}", vec); // requires formatter specialization return 0; } ```
Output: `` Hello, C++23! The answer is 42. Pi is approximately 3.14 Vector: [1, 2, 3] ``
For custom types, specialize std::formatter. This is production-ready and avoids the pitfalls of printf (type safety) and cout (performance).
2. std::expected: Error Handling Without Exceptions
std::expected<T, E> is a vocabulary type that represents either an expected value of type T or an unexpected error of type E. It is similar to std::optional but carries error information. This is ideal for functions that may fail but where exceptions are undesirable (e.g., embedded systems, real-time applications). Use .has_value() to check, .value() to access (throws if unexpected), or .error() to retrieve the error. The monadic operations (and_then, or_else, transform) enable functional composition.
Example: Parsing an integer from a string
```cpp #include <expected> #include <string> #include <charconv> #include <iostream>
std::expected<int, std::string> parse_int(const std::string& s) { int value; auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value); if (ec == std::errc()) { return value; } else { return std::unexpected("Invalid integer: " + s); } }
int main() { auto result = parse_int("42"); if (result) { std::println("Parsed: {}", *result); } else { std::println("Error: {}", result.error()); } return 0; } ```
Output: `` Parsed: 42 ``
Error handling without exceptions is cleaner and more predictable.
3. Deducing this: Simplified Recursive Lambdas and CRTP
C++23 allows lambdas to have an explicit 'this' parameter, enabling recursion without std::function overhead and simplifying CRTP (Curiously Recurring Template Pattern). The first parameter of a lambda can be a deduced 'this' (e.g., auto&& self). This makes it easy to call the lambda recursively or access the enclosing class.
Example: Recursive lambda to compute factorial
```cpp #include <print>
int main() { auto factorial = [](this auto&& self, int n) -> int { if (n <= 1) return 1; return n * self(n - 1); }; std::println("Factorial of 5: {}", factorial(5)); return 0; } ```
Output: `` Factorial of 5: 120 ``
This avoids the need for std::function or Y-combinators, making recursive lambdas efficient and easy.
4. Flat Containers: std::flat_set, std::flat_map
C++23 introduces flat containers: std::flat_set, std::flat_map, std::flat_multiset, std::flat_multimap. These are associative containers backed by a sorted vector (or any random-access container). They offer better cache locality and faster iteration than tree-based containers (std::set, std::map), at the cost of slower insertion and removal (O(n)). They are ideal for lookup-heavy workloads with infrequent modifications.
Example: Using std::flat_map
```cpp #include <flat_map> #include <print> #include <string>
int main() { std::flat_map<int, std::string> fm; fm.insert({1, "one"}); fm.insert({3, "three"}); fm.insert({2, "two"}); for (const auto& [key, value] : fm) { std::println("{}: {}", key, value); } return 0; } ```
Output: `` 1: one 2: two 3: three ``
They are sorted automatically. Use .insert() or emplace for modification.
5. std::mdspan: Multidimensional Array Views
std::mdspan (multidimensional span) is a non-owning view over a contiguous sequence of elements, interpreted as a multidimensional array. It supports both compile-time and runtime dimensions, and allows custom layout policies (e.g., row-major, column-major). It is the successor to std::span for multidimensional data, enabling efficient subarray views and slicing without copying.
Example: Creating a 2D view and accessing elements
```cpp #include <mdspan> #include <print> #include <vector>
int main() { std::vector<int> data = {1, 2, 3, 4, 5, 6}; // 2x3 layout (row-major by default) std::mdspan<int, std::extents<int, 2, 3>> ms(data.data()); for (int i = 0; i < ms.extent(0); ++i) { for (int j = 0; j < ms.extent(1); ++j) { std::print("{} ", ms[i, j]); } std::println(); } return 0; } ```
Output: `` 1 2 3 4 5 6 ``
std::mdspan is zero-overhead and supports submdspan for slicing.
6. Other Notable C++23 Features
- std::out_ptr and std::inout_ptr: Simplify interoperability with C APIs that use output pointers.
- std::move_only_function: A move-only version of std::function for move-only callables.
- std::unreachable(): Marks unreachable code paths, enabling better optimization.
- if consteval: Compile-time conditional for immediate functions.
- Multidimensional subscript operator: Allows operator[] with multiple arguments (used by std::mdspan).
- Stacktrace library: std::stacktrace for capturing call stacks (though not fully standardized in C++23, it's available as an experimental feature).
Example: Using std::out_ptr with a C function
```cpp #include <memory> #include <cstdlib>
// Simulated C function that allocates memory void c_malloc(void* ptr, size_t size) { ptr = std::malloc(size); }
int main() { std::unique_ptr<int, decltype(&std::free)> ptr(nullptr, &std::free); c_malloc(std::out_ptr(ptr), sizeof(int)); *ptr = 42; // ptr automatically freed return 0; } ```
These features round out the C++23 standard, making it more expressive and safer.
The Slow Logging Nightmare: How std::print Saved the Day
- Always measure I/O performance under realistic load.
- Prefer std::print over std::cout for high-performance logging.
- Understand the synchronization behavior of standard streams.
- Use modern C++ features to avoid legacy pitfalls.
- Profile before optimizing; the bottleneck may surprise you.
g++ -std=c++23 -o test test.cppclang++ -std=c++23 -stdlib=libc++ -o test test.cpp| File | Command / Code | Purpose |
|---|---|---|
| print_example.cpp | int main() { | 1. std |
| expected_example.cpp | std::expected | 2. std |
| deducing_this.cpp | int main() { | 3. Deducing this |
| flat_map_example.cpp | int main() { | 4. Flat Containers |
| mdspan_example.cpp | int main() { | 5. std |
| out_ptr_example.cpp | void c_malloc(void** ptr, size_t size) { | 6. Other Notable C++23 Features |
Key takeaways
Common mistakes to avoid
5 patternsUsing std::cout in performance-critical code instead of std::print
Forgetting to include <expected> when using std::expected
Using std::flat_map for frequently modified data
Not using std::submdspan for slicing mdspan
Assuming std::print flushes output automatically
Interview Questions on This Topic
What is std::expected and how does it differ from std::optional?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.
That's C++ Advanced. Mark it forged?
3 min read · try the examples if you haven't