Last Update:
C++23 Language Features and Reference Cards
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!
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 { }
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
}
Deducing this
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}
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
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
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;
}
This is crucial for types like std::mdspan
.
static operator()
and static operator []
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);
}
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);
}
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);
}
Optional ()
:
#include <iostream>
int main() {
auto fn = [x = 0] mutable {
return x++;
};
std::cout << fn() << fn();
}
[[assume]] New Attribute
[[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
- Relax rules for constructors and return types for
constexpr
functions, making them almost identical to regular functions. - Permitting static
constexpr
variables inconstexpr
functions.
Extend Lifetime of Temporaries in Range-Based For
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
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
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
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>
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
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
};
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:
- Lambda Trailing-Return-Type Scope Change (P2036R3, P2579R0)
- Changes the scope rules for lambda expression trailing return types
- Meaningful Exports (P2615R1)
- Improves the semantics of export declarations in modules
- Consteval Propagation (P2564R0)
- Fixes issues with consteval propagation in the language
- Unicode Identifier Syntax (P1949R7)
- Updates C++ identifier syntax to align with Unicode Standard Annex 31
- Duplicate Attributes (P2156R1)
- Allows multiple instances of the same attribute in declarations
- Concepts Feature Test Macro (P2493R0)
- Adjusts the value of
__cpp_concepts
feature test macro
- Adjusts the value of
- Wchar_t Requirements (P2460R2)
- Relaxes requirements on wchar_t to match existing implementation practices
- Unknown Pointers in Constant Expressions (P2280R4)
- Allows using unknown pointers and references in constant expressions
- Equality Operator Enhancement (P2468R2)
- Improves equality operator behavior
- char8_t Compatibility (P2513R4)
- Enhances char8_t compatibility and portability
- Diagnostic Directives Clarification (CWG2518)
- Clarifies reporting of diagnostic directives and static_assert behavior in templates
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!
Back to you
- Hav you played with C++23?
- What are the most important features for you in this release?
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here:
Similar Articles:
- What is the current time around the world? Utilizing std::chrono with time zones in C++23
- std::initializer_list in C++ 2/2 - Caveats and Improvements
- Fun with printing tables with std::format and C++20
- std::initializer_list in C++ 1/2 - Internals and Use Cases
- Structured bindings in C++17, 5 years later