C++20 features

C++20 is the largest release since C++11: concepts, ranges, coroutines, modules and a long list of smaller features that change the code written every day. This lesson takes five of the small ones, because each replaces a piece of boilerplate with a declaration.

The big four have their own lessons: coroutines in lesson 115, modules in lesson 131, and the memory model in lesson 109. What is left is the syntax that makes an ordinary class shorter.

The five features share a property: each replaces code that every project wrote by hand. The designated initialiser replaces a constructor, the spaceship replaces six operators, and the span replaces a pointer and a count.

The span also accepts a std::array and a C array, which is what makes it a replacement for the old pair in an interface that already had one. The type of the elements stays a template parameter.

Designated initialisers and <=>

point p{.x = 1, .y = 2} names the members it initialises, so a struct with five fields can be built without counting the positions. The members must appear in declaration order, and the ones left out are value-initialised.

The program also declares auto operator<=>(const version&) const = default, which gives all six comparisons from the member order. The print is 1 2 1 0: the coordinates, then two is greater than one, and the two versions differ.

The defaulted spaceship is the reason a small value type needs no comparison operators at all. It compares member by member in declaration order, and == is rewritten from it, so the class gets equality for free.

#include <compare> #include <iostream> struct point { int x; int y; }; struct version { int major; int minor; auto operator<=>(const version&) const = default; }; int main() { point p{.x = 1, .y = 2}; version a{2, 1}; version b{2, 0}; std::cout << p.x << ' ' << p.y << ' '; std::cout << (a > b) << ' ' << (a == b) << '\n'; }

g++ -std=c++23 -Wall -Wextra designated.cpp -o designated
./designated

1 2 1 0

std::span is a view of a range

std::span<const int> is a pointer and a length, and it accepts an array, a vector or any contiguous range without copying. A function that takes a span replaces the pointer-and-count pair of lesson 088.

The program sums a raw array and a vector through the same function and prints 6 and 9. The size of the span itself is 16, one pointer and one size_t, which is the two arguments the function would have taken anyway.

A span does not own its elements and does not check the bounds of operator[]. It is a parameter type for a function that reads a block, and the caller keeps the lifetime responsibility, as with every view.

#include <iostream> #include <span> #include <vector> int sum(std::span<const int> values) { int total = 0; for (int value : values) { total += value; } return total; } int main() { int raw[]{1, 2, 3}; std::vector<int> values{4, 5}; std::cout << sum(raw) << ' ' << sum(values) << ' '; std::cout << sizeof(std::span<const int>) << '\n'; }

g++ -std=c++23 -Wall -Wextra span.cpp -o span_demo
./span_demo

6 9 16

The bit operations

<bit> collects the operations that used to be compiler intrinsics or hand-written loops: popcount, countl_zero, rotl, bit_cast and the power-of-two helpers.

The program counts the set bits of 0b1011, the leading zeros of the same 32-bit value and a rotation by two. The answers are 3, 28 and 44.

The functions are constexpr, so the same calls work in a constant expression. bit_cast is the portable replacement for the union trick, and it is the one to reach for when a float has to be read as its bits.

#include <bit> #include <iostream> int main() { unsigned value = 0b1011u; std::cout << std::popcount(value) << ' '; std::cout << std::countl_zero(value) << ' '; std::cout << std::rotl(value, 2u) << '\n'; }

g++ -std=c++23 -Wall -Wextra bits.cpp -o bits_demo
./bits_demo

3 28 44

Designators out of order

Designated initialisers must follow the declaration order of the members. Writing them in a different order is a compile error, and the message names the field that broke the order and the struct.

The program initialises y before x and the compiler refuses. The rule is there because the initialisation order is the declaration order anyway, and a different order would suggest a guarantee the language does not give.

The fix is to write the designators in the order of the declaration. The values are the same, and the code now says what the compiler does.

struct point { int x; int y; }; int main() { point p{.y = 2, .x = 1}; }

g++ -std=c++23 -Wall -Wextra order.cpp -o order

order.cpp: In function 'int main()':
order.cpp:9:27: warning: missing initializer for member 'point::x' [-Wmissing-field-initializers]
9 | point p{.y = 2, .x = 1};
| ^
order.cpp:9:27: error: designator order for field 'point::x' does not match declaration order in 'point'
order.cpp:9:11: warning: unused variable 'p' [-Wunused-variable]
9 | point p{.y = 2, .x = 1};
| ^

Exercises

  1. Add a third member to the version struct and check that the defaulted spaceship still compares all three.
  2. Write a function that takes a std::span of doubles and returns the largest element.
  3. Use std::bit_cast to print the bits of 1.0f as an unsigned int.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does point p{.x = 1, .y = 2} require?

    Answer

    The designators follow the declaration order of the members

    The order rule is what lets the compiler map the designators to the members directly.

  2. What does a defaulted operator<=> provide?

    Answer

    All the comparison operators, member by member in declaration order

    The spaceship gives the ordering, and equality is rewritten from it.

  3. What does std::span store?

    Answer

    A pointer and a length

    The view is two words, which is why its size is 16 on this build.

  4. What does this program print? #include <compare> #include <iostream> struct point { int x; int y; }; struct version { int major; int minor; auto operator<=>(const version&) const = default; }; int main() { point p{.x = 1, .y = 2}; version a{2, 1}, b{2, 0}; std::cout << p.x << ' ' << p.y << ' ' << (a > b) << ' ' << (a == b); }

    Answer

    1 2 1 0

    The coordinates are 1 and 2, a is greater than b, and the two versions are not equal.

  5. What does this program print? #include <iostream> #include <span> #include <vector> int sum(std::span<const int> values) { int total = 0; for (int v : values) total += v; return total; } int main() { int raw[]{1, 2, 3}; std::vector<int> values{4, 5}; std::cout << sum(raw) << ' ' << sum(values) << ' ' << sizeof(std::span<const int>); }

    Answer

    6 9 16

    The two sums are 6 and 9, and the span is a pointer and a size.

  6. What does this program print? #include <bit> #include <iostream> int main() { unsigned value = 0b1011u; std::cout << std::popcount(value) << ' ' << std::countl_zero(value) << ' ' << std::rotl(value, 2u); }

    Answer

    3 28 44

    1011 has three set bits, 28 leading zeros in 32 bits, and a rotate by two gives 101100, 44.

  7. Which header declares std::popcount?

    Answer

    <bit>

    The bit operations moved into their own header in C++20.

  8. Does a span own its elements?

    Answer

    No, it is a view and the caller keeps the lifetime

    A span is a pointer and a length, so the storage belongs to whoever created the range.

  9. What does the compiler say about designators in the wrong order?

    Answer

    That the designator order does not match the declaration order

    The error names the field and the struct, and it is a hard error rather than a warning.

  10. What replaces the pointer-and-count parameter pair in C++20?

    Answer

    std::span

    A span carries both the address and the length of a contiguous range.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev