Table of Contents

In this blog post, you’ll see all C++23 library features! Each with a short description and additional code example.

Prepare for a ride!

For language features please see the previous article: C++23 Language Features and Reference Cards - C++ Stories

This article is “in progress”, most topics are completed, but few sections are in the “to-do” state.

  • 10th Dec: Explicit Lifetime Management
  • 11th Dec: aligned_storage deprecating

Want your own copy to print?  

If you like, I prepared PDF I packed both language and the Standard Library features. Each one has a short description and an example if possible.

All of the existing subscribers of my mailing list have already got the new document, so If you want to download it just subscribe here:

Please notice that along with the new ref card you’ll also get C++20 and C++17 language reference card that I initially published three years ago. With this “package” you’ll quickly learn about all of the latest parts that Modern C++ acquired over the last few years.

New Headers  

  • expected
  • flat_map, flat_set
  • generator
  • mdspan
  • print
  • spanstream
  • stacktrace
  • stdfloat

Stacktrace Library  

P0881

Based on Boost.Stacktrace allows for more context when debugging code. The library defines components to store the stacktrace of the current thread of execution and query information about the stored stacktrace at runtime.

#include <iostream>
#include <stacktrace>

void foo() {
    std::cout << std::stacktrace::current();
}

int main() {
    [] {
  foo();
    }();
}

Experiment @Compiler Explorer, note that GCC 14 requires to be linked with -lstdc++exp.

Possible output on GCC:

   0# foo() at /app/example.cpp:5
   1# operator() at /app/example.cpp:10
   2# main at /app/example.cpp:11
   3#      at :0
   4# __libc_start_main at :0
   5# _start at :0

is_scoped_enum & to_underlying  

P1048R1 (is_scoped_enum), P1682R3 (to_underlying)

#include <print>
#include <utility>  // for std::to_underlying

enum class Color: uint8_t { Red = 1, Green = 2,  Blue = 3 };
enum Col { Red, Green, Blue };

int main() {
    Color c = Color::Blue;

    // Pre-C++23: verbose casting
    auto old_way = static_cast<std::underlying_type_t<Color>>(c);

    // C++23: clean and simple
    auto new_way = std::to_underlying(c);

    std::println("Color value: {}", new_way);
    std::println("is_scoped_enum Color {}", std::is_scoped_enum_v<Color>);
    std::println("is_scoped_enum Col   {}", std::is_scoped_enum_v<Col>);
}

Run @Compiler Explorer

The trait (underlying_type) has been available since C++11.

Bonus: First notes and ideas about to_underlying appeared in Scott Meyers’ book: Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14 @Amazon.

std::string/std::string_view Improvements  

  • contains(char/string_view/const char*) - member function
  • Prohibiting std::basic_string and std::basic_string_view construction from nullptr
  • Range constructor for std::basic_string_view - P1989
  • string::resize_and_overwrite() - P1072R10 - Allows us to initialize/resize strings without clearing the buffer but filling bytes with some user operation.

See the example below:

#include <string>
#include <string_view>
#include <vector>
#include <print>

int main() {
    // 1. contains() example
    std::string str = "Hello C++23 World";

    std::println("{}", str.contains("C++23"));  
    std::println("{}", str.contains('X')); 

    // 2. resize_and_overwrite() example
    std::string numbers {"xyz"};
    numbers.resize_and_overwrite(5, [](char* buf, std::size_t n) {
        for (std::size_t i = 1; i < n; ++i) {
            buf[i] = '0' + i; 
        }
        return n; 
    });
    std::println("Numbers: {}", numbers);

    // 3. string_view range constructor
    std::vector<char> chars = {'H', 'e', 'l', 'l', 'o'};
    std::string_view sv(chars.begin(), chars.end());
    std::println("View: {}", sv);
}

Play @Compiler Explorer

std::out_ptr(), std::inout_ptr(),  

P1132

Functions that wrap a smart pointer into a special type allowing to pass to low-level functions that require pointer-to-pointer parameters.

void lowLevel(int** pp) { if (pp) *pp = new int{42}; }
auto ptr = std::make_unique<int>(10);
lowLevel(std::inout_ptr(ptr));

Handy for interaction with C-style API like WindowsAPI, DirectX, Media libraries. inout_ptr additionally calls .release() on the pointer.

#include <memory>
#include <utility>  // for out_ptr/inout_ptr
#include <print>

// Simulate C-style API functions
void AllocateResource(int** pp) {
    *pp = new int{42};
}

void ModifyResource(int** pp) {
    if (*pp) {
        **pp = 100; 
    }
}

void CleanupResource(int* p) {
    delete p;
}

int main() {
    // Example 1: out_ptr for new allocation
    std::unique_ptr<int, decltype(&CleanupResource)> resource1(nullptr, CleanupResource);
    AllocateResource(std::out_ptr(resource1));
    std::println("Resource1 value: {}", *resource1); 

    // Example 2: inout_ptr for modifying existing resource
    std::unique_ptr<int> resource2 = std::make_unique<int>(42);
    std::println("Resource2 before: {}", *resource2);
    ModifyResource(std::inout_ptr(resource2));
    std::println("Resource2 after: {}", *resource2); 
}

Play @Compiler Explorer

See another example: https://godbolt.org/z/KerKM7fM8

ranges::to<>  

P1206

A way to build containers from a view:

auto v = iota('a') | take(10);
auto vec = v | std::ranges::to<std::vector>();
auto str = v | std::ranges::to<std::string>();

When a container has a reserve() function, ranges:to will also try to use it to make creation more optimal.

#include <algorithm>
#include <concepts>
#include <iostream>
#include <ranges>
#include <vector>

int main()
{
    using namespace std::views;
    auto v = iota('a') | take(10);
    // new in C++23
    auto vec = v | std::ranges::to<std::vector>();
    for (auto x : vec) 
        std::cout << x;

    auto vec2 = vec | std::views::reverse | std::ranges::to<std::vector>();
    for (auto x : vec2) 
        std::cout << x;
}

Run @Compiler Explorer

Ranges Algorithms  

  • ranges::starts_with() and ranges::ends_with()
  • ranges::iota(), ranges::shift_left/right()
  • ranges::find_last(), find_last_if(), find_last_if_not()
  • ranges::contains() and ranges::contains_subrange()
  • Ranges fold algorithms: ranges::fold_*()

Views Additions  

  • cartesian_product
  • repeat
  • enumerate
  • adjacent, adjacent_transform
  • stride
  • slide
  • chunk, chunk_by
  • join_with
  • zip, zip_transform
  • as_rvalue, as_const

For zip you can see my other article: Combining Collections with Zip in C++23 for Efficient Data Processing - C++ Stories

Heterogeneous Erasure for Associative Containers  

P2077

Continuation of the work for heterogeneous operations. This time you can use transparent comparators for erase() and extract() member functions. To be backward compatible, the comparators cannot be convertible to iterator or const_iterator of a given container.

Read more about heterogeneous access in my other article: C++20: Heterogeneous Lookup in (Un)ordered Containers - C++ Stories

Monadic Operations for std::optional  

P0798

New member functions for optional: and_then, transform, and or_else.

auto ret = userName.transform(toUpper)
.and_then([](string x) { return make_optional(x + "OK"); })
.or_else([] { return make_optional(string{"no user"}); });

For example:

#include <optional>
#include <print>
#include <algorithm>
#include <ranges>

void test(const std::optional<std::string>& userName) {
    auto up = [](std::string x) { 
        std::ranges::transform(x, x.begin(), ::toupper);
        return x;
    };

    auto ret = userName.transform(up)
       .and_then([](std::string x) { 
           std::println("x: {}", x);
           return std::optional<int>(x.size()); 
        })
        .or_else([]{ 
            std::println("empty...");
            return std::optional<int>{0}; 
        });
    std::println("ret is {}", *ret);
}

int main() {
    test(std::nullopt);
    test("john");
}

See @Compiler Explorer

output:

empty...
ret is 0
x: JOHN
ret is 4

See my full article about this extension here: How to Use Monadic Operations for `std::optional` in C++23 - C++ Stories

<expected> and Its Monadic Operations  

P0323

A vocabulary type that allows storing either of two values: T or unexpected (in a form of some error type). It’s something between std::optional and std::variant.

enum class FuelErr { DistLarge, Neg };
std::expected<double, FuelErr> calcFuel(int dst) {
  if (dst < 0) return std::unexpected(FuelErr::Neg);
  return distance * 1.333;
}

C++23 also adds monadic operations for this type, so it’s consistent with operations for std::optional.

See my mini-series about this type:

constexpr std::unique_ptr  

P2273

The new() operator can be used in constexpr context since C++20, and now you can wrap it also in a unique_ptr.

#include <numeric>
#include <memory>

constexpr int naiveSum(unsigned int n) {  
    auto p = std::make_unique<int[]>(n);  
    std::iota(p.get(), p.get()+n, 1);
    auto tmp = std::accumulate(p.get(), p.get()+n, 0);
    return tmp;
}

constexpr int smartSum(unsigned int n) {
    return (n*(n+1))/2;
}

int main() {
    static_assert(naiveSum(10) == smartSum(10));
    static_assert(naiveSum(11) == smartSum(11));
}

See @Compiler Explorer

std::mdspan Multidimensional Span, P0009  

A generalization over std::span for multiple dimensions. Supports dynamic as well as static extents (compile-time constants). It also supports various mappings like column-major order, row-major, or even stride access.

#include <vector>
#include <https://raw.githubusercontent.com/kokkos/mdspan/single-header/mdspan.hpp>
#include <algorithm>
#include <iostream>

bool isSymmetric(std::mdspan<int, std::dextents<size_t, 2>> matrix) {
    const auto rows = matrix.extent(0);
    const auto cols = matrix.extent(1);

    if (rows != cols) return false;

    for (size_t i = 0uz; i < rows; ++i) {
        for (size_t j = i + 1; j < cols; ++j) {
            if (matrix[i, j] != matrix[j, i]) return false;
        }
    }
    return true;
}

int main() {
    std::vector<int> matrix_data = {1, 2, 3, 2, 4, 5, 3, 5, 6};
    auto matrix = std::mdspan(matrix_data.data(), 3, 3);

    std::cout << isSymmetric(matrix) << std::endl;
}

See at Compiler Explorer

And see my two bonus articles, available for Patreons, about this type:

<flat_map> and <flat_set>  

P0429 and P1222

Drop-in replacement for maps and sets with better performance characteristics. It gives faster lookup, faster iteration, random access iteration, less memory, and better cache efficiency. But iterators might be invalidated, and insertion is slower than the tree approach. The container is actually a container adaptor with proxy iterators.

Here’s a reference implementation: https://github.com/tzlaine/flat_map/blob/master/implementation/flat_map

Formatted Output Library <print>  

P2093

New Hello World Style for C++23!

#include <print>  
#include <string>  

int main() {  
    std::string name = "C++23";  
    int year = 2024;  
    double version = 23.0;  

    // Basic printing  
    std::println("Hello from {}!", name);  

    // Named arguments  
    std::print("Language: {0}, Version: {0}\n", name);  

    // Multiple arguments with formatting  
    std::println("Release year: {:d}, Version: {:.1f}", year, version);  
    // Alignment and width  
    std::println("{:>10}: {:>5}", "Status", "OK");  // right-aligned  
    std::println("{:<10}: {:<5}", "Error", "None"); // left-aligned  

    // Print without newline and then with newline  
    std::print("Loading");  
    std::println("... done!");  
}  

See @Compiler Explorer

New functions in the <print> header: std::print, std::println (adds a new line) that uses std::format to output text to stdout. Plus lower-level routines like vprint_unicode with more parameters for output.

constexpr to_chars(), from_chars()  

P2291

Integral versions available in constexpr context:

#include <charconv>
#include <optional>
#include <string_view>

constexpr std::optional<int> to_int(std::string_view sv)
{
    int value {};
    const auto ret = std::from_chars(sv.begin(), sv.end(), value);
    if (ret.ec == std::errc{})
        return value;

    return {};
};

int main() {
    static_assert(to_int("hello") == std::nullopt);
    static_assert(to_int("10") == 10);
}

Run @Compiler Explorer

See my article about this new feature and more example: C++ String Conversion: Exploring std::from_chars in C++17 to C++26 - C++ Stories

Standard Library Modules  

P2465

C++23 introduces two standard library modules that significantly improve compilation efficiency and code organization:

import std;  

  • Imports all C++ standard library components in namespace std
  • Includes C++ headers and C wrapper headers
  • Provides ::operator new and related operators
  • Keeps the global namespace clean
  • Ideal for new projects and modern C++ code

import std.compat;  

  • Provides everything from import std;
  • Additionally, imports C functions into global namespace
  • Helps transition legacy code that uses unqualified C functions
  • Useful when working with platforms where C functions are traditionally global
  • Recommended for compatibility with existing codebases

Here are two examples:

import std;  

int main() {  
    std::string str = "Hello";  
    std::size_t len = std::strlen(str.c_str());  // must use std::  
    std::println("Length: {}", len);  
}  

and the compat version:

import std.compat;  

int main() {  
    string str = "Hello";     // works: string is in std  
    size_t len = strlen(str.c_str());  // works: strlen is global  
    printf("Length: %zu\n", len);      // works: printf is global  
}  

Unfortunately as of December 2024, no major compiler supports the above examples.

std::generator Coroutine Generator  

P2502

C++20 introduced coroutines, but the standard library support was minimal, leaving us to implement our own coroutine types or rely on third-party libraries. Synchronous generators are a crucial use case for coroutines, enabling efficient and lazy evaluation of sequences. Writing an efficient recursive generator is non-trivial, and the standard should provide one to simplify this task for us.

std::generator fills this gap by providing a ready-to-use, standardized coroutine type for generating sequences. This makes it significantly easier for us to adopt coroutines in our projects without the overhead of implementing custom coroutine logic.

std::generator is a coroutine-based feature that uses the co_yield keyword to define a sequence of values. Each co_yield call produces a value and suspends execution, allowing us to retrieve the value. When resumed, the coroutine continues from where it left off.

Here’s a simple generator:

std::generator<int> gen(int n) {
    for (int a = 0; a < n; ++a)
        co_yield a;
}

int main() {
    auto g = gen(5);
    auto it = g.begin();
    while (it != g.end())
    {
        std::cout << *it << " ";
        ++it;
    }
}

See @Compiler Explorer

For more examples have a look at my two bonus articles for Patreons:

Explicit Lifetime Management  

P2590, P2679R2

From the proposal:

Since C++20, certain functions in the C++ standard library such as malloc, bit_cast, and memcpy are defined to implicitly create objects.

But when you use non standard techniques to obtain a memory for the object you might end up with UB:

C++23 introduces std::start_lifetime_as/start_lifetime_as_array to explicitly start the lifetime of objects in raw memory, enabling well-defined behavior without invoking constructors. This is useful for low-level tasks like custom allocators or deserialization.

For example:

#include <memory>  

struct X { int a, b; };  

void example() {  
    void* rawMemory = My_Malloc(sizeof(X));         // non standard way...
    X* obj = std::start_lifetime_as<X>(rawMemory); // Start lifetime  
    obj->a = 42; obj->b = 84;                      // Use the object  
    std::free(rawMemory);                          // Free memory  
}  

(as of December no major compiler implements this functionality)

<spanstream> String-Stream with Span Buffers  

P0448

New classes: basic_spanbuf, basic_ispanstream, basic_ospanstream, basic_spanstream analogous to existing stream classes but using std::span as the buffer. They allow explicit buffer management and improved performance.

char buffer[64] { 0 };
std::span<char> spanBuffer(buffer);
std::basic_ospanstream<char> outputStream(spanBuffer);
outputStream << "Hello, " << "C++23!";

std::format Improvements  

  • Compile-time string parsing
  • Formatting Ranges
  • Improve default container formatting

std::visit() for classes derived from std::variant  

/* todo */

std::unreachable()  

/* todo */

Deprecating std::aligned_storage and aligned_union  

P1413

The accepted proposal deprecates C++11’s std::aligned_storage and std::aligned_union in the C++ standard due to their poor API design, undefined behavior risks, and limited utility. These helper types require error-prone usage patterns like reinterpret_cast, lack proper size guarantees, and have inconsistent or confusing APIs, making them unsuitable for modern C++ practices.

The authors analysed libraries like Boost, Folly, and Abseil and most use cases for std::aligned_ involve repetitive patterns to deduce size and alignment manually. This can be replaced with simpler alternatives like alignas and std::byte arrays.

// deprecated:
template <typename T>  
class MyContainer {  
private:  
    std::aligned_storage_t<sizeof(T), alignof(T)> buffer;  
};  

// better:
template <typename T>  
class MyContainer {  
private:  
    alignas(T) std::byte buffer[sizeof(T)];  
};  

These utilities are still available in the standard but are marked as discouraged for use. They may be removed in a future version of the standard (e.g., C++26 or later).

For example clang reports warning: 'aligned_storage_t' is deprecated when compiling in C++23 mode.

Pipe support for user-defined range adaptors, P2387  

/* todo */

Examples  

Books on C++23  

Although the standard is fresh, there are several good books focusing on C++23 worth reading… and probably more to come :)

Title Author(s) Description
Modern C++ Programming Cookbook (3rd Edition) Marius Bancila Master Modern C++ with comprehensive solutions for C++23 and all previous standards
The C++ Programming Language (4th Edition) Bjarne Stroustrup The definitive guide from the creator of C++
Beginning C++23: From Beginner to Pro (7th Edition) Ivor Horton, Peter Van Weert A comprehensive guide for learning modern C++ from the ground up
Modern C++ for Absolute Beginners (2nd Edition) Slobodan Dmitrović A friendly introduction to C++ programming language and C++11 to C++23 standards
C++23 Best Practices Jason Turner Simple rules with specific action items for better C++
Learn C++ by Example Frances Buontempo A practical approach to learning C++ versions 11 to 23

Note: Links are affiliate links and may provide the site with a small commission at no extra cost to you.

Summary  

I hope we covered most if not all C++23 library features!

You can check their implementation status at C++ Reference: https://en.cppreference.com/w/cpp/compiler_support#cpp23

For language features please see the previous article: C++23 Language Features and Reference Cards - C++ Stories

Back to you

  • Have you played with C++23 library features?
  • What are the most important features for you in this release?