C++23 Is Finalized. Here Comes C++26

Antony Polukhin
Yandex
Published in
7 min readFeb 20, 2023

--

Since our previous post six months ago, two meetings of the international C++ standardization working group have taken place.

During first meeting, the committee focused on refining the features of C++23, which include:

  • static operator[]
  • static constexpr in constexpr functions
  • Safe range-based for
  • Interaction of std::print with other console outputs
  • Monadic interface for std::expected
  • static_assert(false) and other features

On a second meeting, the committee worked on developing new features for C++26, including:

  • std::get and std::tuple_size for aggregates
  • #embed
  • Obtaining std::stacktrace from exceptions
  • Stackful coroutines

C++23

static operator[]

Last summer, the committee added the static operator() to C++23 and made it possible to define the operator[] for multiple arguments. The next logical step was to give these operators equal opportunities, namely to add the ability to write the static operator[].

enum class Color { red, green, blue };

struct kEnumToStringViewBimap {
static constexpr std::string_view operator[](Color color) noexcept {
switch(color) {
case Color::red: return "red";
case Color::green: return "green";
case Color::blue: return "blue";
}
}

static constexpr Color operator[](std::string_view color) noexcept {
if (color == "red") {
return Color::red;
} else if (color == "green") {
return Color::green;
} else if (color == "blue") {
return Color::blue;
}
}
};

// ...
assert(kEnumToStringViewBimap{}["red"] == Color::red);

Is this really efficient code for converting string to enum?

It may come as a surprise, but the code is actually very efficient. Сompiler developers use similar approaches, and we’ve implemented a similar technique in the userver framework as well. We created a separate class called utils::TrivialBiMap with a more convenient interface.

constexpr utils::TrivialBiMap kEnumToStringViewBimap = [](auto selector) {
return selector()
.Case("red", Color::red)
.Case("green", Color::green)
.Case("blue", Color::blue);
};

The high efficiency is achieved thanks to the features of modern optimizing compilers (but one must be extremely careful when writing a generalized solution). Proposal P2589R1 describes all the necessary details.

static constexpr in constexpr functions

C++23 has expanded its functionality with the addition of constexpr to_chars/from_chars. However, some implementers encountered a problem. Several standard libraries contained constant arrays for quick conversion of string<>number, which were declared as static variables within functions. Unfortunately, this prevented their use within constexpr functions. This issue can be worked around, but the workarounds looked really clumsy.

Eventually, the committee resolved the issue by allowing the use of static constexpr variables within constexpr functions, as outlined in P2647R1. A small, but welcome improvement.

Safe range-based for

This is likely the most exciting news to come out of the last two meetings!

Speaking of which, let’s start with a fun riddle: can you identify the bug in the code?

class SomeData {
public:
// ...
const std::vector<int>& Get() const { return data_; }
private:
std::vector<int> data_;
};

SomeData Foo();

int main() {
for (int v: Foo().Get()) {
std::cout << v << ',';
}
}

Here is the answer.

Range-based for loops involve a lot of underlying processes, and as a result, these types of bugs may not always be obvious. While it’s possible to effectively catch these issues through tests with sanitizers, modern projects generally include them as standard practice (at Yandex, we’re no exception to this rule). However, it would be ideal to avoid such bugs altogether whenever possible.

At RG21, we made our first attempt to improve this situation four years ago with D0890R0. Sadly, the process stalled during the discussion stage.

Thankfully, Nicolai Josuttis picked up the initiative, and in C++23, similar code will no longer create a dangling reference. All objects that are created to the right of the : in a range-based for loop are now destroyed only when exiting the loop.

For more technical details, please refer to document P2718R0.

std::print

In C++23, there’s a small but notable update to std::print: its output has been adjusted to be “synchronized” with other data outputs. While standard libraries on modern operating systems are unlikely to experience any noticeable changes, the updated standard now guarantees that messages will be output to the console in the order they appear in the source code:

printf("first");
std::print("second");

Monadic interface for std::expected

A fairly significant feature was added to C++23 at the last moment: a monadic interface has been included for std::expected, similar to the monadic interface already available for std::optional.

using std::chrono::system_clock;
std::expected<system_clock, std::string> from_iso_str(std::string_view time);
std::expected<formats::bson::Timestamp, std::string> to_bson(system_clock time);
std::expected<int, std::string> insert_into_db(formats::bson::Timestamp time);

// Somewhere in the application code...
from_iso_str(input_data)
.and_then(&to_bson)
.and_then(&insert_into_db)
// Throws “Exception” if any of the previous steps resulted in an error
.transform_error([](std::string_view error) -> std::string_view {
throw Exception(error);
})
;

You can find a full description of all the monadic interfaces for std::expected in P2505R5.

static_assert(false) and more

In addition to the significant changes outlined above, a vast number of revisions have been made to eliminate minor rough edges and improve everyday development. For example, formatters for std::thread::id and std::stacktrace (P2693) so that they can be used with std::print and std::format. std::start_lifetime_as has also received additional compile-time checks in P2679. Notably, static_assert(false) in template functions no longer triggers without instantiating the function, meaning that code such as the following will compile and issue diagnostics only if the wrong data type is passed:

template <class T>
int foo() {
if constexpr (std::is_same_v<T, int>) {
return 42;
} else if constexpr (std::is_same_v<T, float>) {
return 24;
} else {
static_assert(false, "T should be an int or a float");
}
}

In addition to the changes mentioned earlier, an innumerable amount of improvements have been made to ranges in C++23. The most significant of these is the inclusion of std::views::enumerate in P2164:

#include <ranges>

constexpr std::string_view days[] = {
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
};

for(const auto & [index, value]: std::views::enumerate(days)) {
print("{} {} \n", index, value);
}

C++26

std::get and std::tuple_size for aggregates

There’s an exciting new idea to improve C++ that we’re already actively using in Yandex Go and the userver framework, and that’s available to anyone who wants it thanks to Boost.PFR.

If you’re writing a generic template library, chances are you’ll need to use std::tuple and std::pair. However, there are some issues with these types. Firstly, they make code difficult to read and understand since the fields do not have clear names, and it can be challenging to discern the meaning of something like std::get<0>(tuple). Moreover, users of your library may not want to work with these types directly and will create objects of these types right before calling your methods, which can be inefficient due to data copying. Secondly, std::tuple and std::pair do not “propagate” the triviality of the types they store. Consequently, when passing and returning std::tuple and std::pair from functions, the compiler may generate less efficient code.

However, aggregates — structures with public fields and no special functions — are free from the aforementioned drawbacks.

The idea behind P2141R0 is to allow the use of aggregates in generic code by making std::get and std::tuple_size work with them. This would enable users to pass their structures directly to your generic library without unnecessary copying.

The idea was well-received by the committee, and we’ll be working on testing and addressing any potential rough edges going forward.

#embed

Currently, there’s active development on a new C language standard (the classless one, without the ++), which includes many useful features that have long existed in C++ (such as nullptr, auto, constexpr, static_assert, thread_local, [[noreturn]]), as well as brand new features for C++. The great news is, some new features will be ported over from the new C standard to C++26.

One of such novelties is #embed — a preprocessor directive for substituting the contents of a file as an array at compile time:

const std::byte icon_display_data[] = {
#embed "art.png"
};

Some minor details need to be worked out. The full description of the idea is available in P1967.

Obtaining std::stacktrace from exceptions

The idea from WG21’s P2370 has encountered an unexpected setback.

The ability to obtain a stack trace from an exception is present in most programming languages. This feature is incredibly useful and allows for more informative and understandable diagnostics rather than uninformative error messages like Caught exception: map::at:

Caught exception: map::at, trace:
0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600
1# bar(std::string_view) at /home/axolm/basic.cpp:6
2# main at /home/axolm/basic.cpp:17

When used in a continuous integration (CI) environment, this feature can be incredibly helpful. It allows you to quickly identify issues in the test and avoid the hassle of reproducing the problem locally, which may not always be possible.

Unfortunately, the international committee didn’t fully embrace this idea. We’ll investigate the concerns and work on refining the idea in the hopes of getting more backing.

Stackful coroutines

After years of work, the C++ standard is finally close to adding basic support for stackful coroutines in C++26 (see P0876). It’s worth delving further into stackful vs. stackless coroutines.

Stackless coroutines require compiler support and can’t be implemented on their own as a library. Stackful coroutines, on the other hand, can be implemented on their own — for example, with Boost.Context.

The former offer more efficient memory allocation, potentially better compiler optimization, and the ability to destroy them quickly. They’re also already available in C++20.

The latter are much easier to integrate into existing projects, as they don’t require a complete rewrite to a new idiom like stackless coroutines do. In fact, they completely hide the implementation details from the user, enabling them to write simple linear code that is asynchronous under the hood.

Stackless:

auto data = co_await socket.receive();
process(data);
co_await socket.send(data);
co_return; // requires function to return a special data type

Stackful:

auto data = socket.receive();
process(data);
socket.send(data);

P0876 has already been in the core subgroup. After discussions, it was decided to prohibit the migration of such coroutines between execution threads. The main reason for this prohibition is that compilers optimize access to TLS and cache the values of TLS variables:

thread_local int i = 0;
// ...
++i;
foo(); // Stackful coroutines can switch execution threads
assert(i > 0); // The compiler saved the address in a register; we’re working with the TLS of another thread

Summary

And so it’s done! C++23 has been officially sent to higher ISO authorities and will soon be published as a full standard.

Meanwhile, the development of C++26 is in full swing, and there are exciting prospects for executors, networking, pattern matching, and static reflection. If you have innovative ideas for improving C++, feel free to share them. Or — even better — consider submitting a proposal. We’ll be happy to help!

--

--

Antony Polukhin
Yandex

ISO WG21 C++ Committee Member, National Body; Boost C++ libraries developer; author of the “Boost C++ Application Development Cookbook”. Developer of userver