In this article, Oleksandr Syniakov, who has been working with C++ for over 20 years, explores the latest C++ standards—C++20 and C++23—and some of their new features. With each new release of the standard, the code improves and undergoes significant transformation.
The new standards have a lot to offer, so let's take a look at how the old code looks, how it will look with the release of the new standard, and how it will affect you as a programmer. Of course, you won't find all the new features here. They are gathered based on what would be used most frequently, excluding features that compilers haven't implemented yet, and that cannot be tried out.
Formatting
The first interesting feature is formatting. This feature is not ranked second by mistake, as it is a critical component of a new modern print we will be using going forward. Formatting has always been a pain in C++.
In the pre-C++20 era, you would write:
Formatting in C++20 has undergone significant changes. Now, text formatting has become easier with std::format. std::format has an intuitive syntax similar to other programming languages.
And in C++23, it is simplified to:
<stdfloat>
In addition to int32_t, int64_t, and other types, special floating-point types have also been added:
They have a binary floating-point type with widths of exactly 16, 32, 64, and 128 bits, respectively.
Designated initializers
The designated initializers feature has been around for some time as a non-standard extension, but it has now been standardized in C++20. With designated initializers, the syntax becomes more streamlined and intuitive. You can write your code as follows:
You can initialize variables by specifying their names directly in the initializer. This approach enhances code clarity and reduces errors. If a variable within a structure or class is not explicitly initialized in this manner, it will be default initialized, ensuring safer and more predictable behavior in your C++ programs.
For statements with initializer
This feature appeared in C++20. It extends the idea of combining variable initialization with control structures to the range-based for loop. This enhancement allows for the initialization of a variable directly within the for loop declaration, further simplifying and clarifying code.
Initializing a container in the loop looks in the following way:
In this example, a container is initialized with the return value of clip() and is then used in the range-based for loop to iterate over its items. This pattern is particularly useful when the container is only relevant within the loop's scope.
Combining the loop iteration with another operation looks as follows:
Here, i is initialized to 0 and is incremented within the loop body. It allows for tracking the iteration count while iterating over the elements returned by foo().
The given examples highlight several advantages of this feature:
- Code clarity: By moving variable declarations into the for loop, code becomes more readable, and it's clear where and why each variable is used.
- Reduced scope: Variables declared within the for loop are confined to that loop's scope, minimizing the risk of scope-related errors and making the code cleaner.
- Convenience: This syntax is particularly handy when dealing with temporary or single-use containers and counters, as it avoids cluttering surrounding code with these transient variables.
To sum up, C++20's enhancement to the range-based for loop aligns with the language's ongoing evolution towards more concise, expressive, and safe coding practices.
Scoped enum
Enum classes were a significant improvement in type safety and namespace pollution in C++11 over traditional enums. However, one inconvenience with scoped enums was the need to use the enum's fully qualified name each time one of its values is referenced. In C++11, you would have to write similarly to the example below:
In C++20, the introduction of using enum directive within a scope, such as a switch statement, allows you to use the enumerator names directly without needing to qualify them with the enum class name. It simplifies code and improves readability while maintaining the type safety and scoping benefits of enum classes. The C++20 version of the previous example would look in the following way:
This improvement in C++20 enhances the usability of scoped enums by reducing verbosity without sacrificing their advantages.
Spaceship operator
In C++20, a notable feature called the three-way comparison operator or spaceship operator (<=>) was introduced. This operator simplifies the way developers define comparison behavior for classes or structs. With this operator, the compiler can automatically generate all the necessary comparison operators for you, reducing boilerplate code and potential errors. Consider the following example:
Here, by simply defining operator<=> and marking it as default, the compiler automatically generates all the standard comparison operators (==, !=, <, >, <=, >=) for the RaycastHit struct. This feature greatly simplifies code, especially when working with structures or classes where comparison is necessary. Thanks to the automatically generated comparison operators, code is much more concise and maintainable. It is an example of how modern C++ continues to evolve towards enabling more expressive, concise, and type-safe code.
Template syntax for lambdas
C++20 introduced a significant enhancement to lambda expressions by allowing them to be templated. This addition means that you can now write lambda functions that accept template parameters, providing the same flexibility and power that comes with templated functions.
Here's an example of a templated lambda in C++20:
In this example, the lambda f is templated with a type parameter T, and it takes a std::span of that type as an argument. This feature allows the lambda to work with a wide range of types, making it highly versatile.
However, it's important to note the syntax required to call such a templated lambda. When you want to invoke this lambda with a specific type, you need to specify the type explicitly. The syntax for calling the templated lambda uses the operator() method explicitly, along with the template argument. For instance, to call this lambda for the int type, you would write:
Here, v would be a std::span<int> or similar container that matches the expected lambda parameter. This explicit syntax is necessary because the lambda does not have a name in the traditional sense, so you must use operator() to call it with a specific template argument.
The introduction of templated lambdas in C++20 opens up new possibilities for generic programming, allowing for more concise and flexible lambda expressions that can be tailored to a wide range of types. This enhancement further demonstrates C++'s ongoing evolution to support more advanced and expressive programming paradigms.
span
std::span is a template class introduced in C++20 that provides a view over a contiguous sequence of objects, much like how std::string_view offers a view over a sequence of characters. It can be highly beneficial for functions that need to operate on a slice of an array or a container without caring about its type or ownership, which can lead to cleaner and more efficient code.
Benefits of std::span:
- Safety and bounds checking: Unlike raw pointers, std::span includes information about the number of elements it refers to, enabling bounds checking.
- Being type agnostic: It can be used with any contiguous sequence, such as raw arrays, std::vector, std::array, or even manual memory allocations.
- No ownership: std::span does not own the memory it points to, avoiding any memory management issues and overhead.
- Runtime flexibility: The size of std::span can be set at runtime, unlike std::array, which requires a compile-time size.
- Function parameter simplification: Functions can accept std::span instead of different overloads for pointers with a size, different types of containers, and so on.
In the example above, calculateCentroid is a function that accepts std::span to any contiguous sequence of elements. It means you can use it to process part of std::vector, a subset of a raw array, or any other contiguous sequence without overloading the function for different container types or worrying about the lifetime of the data being pointed to.
Expected
C++23 std::expected is a concept that provides a type that can hold either a value (similar to std::optional) or error information (often in the form of an exception or error code). It is useful for functions that usually return a value but may encounter errors that should not lead to an immediate throw of an exception. Thus, std::expected offers a way to safely return and handle potential errors without using exceptions for control flow management.
Now we can safely divide one number by another and check the result and error, if any:
special literal for size_t
At the moment, you can safely use the index as size_t without mixing it with int in C++23.
Compare signed and unsigned integers
Comparing signed and unsigned integers has always been a very painful issue in C++. A huge number of programmers still do not know the rules for comparing signed and unsigned integers. To avoid it, C++23 introduced special functions that take all these peculiarities into account.
The following example produces output that reflects the typical behavior of signed and unsigned integers when compared.
std::source_location
The std::source_location class in C++20 represents certain information about source code, such as file names, line numbers, and function names.
Stack trace
It is a special class in C++23 that represents a snapshot of the whole stack trace or its given part. Finally, you can log and output stack traces.
Output:
<numbers>
In C++, there are specialized constexpr mathematical constants for all floating point types, including pi, inv_pi, and so on.
Example:
<bit>
C++20 introduced plenty of useful functions for bits manipulations. Check the bit header for more info. For example, popcount counts the number of 1-bits in an unsigned integer:
Multidimensional subscript operator
In C++23, you can now define operator[] that takes any number of subscripts. For example:
It greatly simplifies the way of writing a subscript operator.
<mdspan>
C++23 mdspan<T> represents a multidimensional span of elements, extending the concept introduced by std::span<T> into the multidimensional space.
At its simplest, mdspan<T> can be thought of as a structure comprising a pointer to the first element (T* ptr) and an array of extents (size_type extents[d]) that define the size of each dimension in a multidimensional array (d being the number of dimensions, which is determined at runtime).
Further, you can see a very simple example that demonstrates how efficient mdspan is.