C++ 20 and 23 have a lot of new features which can reduce boilerplate code, increase efficiency and make the code drastically more readable. Not only they can be used with mainstream applications, but also a lot of advantages for embedded world can be discussed.
Here we will briefly take a look at few select features.
Concepts (C++ 20)
Concepts are very important features which come from the mathematics world. They can increase readability and reduce debug time drastically. Before concepts came around, if we wanted to make compile-time decisions based on type properties, we would use SFINAE (Substitution failure is not an error) principle and traits library. This was not a clean solution and wasn’t easy to debug. But then concepts came around to address this problem. With concepts we can make compile-time decisions such as choosing which function overload based on tem- plate’s properties, strictly describe the properties a template parameter should possess and so on. For example lets say we have a function called Adder which just takes two objects of the same type and returns the result of ‘+’ operator on them:
template
auto Caller(T&& first, T&& second)
{
return first + second;
}
Now this is simple enough but imagine you have function which does a lot more operations on the object. What happens if we pass an object that does not implement the ‘+’ operator? Well we get errors, but not a very readable one! and specially if our object needs to satisfy multiple properties, either we need to read the whole code to extract them or try adding one property at a time to get all of them for example. Now lets use concepts here:
template
concept Addable = requires (T&& t)
{
t + t;
};
template
auto Caller(T&& first, T&& second) requires Addable
{
return first + second;
}
Or even simpler
template
auto Caller(T&& first, T&& second)
{
return first + second;
}
Basically what Addable is saying is that all of operations and lines of code performed with this type (for instance here just “a + b”) must be valid an compliable, otherwise, that’s an error. Now if the template type T is not addable compiler will give us a better error and using this mechanism we can avoid introducing undefined behavior and debug the code easier.
3-way comparisons (C++ 20)
3-way comparisons or otherwise known as spaceship operator, is a really neat new feature of C++ which can reduce significant amount of boilerplate code. It is used to guide the compiler on generating comparison operators automatically, thus reducing significantly the amount of redundant code the developer must implement. We implement only this operator (or even just use =default to ask the compiler to do it for us) and the compiler automatically generates all of six ==, !=, <, <=, >, and >= comparison operators! isn’t that just amazing? Lets take a look at how it works. In a basic sense this is how it simplifies the comparisons, The expression a <=> b returns an object such that:
(a <=> b) < 0 if a < b
(a <=> b) > 0 if a > b
(a <=> b) == 0 if a and b are equal/equivalent.
Here’s an example:
#include
#include
int main()
{
double foo = -0.0;
double bar = 0.0;
auto res = foo <=> bar;
if (res < 0)
std::cout << "-0 is less than 0";
else if (res > 0)
std::cout << "-0 is greater than 0";
else if (res == 0)
std::cout << "-0 and 0 are equal";
else
std::cout << "-0 and 0 are unordered";
}
the output:
-0 and 0 are equal
Constexpr relaxations (C++ 20)
C++ 20 compile time context is now Turing complete! What does that exactly mean? Well we were already able to some computations in compile time like:
constexpr auto Add(int a, int b)
{
return a + b;
}
...
constexpr auto result = add(10, 20);
The value of result will be calculated at compile time and inserted before the compilation. But what with C++ 20 is new is that we can now also call new and delete in compile time context but with one caveat. The compile time heap memory usage must not introduce memory problems and undefined behavior. Meaning if we allocate some variable with new and we dont delete it, we will get compilation error! How great is that?
For example if we try to compile this code:
#include
#include
constexpr int Allocate()
{
int * a = new int(10);
int b = *a;
delete a;
return b;
}
int main()
{
constexpr auto res = Allocate();
return res;
}
this will compile without problem but watch what happens when we forget to delete the allocated memory:
#include
#include
constexpr int Allocate()
{
int * a = new int(10);
int b = *a;
// delete a;
return b;
}
int main()
{
constexpr auto res = Allocate();
return res;
}
Compiler gives us:
This way we can even test our code for memory leaks and other problems at compile time and get a compiler error instead of runtime error and we can guarantee the code is memory safe. This is really huge.
source_location (C++ 20)
source_location is an awesome tool added to C++ 20 and can be used to query some information about te source code like the file name, file line, etc. It is specially beneficial in logging and debugging. Here’s an eample of how we can use it:
#include
#include
int main() {
const auto sl = std::source_location::current();
std::cout << sl.file_name() << "("
<< sl.line() << ":"
<< sl.column() << ") "
<< sl.function_name() << std::endl;
return 0;
}
The output would be for example:
example.cpp(28:50) int main()
Or in a more practical sense we can do:
#include
#include
void log(std::string const& message, const std::source_location sl = std::source_location::
{
std::cout << sl.file_name() << "("
<< sl.line() << ":"
<< sl.column() << ") "
<< sl.function_name() << " : "
<< message << 'n';
}
int main() {
Log("Hello world");
return 0;
}
Which would output something like:
example.cpp(25:8) int main() : Hello world
Notice one very important thing here that function parameters are constructed on the caller function’s stack therefore source_location object will actually con- tain the source_location data about the caller function which is actually a genius idea and can be useful to many logging libraries
Format (C++ 20 and 23)
In C++ 20 some mechanisms similar to fmt library was added to standard library. lib fmt is a very popular and loved library for string formatting and printing which is a very good and easy to use substitute for using std::cout. Unfortunately not all of the features are adopted. Here is an example:
#include
#include
#include
#include
int main() {
std::cout << std::format("Hello {}!n", "world");
std::string fmt;
for (int i{}; i != 3; ++i) {
fmt += "{} "; // constructs the formatting string
std::cout << fmt << " : ";
std::cout << dyna_print(fmt, "alpha", 'Z', 3.14, "unused");
std::cout << 'n';
}
}
The good news is that in C++ 23 more lib fmt like features are adopted for example std::print and even an extra std::println is added.
#include
#include
#include
int main()
{
std::print("{0} {2}{1}!n", "Hello", 23, "C++"); // overload (1)
const auto tmp {std::filesystem::temp_directory_path() / "test.txt"};
if (std::FILE* stream {std::fopen(tmp.c_str(), "w")})
{
std::print(stream, "File: {}", tmp.string()); // overload (2)
std::fclose(stream);
}
}
Modules (C++ 20)
C++ 20 introduced modules. Modules are translation units which will pro- vide functionalities for other parts of the code. They can be an alternative to header/source includes.
here’s an example of exporting a module:
export module helloworld; // module declaration
import ; // import declaration
export void hello() // export declaration
{
std::cout << "Hello world!n";
}
export
{
int one() { return 1; }
int zero() { return 0; }
}
// Exporting namespaces also works: hi::english() and hi::french() will be visible.
export namespace hi
{
char const* english() { return "Hi!"; }
char const* french() { return "Salut!"; }
}
and like this we can use it:
import helloworld; // import declaration
int main()
{
hello();
}
Consteval if (C++ 23)
Executing in compile time is awesome but only if there was a way to know if a function is executing in compile time or runtime so that we could for example choose a more compile-time friendly algorithm if needed. Well good news is that there is now! with consteval if we can make decisions based on the execution context and execute different strategies for compile time and runtime execution. Here’s an example:
constexpr int fibonacci(int n)
{
if consteval
{
// choose a compile time friendly algorithm
}
else
{
// choose another algorithm
}
}
Multi-dimension array subscript operator (C++ 23)
Finally the moment we’ve been waiting for. We can finally have multi-dimension array subscript operators. Here’s an example:
#include
#include
#include
template
struct Array3d
{
std::array m{};
constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23
{
assert(x < X and y < Y and z < Z);
return m[z * Y * X + y * X + x];
}
};
int main()
{
Array3d v;
v[3, 2, 1] = 42;
std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << 'n';
}
Deducing this (C++ 23)
In a lot of other languages, it is possible to explicitly access the current object through the parameter list of a member function. It is also now possible in C++. A non-static member function can be declared to take as its first parameter an explicit object parameter, denoted with the prefixed keyword this.
struct X
{
void foo(this X const& self, int i); // same as void foo(int i) const &;
// void foo(int i) const &; // Error: already declared
void bar(this X self, int i); // pass object by value: makes a copy of `*this`
};
By taking advantage of this new feature we can do a technique called deducing this like bellow:
struct X
{
template void foo(this Self&&, int);
};
struct D : X {};
void ex(X& x, D& d)
{
x.foo(1); // Self = X&
move(x).foo(2); // Self = X
d.foo(3); // Self = D&
}
Its specific benefit is that it will help us reduce boilerplate code specially when writing the same member functions for const and non-const instances of the object.
Stacktrace (C++ 23)
Although this feature is still very immature and not yet fully supported, but it is possible to use it with the trunk version of gcc and when linking against -lstdc++_libbacktrace library. There is not yet much information about it and it is very much possible to change but the straight forward usage is something like:
#include
#include
#include
int main()
{
std::cout << std::to_string(std::stacktrace::current()) << std::endl;
}
Ranges and Views (C++ 20 and 23)
A new interesting library is added in C++ 20 called ranges. The library cre- ates and manipulates range views, lightweight objects that indirectly represent iterable sequences (ranges). Ranges are an abstraction on top of it. with this library we can now do very interesting things. we can also use | (pipe operator) and they can be combined as well. Take this example:
#include
#include
#include
#include
#include
int main()
{
std::vector vec{1, 2, 3, 4, 5, 6};
auto v = vec | std::views::reverse | std::views::drop(2);
std::cout << *v.begin() << 'n';
}
Will print:
4
Or another example:
#include
#include
#include
#include
int main()
{
constexpr std::string_view words{"Hello^_^C++^_^20^_^!"};
constexpr std::string_view delim{"^_^"};
for (const auto word : std::views::split(words, delim))
std::cout << std::quoted(std::string_view{word.begin(), word.end()}) << ' ';
}
Will print:
"Hello" "C++" "20" "!"
Conclusion
There are many new C++ feature from which I only could mention the very select one. These new features are there to help the developers reduce their workload and the amount of boilerplate code they require to write. Some of these features like constexpr context can also be used to do infinite things and really clever compile time optimizations specially for embedded devices with limited memory and processing power.