C++23 is complete and pretty much finalized, so let’s explore the new features in C++, from a data science point of view. This is not as large of a release as C++20 or C++11, but it’s still a hefty release with lots of nice new features - larger than C++17.
Some of the major new features are building on themes from C++20, such as vastly
expanded ranges and views, more constexpr/consteval, module support for the
standard library, std::print to supplement std::format, and a
std::generator to support coroutines. There are also several general features,
lambdas and classes get along much better now, there are new preprocessor
additions (!), a stacktrace library, flat maps/sets, and new std::optional
monadics, deducing this, and many various additions.
Updates on C++20
Like C++14, there were quite a few updates to features added in the previous standard. Let’s start by revisiting my major points from C++20, and see how they were updated. The only one without an update was concepts.
Standard lib as a module
Module support in C++ landed in C++20, but there was a glaring omission: the standard library! That’s now been corrected (P2465).
Classic C++
#include <vector>
#include <iostream>
...
C++23
import std;
This is fantastic, and shows the power of modules. You don’t need to list a bunch of includes, just this one import, and now the compiler can intelligently select what is needed. It’s so clean!
More Constexpr all the things
The largest new addition is if consteval (P1938), which further expands on
consteval from the last release.
There are also more constexpr additions: std::optional, std::variant
(P2231), and std::unique_ptr (!) (P2273), along with the items in cmath
and cstdlib (P0533). This really improves compile time programming in C++23
over its predecessors.
Ranges redux
More additions to ranges! This is a massive usability improvement. It’s really
hard to do much with ranges without a zip, and now it’s there!
auto num = std::vector{1, 2, 3};
auto letters = std::array{'A', 'B', 'C'};
for (const auto [n, l] : std::views::zip(num, letters)) {
std::println("{} {}", n, l);
}
The additions to std::ranges::* are:
to: Convert ranges to containers (including nesting!) (P1206)iota: Likestd::iota(P2440)shift_left/shift_right: likestd::shift_left/std::shift_right(P2440)fold*: Folding algorithms (P2322)find_last*: Find last element that matches (P1223)starts_with/ends_with: Check beginning/end of range with another range (P1659)contains/contains_subrange: If a range contains an element or another range (P2302)
And, std::views::*:
zip*: Combine ranges (shown above) (P2321)zip_transform: Feed zip output to a function (P2321)chunk/chunk_by: Divides range into chunks, last chunk can be shorter (good for splitting up work, for example) (P2442, P2443)adjacent: Make a sliding window (overlapping chunks) (P2321)adjacent_transform: Apply a function on the adjacent window (P2321)slide: Runtime version ofadjacent, sliding window (P2442)cartesian_product: Compute a Cartesian product (P2374)join_with: Flatten range of ranges into a range (P2441)repeat: Repeats a range (n times or infinite) (P2474)enumerate: Pair each element with its index, like Python (P2164)stride: Take every nth element (P1899)as_const/as_rvalue: Change how elements are handed out (P2278, P2446)
There still are things missing that were in ranges v3, but a lot of them can be easily implemented using the next feature in this list!
Coroutines - std::generator
Making a generator is one of the first things users likely think of when seeing
coroutines in C++20, and now it’s much easier with std::generator (P2502,
which is an input range). For example, we could create an infinite range, and
then use take to get the first 10 items:
import std;
std::generator<int> range(int start = 0) {
while(true) {
co_yield start++;
}
}
int main() {
for (int i : range() | std::views::take(10)) {
...
}
}
Formatting now has std::print(ln)
This was skipped over for C++20, but now is here; std::print and
std::println (P2093)! We can finally say goodbye to
std::cout << std::format(...) << std::endl. These write to stdout by
default, but you can give a FILE* or an std::ostream as the first argument
(P2539).
You can also use std::format and friends on ranges (P2286).
New features
Now the major new features of C++23 that don’t expand on C++20.
MDSpan
C++20 deprecated using the comma operator, and now it’s back as a
multidimensional subscript (P2128)! item[1,2] is now valid C++. This is
implemented in a brand new std::mdspan (P0009), which gives you a
multidimensional view of an existing data structure. This means we have a
language for passing views of and using multidimensional arrays
std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
auto view = std::mdspan(v.data(), 2, 6);
// view.extent(0) == 2
// view.extent(1) == 6
// view[0,0] == 1
This is only available as a view (joining std::span and std::string_view
from C++17) for now. Also, sub-views are not yet included (likely addition for
C++26).
Deducing this
One of the most powerful tiny additions is “deducing this” (P0847). This
allows you to explicitly take a “self” argument (noted by the this keyword
proceeding it).
It looks like this:
Classic C++
struct Foo {
void bar() &;
void bar() const&;
void bar() &&;
void bar() const &&;
};
C++23
struct Foo {
void bar(this Foo& self);
void bar(this Foo const& self);
void bar(this Foo&& self);
void bar(this Foo const&& self);
};
Why would you want to write it that way? Well, besides being more consistent with the way you specify other parameters, and looking more like languages like Python, there’s a huge reason to do this: you can template on this parameter! For example, the above four overloads can be written as:
struct Foo {
template<typename Self>
void bar(this Self&& self) {...}
};
Now you no longer need to make separate const and non-const versions of your methods!
Another use of this is for CRTP. One of the first advanced C++ tricks you might
learn is CRTP, which allows you to implement compile-time polymorphism by
requiring a subclass to put itself into a template parameter. Deducing this
allows you to simply write this as normal class, since this is deduced as the
child type!
Here’s an example, taken from CLI11’s Options classes, with shortened names for clarity:
Classic C++
template<typename CRTP>
struct OptionBase {
/// [snip]
CRTP* take_last() {
auto *self = static_cast<CRTP *>(this);
self->mo_policy(MOPolicy::TakeLast);
return self;
}
};
struct OptionDefaults : OptionBase<OptionDefaults> {
/// [snip]
};
struct Option : OptionBase<Option> {
/// [snip]
};
C++23
struct OptionBase {
/// [snip]
template<typename Self>
auto take_last(this Self&& self) {
self.mo_policy(MOPolicy::TakeLast);
return self;
}
};
struct OptionDefaults : OptionBase {
/// [snip]
};
struct Option : OptionBase {
/// [snip]
};
Simplifying the base class (no longer a template class), the methods, and the child classes (no longer having to template on themselves) is a huge win!
There are other things that can be done with this as well, like recursive
lambdas. By taking a this auto self, you can call self(...) inside the
lambda. To read more, see the Deducing this article or the paper (P0847).
std::expected and monadics
C++ is well known for “pay only for what you use”. The best known exception to
that rule is exception handling; you pay for it (in binary size, etc.) even if
you don’t use it. A different take on handling exceptions is present in Rust:
you make the exception part of a variadic return type. This both avoids
traditional exception handling and forces a user to explicitly handle all
expected errors. Similar to how std::optional is a specialization of a variant
for a value or None return, std::expected (P0323) is a specialized variant
for a value or an error. C++ doesn’t have as many syntax features to help you
use this, but it’s still a welcome addition. And we do have monadic methods
.and_then, .or_else, .transform, and .transform_error (P2505)! These
allow a very elegant chaining of expected return types.
That’s not the only place monadics have been added; std::optional gets them
too (P0798).
std::optional<int> v = /* something */;
v.transform([](int n){...}) // Processes values, skips if nullopt
v.and_then([](int n){...}) // Similar, but returns optionals
v.or_else([]{...}) // Converts empty values, returns optionals
Combined with the existing .value_or, these provide a way to work with
optionals and expected quite elegantly.
Smaller updates
Lambda functions
We can now drop the () from a lambda that doesn’t take any parameters
(P1102). Lambdas can be declared static if they don’t capture anything, which
saves a registry call (P1169).
Preprocessor updates
We finally have #elifdef and #elifndef (P2334), which you would rather
expect since we have #ifdef. #warning is now officially part of the language
(P2437).
Range based for
This used to be buggy:
for(auto&& val: foo().bar()) {...}
Here, foo returns a temporary object, and bar returns a reference to a member,
the compiler could destroy the object before the body of the loop, causing the
reference to be invalid. Now, the lifetime of objects created on the right of
the colon guaranteed to last through the body of the loop (P2718).
Contains
There are now member functions for std::string and std::string_view to check
to see if a string contains a substring or a single character (P1679)! This
tiny thing is a staple in most other languages (it even has a keyword, in, in
Python) and was sorely lacking in C++. There is a matching new ranges feature
for arbitrary ranges, as well (P2302). This complements the .contains that
containers got in C++20.
Even smaller things
Here are a few too small to write up much about:
- Literal suffix for
std::size_t(uz) andstd::ssize_t(z) (P0330) auto(x)/auto{x}, which do a decay copy (and optimize to nothing on prvalues) (P0849) (nice post)- Class template argument deduction improvements for inheritance (P2582) (nice post)
operator()andoperator[]can be static (P1169, P2589)[[assume]]attribute to tell the compiler that an (unevaluated) expression can be assumed true (P1774)std::to_underlyingto convert an enum to the underlying type. (P1682)std::unreachableto mark unreachable code (P0627)std::move_only_function, thestd::functionyou want when the callable holds aunique_ptr(P0288)std::forward_liketo forward a member with the value category of the object it came from - handy with deducingthis(P2445)std::out_ptr/std::inout_ptrto pass a smart pointer to a C API that wants aT**(P1132)std::start_lifetime_as, the well-defined way to say “these bytes are now aT” instead of thereinterpret_castmost of us write (P2590)std::byteswapto reverse the bytes of an integer, useful for file formats (P1272)std::string::resize_and_overwriteto fill a buffer without paying for the zero fill first (P1072)std::invoke_r, anstd::invokewith an explicit return type (P2136)- Labels are allowed at the end of a compound statement (P2324)
- Narrowing contextual conversions are allowed in
static_assertandif constexpr(P1401)
Other new libraries
We’ve already covered <print>, <expected>, and <mdspan> above.
Stacktraces
C++ now has a built in library to help with stacktraces, <stacktrace>
(P0881). For example:
std::cout << std::stacktrace::current() << '\n';
Better support for exceptions is planned for C++26.
Flat maps and sets
There are new flat (dense storage) versions of maps and sets (P0429, P1222). These store the map/set values together in memory, at the expense of modification time. In general, these are probably better if you don’t have to modify them much.
Spanstream
<spanstream> gives you a stream over a buffer you own, like std::span does
for arrays (P0448). Unlike stringstream, there is no allocation, which makes
it a good fit for parsing.
Final words
C++23 is a great addition to the language. C++ is still getting Rust-like
features (like std::expected), better ranges, and features that deeply impact
what you can do (deducing this). Now we need to watch the various compilers to
see when we can start using these features.
I normally post reports from the meetings here. But with COVID, this development cycle has been very strange. Here’s what I could find:
- Winter 2022: Herb Sutter’s Issaquah report: C++23 is complete
- Autumn 2022: Herb Sutter’s Kona report
- Summer 2021: Herb Sutter’s virtual report
- Winter 2021: Herb Sutter’s virtual report
- Autumn 2020: Herb Sutter’s virtual report
C++26
The first C++26 meeting was held hybrid:
- 2023 Summer: Sutter’s Mill Bulgaria
A few exciting things are already adopted for C++26, including _ as a
placeholder, work in making string formatting support constexpr, a type-erased
callable reference, 100 more constexpr functions, even constexpr sorting
(including ranges-based sorts).
C++26 looks to be focused on Contracts, Reflection (maybe?), and the async model
(senders/receivers). Executors and a form of std::simd could land too. It’s
much too early to tell what will make it in, but if Reflection does, it will be
the biggest change to C++ in a long time.
The next ++?
There also is a lot of work going into making a successor language to C++. It’s unclear what that would be, with several groups working on several different languages, like:
- Val: making a safe version of C++
- Carbon: A cleanup project by Google, but interop with C++ will be hard due to removed features like constructors.
- Cpp2: A better C++, 50x safer and 10x simpler (target goal) from Herb Sutter
What makes these different from some other languages (like D, Go, and Rust) is that they are designed to interface smoothly with C++. A great article on these can be found here.