Table of Contents

C++23 Language Features  

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

Prepare for a ride!

For library features please see the next article: C++23 Library Features and Reference Cards - C++ Stories

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.

Timeline  

Here’s a short overview about the timeline for the new standard.

  • February 2020 (Prague): Final C++20 meeting where initial plans for C++23 were adopted
    • Key planned features: library support for coroutines, modular standard library, executors, and networking
  • June 2020: First planned WG21 meeting for C++23 in Varna was cancelled due to COVID-19 pandemic
  • November 2020: New York meeting cancelled, moved to virtual format
    • Key additions: size_t literals, stacktrace library, contains() member functions, C atomic interoperability
  • February 2021: Virtual meeting
    • Notable features: Lambda expression simplifications, variant improvements, conditionally borrowed ranges
  • June 2021: Virtual summer plenary meeting
    • Major additions: if consteval, spanstream, out_ptr/inout_ptr, constexpr improvements
  • October 2021: Virtual autumn plenary meeting
    • Significant features: Explicit this parameter, multidimensional subscript operator, zip ranges
  • February 2022: Virtual meeting
    • Key additions: std::expected, ranges::to, windowing range adaptors
  • July 2022: Virtual meeting
    • Major features: static operator(), UTF-8 source files, std::mdspan, flat containers
  • November 2022: First hybrid meeting
    • Notable additions: Static operator[], lifetime extensions for range-based for loops
  • February 2023: Final hybrid meeting in Issaquah
    • Technical content finalized
    • Last features added: views::enumerate, formatting improvements, std::barrier guarantees
  • October 2024: Published with a long delay at ISO: as ISO/IEC 14882:2024 - Programming languages — C++

Now let’s jump into the features:

if consteval { }  

P1938

The new syntax is equivalent to the following code from C++20:

if (std::is_constant_evaluated()) { }

It’s a language feature now, so no need for a separate header. It can call consteval functions and it is easier to understand.

#include <iostream>
 
constexpr int run(int i) {
    if consteval {
        return i*2;
    }
    else {
        return i;
    }
}
 
int main() {
    static_assert(run(10) == 20); // compile-time
    int a = 10;
    std::cout << run(a); // run-time
}

Run @Compiler Explorer

Deducing this  

P0847

A way to explicitly pass the this parameter into a member function, allowing for more control and reduces code duplication. You can pass this by value, call lambdas recursively, simplify the CRTP pattern, and more.

struct Pattern {
  template <typename Self> void foo(this Self&& self) { self.fooImpl(); }
};
struct MyClass : Pattern { void fooImpl() { ... } };

See my examples at this Patreon post: C++23: Deducing This, a few examples (Available for all patrons, even at the free tier).

auto(x) and auto{x}  

P0849

Replaces decay_copy (an internal library helper) with a language feature. Allows creating a decay rvalue copy of the input object.

void pop_front_alike(Container auto& x) {
    using T = std::decay_t<decltype(x.front())>;
    std::erase(x.begin(), x.end(), T(x.front()));
}
// becomes:
std::erase(x.begin(), x.end(), auto(x.front()));

const std::string& str = "hello";
auto(str);  // Creates a new std::string (decayed copy)

int arr[] = {1, 2, 3};
auto(arr);  // Creates int* (arrays decay to pointers)

Extend init-statement to allow alias-declaration  

P2360

In C++20, using wasn’t allowed in the for loop, now it’s possible:

for (using T = int; T e : container) { ... }

Multidimensional Subscript Operator  

P2128

Change the rules to allow multiple parameters for operator[]:

#include <vector>
 
template <typename T>
class Array2D {
    std::vector<T> m;
    size_t w, h;

public:
    Array2D(size_t width, size_t height)
        : m(width * height), w(width), h(height) {}

    T& operator[](size_t i, size_t j) { return m[i + j * w]; }
};

int main() {
    Array2D<float> arr(4, 4);
    arr[1, 2] = 0.0f;
}

Run @Compiler Explorer

This is crucial for types like std::mdspan.

static operator() and static operator []  

P1169R4 and P2589R1

Allows for more optimization in the compiler. The call operator is especially handy for captureless lambdas. The compiler can optimize away passing the “this” pointer to the call. You can specify a lambda to be static.

struct Fn {
    constexpr static int operator()(int x) {
        return x*10;
    }
};

int main() {
    static_assert(Fn::operator()(10) == 100);
    Fn x;
    static_assert(x(10) == 100);
}

Run @Compiler Explorer

Features for Lambdas  

  • Attributes on lambdas
  • () is more optional - P1102
  • The call operator can be static
  • Deducing this adds new capabilities like better recursion
  • Change scope of lambda trailing-return-type

Examples:

int main() {
    auto identity = [](int x) static { return x; };
    return identity(100);
}

Run @Compiler Explorer

Recursive lambda:

int main() {
    auto fib = [](this auto self, int n) {
       if (n < 2) return n;
       return self(n-1) + self(n-2);
    };
    static_assert(fib(7) == 13);
}

Run @Compiler Explorer

Optional ():

#include <iostream>
int main() {
    auto fn = [x = 0] mutable {
        return x++;
    };
    std::cout << fn() << fn();
}

Run @Compiler Explorer

[[assume]] New Attribute  

P1774

[[assume]] specifies that an expression will always evaluate to true at a given point. It standardizes the existing vendor-specific semantics like __builtin_assume (Clang) and __assume (MSVC, ICC). Offers potential optimization opportunities for compilers. If the assumption turns out to be false during runtime, the behavior is undefined, making it crucial to use this attribute only when absolutely certain about the condition.

The attribute can help compilers eliminate unnecessary bounds checking, enable better loop optimizations, and remove redundant error handling paths.

For example:

// Basic usage - compiler can optimize sqrt calculation
void process_positive(double x) {
    [[assume(x >= 0)]];
    return std::sqrt(x); // No need for negative number checks
}

// Loop optimization example
void process_array(int* arr, size_t size) {
    [[assume(size % 4 == 0)]];  // Assume size is multiple of 4
    [[assume(size > 0)]];       // Assume non-empty array
    for (size_t i = 0; i < size; i += 4) {
        // Compiler can optimize for 4-element chunks
        // No need for remainder handling
        arr[i] = arr[i] * 2;
        arr[i + 1] = arr[i + 1] * 2;
        arr[i + 2] = arr[i + 2] * 2;
        arr[i + 3] = arr[i + 3] * 2;
    }
}

Constexpr Updates  

P2448, P2647, and P2242

  • Relax rules for constructors and return types for constexpr functions, making them almost identical to regular functions.
  • Permitting static constexpr variables in constexpr functions.

Extend Lifetime of Temporaries in Range-Based For  

P2718

This is a very popular and long-standing proposal that generated significant discussion in the C++ community. While initially aimed at addressing all temporary lifetime issues, it was ultimately restricted to focus on range-based for loops, where the problem was most acute and the solution most clear-cut.

The change extends the lifetime of temporary objects in the for-range-initializer until the end of the loop. This fixes a common source of undefined behavior that many developers encountered, especially when working with chain calls or complex expressions.

Before C++23:

// Undefined Behavior in C++20 and earlier:
std::vector<std::vector<int>> getVector();
for (auto e : getVector()[0]) {  // temporary vector destroyed here!
    std::cout << e << '\n';      // accessing destroyed object
}

// Workaround required in C++20:
auto temp = getVector();
for (auto e : temp[0]) {
    std::cout << e << '\n';
}

C++23 makes the first version safe and equivalent to the workaround:

// Now valid in C++23:
for (auto e : getVector()[0]) {  // temporary vector lives through the loop
    std::cout << e << '\n';
}

// More complex example that's now safe:
struct Matrix {
    auto getRow(int i) const { return /* ... */; }
};
std::vector<Matrix> matrices;

for (auto val : matrices.back().getRow(42)) {
    // Both the temporary from back() and getRow() are preserved
    process(val);
}

This change significantly improves the safety and usability of range-based for loops, eliminating a common class of bugs while maintaining the expressive power of the syntax. However, it’s important to note that this fix is specific to range-based for loops and doesn’t address temporary lifetime issues in other contexts.

New Preprocessor Directives  

P2334 and P2437

C++23 introduces two sets of new preprocessor directives that improve code readability and maintain compatibility with C23. The #elifdef and #elifndef directives simplify conditional compilation chains, while #warning provides a standardized way to emit compiler warnings.

These additions reduce verbosity and make the code more maintainable compared to traditional preprocessor constructs.

// Old style:
#ifdef _WIN32
    #define PLATFORM "Windows"
#else
    #ifdef __linux__
        #define PLATFORM "Linux"
    #else
        #ifdef __APPLE__
            #define PLATFORM "macOS"
        #endif
    #endif
#endif

// New style in C++23:
#ifdef _WIN32
    #define PLATFORM "Windows"
#elifdef __linux__
    #define PLATFORM "Linux"
#elifdef __APPLE__
    #define PLATFORM "macOS"
#else
    #warning "Unknown platform detected!"
#endif

The new directives are particularly useful in cross-platform code and library compatibility checks, making conditional compilation more straightforward and easier to maintain.

Literal Suffix for (Signed) size_t – uz, UZ  

P0330

A simple yet effective fix for various numerical conversions between integer types, unsigned, and size_t which is commonly returned from std:: containers. The feature introduces three new literal suffixes:

  • uz or UZ for size_t
  • z or Z for the signed counterpart (ptrdiff_t or equivalent)

This addition eliminates common warnings and potential bugs related to signed/unsigned mismatches, particularly in loop counters and container operations.

// Before C++23 - potential warnings or issues:
for (int i = 0; i < vec.size(); ++i)  // warning: comparison between signed/unsigned
for (size_t i = 0; i < vec.size(); ++i)  // verbose

// C++23 - clean and portable:
for (auto i = 0uz; i < vec.size(); ++i)  // perfect match with container's size_t
    std::cout << i << ": " << vec[i] << '\n';

The feature is particularly valuable for writing portable code that needs to work correctly across different platforms and architectures, automatically adapting to the platform’s size_t width without explicit type specifications.

CTAD from inherited constructors  

P2582R1

C++23 extends Class Template Argument Deduction to work with inherited constructors, filling an important gap in CTAD functionality. This allows template arguments to be deduced when using inherited constructors through using declarations.

// Before C++23 - CTAD didn't work with inherited constructors
template<typename T>
struct Base {
    Base(T value) {}
};

template<typename T>
struct Derived : Base<T> {
    using Base<T>::Base;  // Inherit constructor
};

Derived d(42);  // Error in C++20: couldn't deduce T
                // OK in C++23: deduces Derived<int>

Run @Compiler Explorer

This feature makes template class hierarchies more intuitive to use and eliminates the need for explicit template arguments when using inherited constructors, improving code readability and maintainability.

Simpler implicit move  

P2266

C++23 simplifies and fixes the implicit move rules introduced in C++20, making them more consistent and easier to implement. The new rules state that a move-eligible id-expression is always treated as an xvalue, eliminating the previous two-step overload resolution process.

// Before C++23 - inconsistent behavior
Widget&& foo(Widget&& w) {
    return w;  // Error in C++20
};

See @Compiler Explorer

DRs  

Here’s the formatted list of C++23 DRs:

These Defect Reports (DRs) are actually retroactive fixes that apply to C++20 as well as C++23:

  1. Lambda Trailing-Return-Type Scope Change (P2036R3, P2579R0)
    • Changes the scope rules for lambda expression trailing return types
  2. Meaningful Exports (P2615R1)
    • Improves the semantics of export declarations in modules
  3. Consteval Propagation (P2564R0)
    • Fixes issues with consteval propagation in the language
  4. Unicode Identifier Syntax (P1949R7)
    • Updates C++ identifier syntax to align with Unicode Standard Annex 31
  5. Duplicate Attributes (P2156R1)
    • Allows multiple instances of the same attribute in declarations
  6. Concepts Feature Test Macro (P2493R0)
    • Adjusts the value of __cpp_concepts feature test macro
  7. Wchar_t Requirements (P2460R2)
    • Relaxes requirements on wchar_t to match existing implementation practices
  8. Unknown Pointers in Constant Expressions (P2280R4)
    • Allows using unknown pointers and references in constant expressions
  9. Equality Operator Enhancement (P2468R2)
    • Improves equality operator behavior
  10. char8_t Compatibility (P2513R4)
    • Enhances char8_t compatibility and portability
  11. Diagnostic Directives Clarification (CWG2518)
    • Clarifies reporting of diagnostic directives and static_assert behavior in templates

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 language features!

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

And next time we’ll handle Standard Library changes - see it here: C++23 Library Features and Reference Cards - C++ Stories

Back to you

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