C++23: A std::expected-ly Graceful Release
With the last big release, namely C++20, the language had been extended with what I’d consider some essential features a modern programming language should have. Stackless coroutines are basic necessities to elegantly and efficiently implement asynchronous code, statemachines and more. Modules should significantly improve compilation times. And concepts will make working with templates a better experience. Understandably with such a huge release the expectations for C++23 were high. Nevertheless the new release turned out to be more down-to-earth, but that might actually be a good thing!
What’s new in C++23?
Both wikipedia.org and cppreference.com feature a nice overview on the changes introduced with C++23. Reading through the list one might notice a lot of small improvements to the language as well as the standard template library. In contrast to the previous release where the most exciting changes where language related, this time around I think the STL changes will have a larger impact. This is why I’d like to use the chance to discuss a few exciting ones.
std::expected
Lot’s of people will likely recall C-like API’s with integer return codes or implementations throwing random exceptions. Modern languages have cleaner alternatives to signal errors to the caller. In Rust we have the Result
type and in Haskell there is Maybe
. Both allowing us to either return a value or an error. While implementing a similar solution in C++ isn’t too hard, with C++23 we now have a standardized solution with std::expected. Combined with some basic monadic functions this ends up being rather useful to write elegant code including error handling.
enum class HttpError {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
};
struct Uuid { /* ... */ };
struct SongProperties { /* ... */ };
std::expected<SongProperties, HttpError> getSongProperties(const Uuid songUuid);
Now let’s consider the hypothetical api and process the result. It’s easy the check the state of the result by calling operator bool()
and then retrieve either the value by dereferencing the std::expected
instance or the error by calling the error()
member function. Additionally some monadic functions are available: and_then
, or_else
, transform
and transform_error
. The first allows us to process a result containing a value, the second an error case. The other two transform instances containing either a value or an error. As an example this allows us to write rather nicely readable code like the following example:
void handleRestError(const HttpError);
std::expected<void, HttpError> startPlayback(const SongProperties&);
void playSong(const Uuid songUuid)
{
getSongProperties(songUuid)
.and_then(startPlayback)
.or_else(handleRestError);
}
As part of the proposal Sy Brand, github.com:TartanLlama/expected has already implemented this proposal and made it available under a creative commons license.
std::optional
While std::optional
certainly isn’t new, it received the same treatment as it’s similar sibling std::expected.
. The monadic functions and_then
, or_else
and transform
are also available.
std::generator
The ``std::generator` is the first STL component I’m aware of that makes use of the stackless coroutines added to C++20. It’s a simple way of showing how using a coroutine might look like. It allows us to generate an output, yield the result, suspend the coroutine and resume it later on. The example below generates the fibonacci numbers one by one.
#include <cstdint>
#include <generator>
#include <iostream>
#include <ranges>
std::generator<std::uint64_t> fibonacci()
{
std::uint64_t a = 0, b = 1;
while (true) {
co_yield a;
const std::uint64_t sum = a + b;
a = b;
b = sum;
}
}
int main()
{
for ( const auto& value : fibonacci() | std::views::take(10)) {
std::cout << value << "\n";
}
}
A similar implementation of std::generator
can already be found in cppcoro. Unfortunately no STL implementation is shipping an official implementation yet.
std::flat_map and std::flat_set
While in most cases std::unordered_map
and std::unordered_set
should be the first choice, sometimes sorting might be required. In those cases std::map
and std::set
are an option. Unfortunately those two are implemented on top of balanced binary trees which requires expensive re-balancing. Sandor Dargo explains on his blog how the new flat versions resolve these limitations.
Fortunately since this limitation isn’t new, there are already implementations available. One of those can be found in boost.
std::stacktrace
Another feature available in boost is boost::stacktrace
which has been the template for std::stacktrace
. It allows the caller to get information about the current stack. The boost documentation mentions a few good use cases. One example is dumping the current stack information if the application terminated unexpectedly due to an unhandled exception:
#include <iostream>
#include <stracktrace>
void onTerminate() {
try {
std::cerr << std::stacktrace::current() << '\n';
} catch (...) {}
std::abort();
}
std::set_terminate(&onTerminate);
As also described in the boost documentation, it might be a good idea to add the stack information whenever throwing an exception.
Conclusion
Next to the mentioned changes there is of course more in C++23. The std module and improvements to the ranges library just to name another few big improvements. What differentiates those partially from the listed goodies, is that the latter can be used today. Either through home-grown implementations or through existing libraries.
Additionally C++20 support still isn’t that great. Only Microsoft features full C++20 support. While GCC and clang both don’t have full support for modules, GCC at least has coroutine support. The team behind cmake is working on supporting modules. Summing up the current state, it looks like it will take some more time until we can make use of most of the C++20 and C++23 features.