Home C / C++ C++23 Features Complete Guide: What's New and How to Use Them
Advanced 3 min · July 13, 2026

C++23 Features Complete Guide: What's New and How to Use Them

Explore all C++23 features with production-ready code examples.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • 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+)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is C++23 Features Complete?

C++23 is the latest version of the C++ programming standard, bringing new features like std::print, std::expected, deducing this, flat containers, and std::mdspan to improve safety, performance, and expressiveness.

Think of C++23 as a new toolbox for a master carpenter.
Plain-English First

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

print_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
#include <print>
#include <vector>

int main() {
    std::println("Hello, C++23!");
    std::print("The answer is {}.\n", 42);
    std::println("Pi is approximately {:.2f}", 3.14159);
    std::vector<int> vec = {1, 2, 3};
    std::println("Vector: {}", vec);
    return 0;
}
Output
Hello, C++23!
The answer is 42.
Pi is approximately 3.14
Vector: [1, 2, 3]
💡Performance Tip
📊 Production Insight
In production, ensure that the output stream is flushed when needed; std::print does not flush automatically. Use std::println or std::flush for interactive output.
🎯 Key Takeaway
std::print and std::println are the new standard for type-safe, fast output in C++23.

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.

expected_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <expected>
#include <string>
#include <charconv>
#include <print>

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
🔥When to Use std::expected
📊 Production Insight
In production, avoid storing large error types in std::expected to minimize overhead. Use error codes or enums for efficiency.
🎯 Key Takeaway
std::expected provides a standard way to handle errors without exceptions, enabling functional composition.

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.

deducing_this.cppCPP
1
2
3
4
5
6
7
8
9
10
#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
⚠ Compiler Support
📊 Production Insight
Use deducing this for recursive algorithms that are called frequently; it avoids the overhead of std::function and virtual dispatch.
🎯 Key Takeaway
Deducing this enables simple recursive lambdas and CRTP without boilerplate.

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.

flat_map_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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
💡When to Use Flat Containers
📊 Production Insight
In production, consider using std::flat_map over std::map for small to medium datasets (up to a few thousand elements) where insertion is rare. For large datasets, benchmark both.
🎯 Key Takeaway
Flat containers provide cache-friendly associative storage with fast iteration, ideal for read-heavy workloads.

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.

mdspan_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <mdspan>
#include <print>
#include <vector>

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6};
    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
🔥Layout Policies
📊 Production Insight
Use std::mdspan in scientific computing and image processing to avoid manual index calculations and enable efficient subarray operations.
🎯 Key Takeaway
std::mdspan provides a flexible, zero-overhead multidimensional array view for modern C++.

6. Other Notable C++23 Features

C++23 includes several other improvements
  • 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.

out_ptr_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <memory>
#include <cstdlib>

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;
    return 0;
}
⚠ Compiler Support Varies
📊 Production Insight
Use std::out_ptr when integrating with legacy C libraries to avoid manual resource management.
🎯 Key Takeaway
C++23 brings many quality-of-life improvements that make code safer and more efficient.
● Production incidentPOST-MORTEMseverity: high

The Slow Logging Nightmare: How std::print Saved the Day

Symptom
Users experienced timeouts and slow responses; logs showed high latency in output operations.
Assumption
The developer assumed std::cout was fast enough and blamed network issues.
Root cause
std::cout's synchronization with C stdio (ios_base::sync_with_stdio) caused contention and performance degradation under high concurrency.
Fix
Replaced std::cout with std::print (C++23) which is lock-free and type-safe, eliminating synchronization overhead.
Key lesson
  • 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.
Production debug guideSymptom to Action5 entries
Symptom · 01
std::print output is missing or garbled
Fix
Check if the output stream is flushed; std::print does not flush automatically. Use std::println or add std::flush.
Symptom · 02
std::expected contains unexpected error
Fix
Verify that the error type matches; use .error() to inspect. Ensure no implicit conversions.
Symptom · 03
Deducing this lambda fails to compile
Fix
Ensure the lambda is generic (auto parameter) and the explicit this parameter is the first parameter.
Symptom · 04
Flat container insertion is slow
Fix
Flat containers are sorted vectors; insertion is O(n). Use bulk insertion or consider std::set for frequent inserts.
Symptom · 05
std::mdspan access out of bounds
Fix
Use .extent() to check bounds; enable assertions with -D_GLIBCXX_ASSERTIONS for GCC.
★ Quick Debug Cheat SheetCommon issues and immediate fixes for C++23 features
std::print not found
Immediate action
Check compiler support (GCC 14+, Clang 17+, MSVC 2022 17.6+). Include <print>.
Commands
g++ -std=c++23 -o test test.cpp
clang++ -std=c++23 -stdlib=libc++ -o test test.cpp
Fix now
Use fmt::print from {fmt} library as fallback.
std::expected compile error+
Immediate action
Include <expected>. Ensure both value and error types are complete.
Commands
g++ -std=c++23 -o test test.cpp
clang++ -std=c++23 -o test test.cpp
Fix now
Use std::optional or boost::outcome as workaround.
Deducing this not working+
Immediate action
Use explicit this parameter with auto&& self.
Commands
g++ -std=c++23 -o test test.cpp
clang++ -std=c++23 -o test test.cpp
Fix now
Use traditional recursion with std::function.
Flat container compilation error+
Immediate action
Include <flat_set> or <flat_map>. Use C++23 standard.
Commands
g++ -std=c++23 -o test test.cpp
clang++ -std=c++23 -o test test.cpp
Fix now
Use std::set or std::map as fallback.
std::mdspan not recognized+
Immediate action
Include <mdspan>. Use C++23 standard.
Commands
g++ -std=c++23 -o test test.cpp
clang++ -std=c++23 -o test test.cpp
Fix now
Use raw pointers with manual indexing.
FeatureC++23Previous StandardBenefit
Outputstd::printstd::cout / printfType-safe, faster, thread-safe
Error handlingstd::expectedstd::optional / exceptionsCarries error info, no exceptions
Recursive lambdasDeducing thisstd::function / Y-combinatorZero overhead, simple syntax
Associative containersstd::flat_mapstd::mapBetter cache locality, faster iteration
Multidimensional arraysstd::mdspanRaw pointers / std::vectorZero-overhead view, slicing
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
print_example.cppint main() {1. std
expected_example.cppstd::expected parse_int(const std::string& s) {2. std
deducing_this.cppint main() {3. Deducing this
flat_map_example.cppint main() {4. Flat Containers
mdspan_example.cppint main() {5. std
out_ptr_example.cppvoid c_malloc(void** ptr, size_t size) {6. Other Notable C++23 Features

Key takeaways

1
C++23 introduces std::print for fast, type-safe output.
2
std::expected provides a standard error-handling mechanism without exceptions.
3
Deducing this simplifies recursive lambdas and CRTP.
4
Flat containers offer cache-friendly associative storage for read-heavy workloads.
5
std::mdspan enables efficient multidimensional array views with zero overhead.
6
Always check compiler support for C++23 features before using them in production.

Common mistakes to avoid

5 patterns
×

Using 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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is std::expected and how does it differ from std::optional?
Q02SENIOR
Explain deducing this in C++23 and give an example.
Q03SENIOR
When would you choose std::flat_map over std::map?
Q04SENIOR
What is std::mdspan and how does it improve multidimensional array handl...
Q05JUNIOR
How does std::print improve upon printf and std::cout?
Q01 of 05SENIOR

What is std::expected and how does it differ from std::optional?

ANSWER
std::expected<T, E> can hold either a value of type T or an error of type E, while std::optional<T> can hold a value or nothing. std::expected carries error information, making it suitable for functions that may fail with a reason.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What compilers support C++23?
02
Is std::print faster than std::cout?
03
Can I use std::expected with exceptions disabled?
04
How do I slice a std::mdspan?
05
What is the difference between std::flat_map and std::map?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's C++ Advanced. Mark it forged?

3 min read · try the examples if you haven't

Previous
Expression Templates in C++
19 / 41 · C++ Advanced
Next
C++26 Preview: What's Coming Next