C++23 Features in Practice
C++23 is the current published C++ standard (ISO/IEC 14882:2024). You have already used several of its library additions in this course: std::print and std::println, std::expected, the enumerate and zip views, std::ranges::to, std::to_underlying and import std;. This lesson covers the other C++23 features you are most likely to meet in modern codebases and interviews, with a note on compiler support for each.
You will learn deducing this, which removes duplicate member functions; std::generator, the first standard coroutine type; std::mdspan with the new multidimensional []; std::stacktrace; and a handful of small but handy additions.
Deducing this: one function instead of two
Classes that give access to their contents often need two versions of the same member function: a const one that returns a const&, and a non-const one that returns a plain reference. C++23 lets a member function take the object itself as an explicit first parameter, written with the keyword this. Combined with auto, one template handles both cases:
#include <cstddef>
#include <print>
#include <string>
#include <utility>
#include <vector>
class Playlist {
public:
void add(std::string song) { songs_.push_back(std::move(song)); }
auto& operator[](this auto& self, std::size_t i) { // self is const or not, as the caller is
return self.songs_[i];
}
private:
std::vector<std::string> songs_;
};
int main() {
Playlist trip;
trip.add("Morning Raga");
trip.add("Highway Song");
trip[1] = "Monsoon Song"; // non-const object: returns std::string&
const Playlist& view = trip;
std::println("{} / {}", view[0], view[1]); // const object: returns const std::string&
// view[0] = "Oops"; // error: cannot assign through a const reference
}Morning Raga / Monsoon Song
self is the object the function was called on, so there is no implicit this->; you write self.songs_. When called on a const object, self is deduced as const Playlist&, and self.songs_[i] is a const std::string&, so the auto& return type becomes const too. The same trick removes the four overloads (&, const&, &&, const&&) that library code used to write by hand.
Explicit object parameters also make recursive lambdas easy, which was awkward before:
#include <print>
int main() {
auto factorial = [](this auto self, int n) -> long long {
return n <= 1 ? 1 : n * self(n - 1); // the lambda calls itself
};
std::println("15! = {}", factorial(15));
}15! = 1307674368000
std::generator: lazy sequences with co_yield
A coroutine is a function that can pause and later continue where it stopped. C++20 added the language support (co_yield, co_await, co_return) but no ready-to-use coroutine types. C++23 adds std::generator (header <generator>): a function returning std::generator<T> produces values one at a time with co_yield, and the caller can loop over it like a range. Each value is calculated only when the loop asks for it.
In an Indian Railways sleeper coach, seats come in groups of eight, and positions 1, 4 and 7 of each group are lower berths:
#include <generator>
#include <print>
#include <ranges>
std::generator<int> lower_berths(int coach_size) {
for (int seat = 1; seat <= coach_size; ++seat) {
int position = seat % 8;
if (position == 1 || position == 4 || position == 7) {
co_yield seat; // pause here and hand out one seat
}
}
}
int main() {
bool first = true;
for (int seat : lower_berths(72) | std::views::take(6)) {
std::print("{}{}", first ? "" : ", ", seat);
first = false;
}
std::println("");
std::println("Lower berths in a 72-seat coach: {}", std::ranges::distance(lower_berths(72)));
}1, 4, 7, 9, 12, 15 Lower berths in a 72-seat coach: 27
With views::take(6), the generator runs only until the sixth berth; the rest of the loop inside lower_berths never executes. Generators can even be infinite (for (int n = 1; ; ++n)), because the caller decides how many values to take. A generator is an input range: you can loop over it once, but not go back.
std::mdspan and the multidimensional subscript
C++23 lets operator[] take several arguments, so a grid can be indexed as grid[row, col] instead of grid[row][col] or grid(row, col). The standard library uses this in std::mdspan (header <mdspan>): a non-owning view that treats a flat block of memory as a multi-dimensional array, like std::span does for one dimension.
#include <cstddef>
#include <mdspan>
#include <print>
#include <vector>
int main() {
std::vector<char> seats(3 * 4, '.'); // 3 rows of 4 seats, one block of memory
std::mdspan hall(seats.data(), 3, 4); // view it as 3 x 4
hall[0, 1] = 'X'; // C++23 multidimensional subscript
hall[2, 3] = 'X';
for (std::size_t r = 0; r < hall.extent(0); ++r) {
for (std::size_t c = 0; c < hall.extent(1); ++c) {
std::print("{}", hall[r, c]);
}
std::println("");
}
}.X.. .... ...X
extent(0) is the number of rows and extent(1) the number of columns. Storing a grid in one std::vector and viewing it with mdspan is faster than a vector of vectors, because all data sits together and there is one allocation instead of one per row. Scientific, image-processing and machine-learning code uses this layout everywhere. Your own classes can also declare T& operator[](std::size_t row, std::size_t col).
std::stacktrace: where did this happen?
When something goes wrong deep inside a program, the error message is far more useful if it says how the program got there. std::stacktrace (header <stacktrace>) captures the current call stack, and C++23 can print it with std::println:
#include <print>
#include <stacktrace>
void reserve_seat(int seat) {
if (seat < 1 || seat > 72) {
std::println("Invalid seat {}. Call stack:\n{}", seat, std::stacktrace::current());
}
}
int main() {
reserve_seat(80);
}The output lists one numbered line per active function call, starting with reserve_seat and then main, each with the source file and line when the program was built with -g. The exact format and the extra frames from the C runtime differ between compilers, so it is not shown here. With GCC you must link an extra library: -lstdc++exp in GCC 14 and later. Stack traces are especially useful in log files and in exception classes that record where they were thrown.
Smaller additions worth knowing
#include <algorithm>
#include <print>
#include <string>
#include <utility>
#include <vector>
using namespace std::string_literals;
enum class Coach { sleeper, ac3, ac2 };
int fare_per_km(Coach c) {
switch (c) {
case Coach::sleeper: return 1;
case Coach::ac3: return 3;
case Coach::ac2: return 4;
}
std::unreachable(); // promise: every enumerator is handled above
}
int main() {
std::vector stations = {"Rohtak"s, "Jaipur"s, "Pune"s};
std::string route = "Delhi-Rohtak-Jaipur";
std::println("{}", std::ranges::contains(stations, "Pune"s)); // C++23 algorithm
std::println("{}", route.contains("Rohtak")); // C++23 string member
for (auto i = 0uz; i < stations.size(); ++i) { // uz: a std::size_t literal
std::println("{}. {}", i + 1, stations[i]);
}
std::println("AC3 fare for 500 km: Rs. {}", fare_per_km(Coach::ac3) * 500);
}true true 1. Rohtak 2. Jaipur 3. Pune AC3 fare for 500 km: Rs. 1500
| Feature | What it does |
|---|---|
std::ranges::contains | Is a value in the range? No more find(...) != end() |
string::contains | Does the text contain a substring or character? |
0uz | A std::size_t literal, so loop counters match size() without casts or warnings |
std::unreachable() | Tells the compiler a point is never reached; reaching it is undefined behaviour |
[[assume(expr)]] | Lets the optimiser assume a condition is true; like unreachable, a wrong promise is UB |
std::move_only_function | Like std::function, but can hold move-only callables such as a lambda owning a unique_ptr |
optional and_then, transform, or_else | Chain steps on an optional without if checks |
Use std::unreachable() and [[assume]] only when you are certain; in debug builds, an assert or a thrown exception is safer.
Common mistakes
- Writing
this->inside an explicit-object function. There is no implicitthis; use the named parameter, such asself. - Looping over a generator twice. It is a one-pass input range; call the function again for a new sequence.
- Using
grid[r, c]on a type without a two-argumentoperator[]. On a plain vector of vectors,[r, c]is not valid; it was a comma expression before C++23 and is now an error in that position. - Thinking
mdspanowns memory. Likespan, it is a view; the vector must outlive it. - Using
std::unreachable()as an error handler. If the point can be reached, the program has undefined behaviour.
Key points
- C++23 is the current ISO standard; its library additions include
print,expected,generator,mdspan,stacktraceandflat_map. - Deducing
this(this auto& self) replaces duplicatedconstand non-constmember functions and enables recursive lambdas. std::generatorwithco_yieldproduces values lazily and works with range pipelines.std::mdspanviews flat memory as a grid, indexed with the new[r, c]subscript.- Check compiler support for each feature; library features arrive at different times in GCC, Clang and MSVC.
Exercise
Write a generator std::generator<int> tokens(int start, int step) for a clinic in Rohtak that yields token numbers forever: start, start + step, start + 2 * step, and so on. Print the first five tokens starting at 101 with step 5, using std::views::take.
Show solution
#include <generator>
#include <print>
#include <ranges>
std::generator<int> tokens(int start, int step) {
for (int n = start; ; n += step) {
co_yield n;
}
}
int main() {
for (int t : tokens(101, 5) | std::views::take(5)) {
std::println("Token {}", t);
}
}Token 101 Token 106 Token 111 Token 116 Token 121
The loop inside tokens never ends on its own, but that is fine: the generator only runs when the next value is requested, and take(5) stops asking after five.
Quick Quiz
1. What does auto& operator[](this auto& self, std::size_t i) replace?
self is deduced as const or non-const from the object, so one template covers both.
2. When does code inside a std::generator function run?
The coroutine pauses at each co_yield until the next value is requested.
3. What is std::mdspan?
Like span, it does not own memory; it adds multi-dimensional indexing.
4. What happens if the program actually reaches std::unreachable()?
It is a promise to the optimiser, not a check.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
