Contents

New C++23 features I'm excited about

Contents

Work on c++23 standardisation is well in progress and we already have a couple of new features to play with. Toolchain support varies but some early testing is already possible. I’ve prepared a list of features that I, personally appreciate a lot and which most definitely will improve my code. Let’s go through them.

CppCon overview

There’s a set of great CppCon talks regarding new additions coming with c++23. Amongst others, a nice, concise and to the point overview is probably best presented by Marc Gregoire.

Features I like

Below is a short list of things, I think are definitely a good way forward.

P0881R7 - builtin support for stacktraces

This is something very much welcomed. A builtin, standardised support for stacktrace generation is a great tool when debugging crashes or software problems. I’m sure that this feature will be embraced by crash reporting software and CI systems as well to provide more details about potential problems.

Stacktraces, could be generated prior to raising an assertion in debug versions of the builds to provide more context to the bug. This will dramatically improve the debugging efforts as, most of the time, when investigating a crash or a bug, obtaining a backtrace is crucial to understand the nature of the problem.

It seems like the implementation is gonna be based on Boost.StackTrace which seems like a reasonable choice that will guarantee sufficient quality and robustness. Obtaining stacktraces will be as simple as:

1
2
3
4
5
6
#include <stacktrace>
...
void foo() {
    auto st = std::stacktrace::current();
    std::cout << st << std::endl;
}

How does it work in practice? I wasn’t able to verify the stacktrace library so far. I’ve briefly tried with gcc 12.2.0 shipped with Arch Linux but, during the compilation of a trivial example, the compiler core dumped (heh :)). Version of clang shipped with Arch Linux is complaining about problems within the stacktrace library. This is something I definitely will verify once again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cd $HOME/dev/cpp23 && clang++ -std=c++2b ./stacktraces.cpp -lstdc++_libbacktrace && ./a.out
In file included from ./stacktraces.cpp:1:
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/stacktrace:632:3: error: no matching function for call to 'operator delete'
                _GLIBCXX_OPERATOR_DELETE (static_cast<void*>(_M_frames),
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/stacktrace:589:35: note: expanded from macro '_GLIBCXX_OPERATOR_DELETE'
# define _GLIBCXX_OPERATOR_DELETE __builtin_operator_delete
                                  ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/new:144:6: note: candidate function not viable: no known conversion from 'unsigned long' to 'const std::nothrow_t' for 2nd argument
void operator delete(void*, const std::nothrow_t&) _GLIBCXX_USE_

Implementation status

  • ❌ gcc 12.2.0 - crashes when building
  • ❌ clang 14.0.6 - fails to build

P0288R9 std::move_only_function<>

This is a great addition. Prior to it, all function objects were copied when passed as arguments. This also means that lambdas capturing move only objects could not be passed around by value. This changes entirely with std::move_only_function<>.

1
2
3
4
5
6
7
8
int foo(std::move_only_function<int(int)> f) {
  return f(123);
}

int main() {
  std::cout << foo([c = std::make_unique<int>(1)](int i) { return *c + i; }) << std::endl;
  return 0;
}

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P0323R12 std::expected

This is a new thing very similar to Result object in rust. In a way, std::expected introduces new error handling paradigm. std::expected wraps a return value and an error in case the returning function failed to complete. Here’s a simple piece of code that demonstrates this in action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
std::expected<int, std::string> foo(int n, int d) {

  if (d == 0) {
    return std::unexpected("Division by zero");
  }

  return n / d;
}

int main(int argc, const char* argv[]) {
  if (auto r = foo(123, 0); r.has_value()) {
    std::cout << r.value() << std::endl;
  } else {
    std::cout << "Error occured: " << r.error() << std::endl;
  }
  return 0;
}

There’s also value_or() which allows to conveniently retrieve the expected value or a provided default in case the std::expected contains an error.

value() called on an std::expected containing an error will result in std::bad_expected_access being thrown.

It would be great to have a similar set of monadic operations for std::expected as proposed with std::optional. Programmers with rust background will also miss language syntax for error propagation. Still though, it’s a great addition and definitely a step in the right direction.

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P2128R6 Multidimensional subscript operator

P2128R6 allows for arbitrary number of arguments to operator[]. Which is a great change. I’ve already read some articles calling operator[] a new call operator in disguise, since the semantics is now quite similar. Regardless, this is a great change which most likely will simplify a lot of code implementing operations on multidimensional data. In current C++ implementation most of the time, implementations relied on pairs of tuples to support multiple index arguments i.e:

1
T operator[](std::pair<std::size_t, std::size_t> idxs);

With C++23 it’s gonna be a lot simpler:

1
T operator[](std::size_t i, std::size_t j);

And yes, you can have multiple overloads if required:

1
2
3
4
5
6
7
8
9
T operator[](std::size_t i) {
    // implementation for single index
}

T operator[](std::size_t i, std::size_t j) {
    // implementation for two indices
}

... and so on

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P0330R8 std::size_t literals

New suffix literals for std::size_t are a great small detail that allows to avoid implicit conversion to int. To be precise, the change introduces the following suffixes:

  • z/Z for signed integer type corresponding to std::size_t
  • uz/zu for std::size_t
  • t for std::ptrdiff_t
  • ut/tu for unsigned integer type corresponding to std::ptrdiff_t

This is something I see myself relying on very often. The examples in the proposal draft illustrate the need for these suffixes very clearly.

In case of std::min, std::max I can’t even count how many times I had to static_cast the constants or explicitly declare the function types. This can now be avoided.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <algorithm>
#include <vector>

int main() {
	std::vector<int> v;
	/* work with v... */

	std::size_t clamped_space = std::max(0zu,
		std::min(54zu, v.size()) // error without the `zu` suffix
	);

	return 0;
}

Same goes, when declaring more than one variable in for loop or initialisation expressions:

1
2
3
for (auto i = 0zu; i < v.size(); ++i) {
	std::cout << i << ": " << v[i] << '\n';
}

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P0627R6 std::unreachable()

This one is pretty self explanatory. It allows to explicitly mark unreachable code which often happens at the end of the function returning a value, like this one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <utility>

...

int foo(int i) {
  if (i >= 0) {
    return i;
  } else {
    return i + 1;
  }

  std::unreachable();
}

This should silence any potential “missing return value” warnings and, at least for me, makes the code more readable and self-documenting.

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P0798R6 Monadic std::optional operations

Oh no, we’ve got the “M” word. Hopefully, if you read my post about functional parsing (which I strongly recommend) then you should have a general understanding what a monad is (or rather what it does). I’m gonna be bashed by this explanation but still gonna take the risk, in short, monadic operation transforms the value category from one to another - in case of parser combinators, this was a transformation from a parser of type “a” to a parser of type “b” (Parser A -> Parser B). In case of std::optional it will be a transformation from std::optional<A> to std::optional<B> using a given transformation function… and that’s what this change is all about.

We’ll now have the following new functions that allow to chain operations on optionals:

  • transform
  • and_then
  • or_else

transform is purely monadic and meant to indeed transform from std::optional<A> -> std::optional<B>.

and_then - allows to compose/chain functions returning optionals

or_else - allows to call a function when optional bears no value

Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
std::optional<int> divide(int n, int d) {
  if (d != 0) {
    return n / d;
  }

  return {};
}

int main() {
  auto result = divide(10,0)
    .or_else([]() -> std::optional<int>{ std::cerr << "division failed" << std::endl; return {}; })
    .and_then([](auto v) -> std::optional<int> { return v * 10; })
    .transform([](auto v) { return std::to_string(v); });

  std::cout << "result: " << result.value() << std::endl;
  return 0;
}

Best thing about this approach is that the chain will be short-circuited should the optional contain no value at any stage.

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P1938R3 consteval conditions

This is a huge change which allows to conveniently bridge the build and run time execution. It’s now possible to explicitly check, if execution happens during build time and control the flow:

1
2
3
4
5
6

if consteval {
    // this will happen during build time
} else {
    // this will happen in runtime
}

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P2216R3 std::format improvements & print header

This addition provides a new header <print> with functions like:

  • std::print
  • std::println

Initially, I thought that it’s a bit gimmicky but actually this feels pretty cool. I’m about to find out in practice how good it works and with what caveats it comes but I see myself using these functions often.

Implementation status

  • ❌ gcc 12.2.0
  • ❌ clang 14.0.6

Features I don’t like

P2334R1 New preprocessor directives

This proposal adds two new pre-processor directives:

  • #elifdef
  • #elifndef

These are to simplify the ubiquitous:

  • #elif defined()
  • #elif !defined()

Honestly, I don’t see much value in that and considering the fact that there were hopes to deprecate preprocessor use in C++ altogether I find these rather superfluous.

I’m worried as well that this will cause further code fragmentation and in order to provide as much portability as possible, we will avoid these anyway as much as we can.

Implementation status

  • ✔️ gcc 12.2.0
  • ✔️ clang 14.0.6

P0849R8 out_ptr

This change solves a real, existing problem (or rather inconvenience) in many code bases that interface with C APIs that take a pointer as a return value. Consider the following:

1
2
3
4
5
6
7
8
void allocate_linked_list(Node** n);
void deallocate_node(Node* n);

Node* head;
allocate_linked_list(&head);
// we've got a head to our linked list now
...
deallocate_node(head);

The question is how to interface that with smart pointers? The usual approach looks something like:

1
2
3
4
5
6
std::unique_ptr<Node, decltype(&deallocate_node)> head(nullptr, &deallocate_node);
Node* tmp;
allocate_linked_list(&tmp);
head.reset(tmp);

// head is now managed by a smart_ptr

Granted, this isn’t the prettiest and requires an extra temporary pointer to adopt the interface between the linked list allocation API and the smart pointer. Therefore, it is proposed to introduce a wrapper that would perform all these steps behind the scenes, like so:

1
2
3
...
allocate_linked_list(std::inout_ptr(head));
// head is now managed by a smart_ptr

I find it… redundant. The name inout_ptr is pretty bad IMHO and very poorly conveys the intentions, additionally it hides the details unnecessarily which from my point of view are quite important for code readability.

Implementation status

  • ❌ gcc 12.2.0
  • ❌ clang 14.0.6

Conclusion

C++23 brings a lot of great changes to the language. It’s definitely something I look forward to. I’m still waiting for the introduction of networking which is a piece of puzzle C++ is definitely missing out of the box.