Core Language

Get an overview of the core language's changes brought in C++20.

Three-way comparison operator

The three-way comparison operator (<=>) determines for two values A and B, whether A < B, A == B, or A > B. It’s also known as the spaceship operator.

By declaring the three-way comparison operator default, the compiler will attempt to generate a consistent relational operator for the class. In this case, you get all six comparison operators: ==, !=, <, <=, >, and >=.

struct MyInt {
int value;
MyInt(int value): value{value} { }
auto operator<=>(const MyInt&) const = default;
};

The compiler-generated operator <=> performs a lexicographical comparison, starting with the base classes and taking into account all the non-static data members in their declaration order. Here is a quite sophisticated example from the Microsoft blog: Simplify Your Code with Rocket Science: C++ 20’s Spaceship Operator.

Designated initialization

Look at the code below.

struct Basics {
int i;
char c;
float f;
double d;
auto operator<=>(const Basics&) const = default;
};
struct Arrays {
int ai[1];
char ac[2];
float af[3];
double ad[2][2];
auto operator<=>(const Arrays&) const = default;
};
struct Bases : Basics, Arrays {
auto operator<=>(const Bases&) const = default;
};
int main() {
constexpr Bases a = { { 0, 'c', 1.f, 1. },
{ { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
constexpr Bases b = { { 0, 'c', 1.f, 1. },
{ { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
static_assert(a == b);
static_assert(!(a != b));
static_assert(!(a < b));
static_assert(a <= b);
static_assert(!(a > b));
static_assert(a >= b);
}

I assume the most complicated stuff in this code snippet is not the spaceship operator, but the initialization of Bases using aggregate initialization. Aggregate initialization essentially means that you can directly initialize the members of class types (class, struct, or union) if all members are public. In this case, you can use a braced initialization list, as in the example.

Before I discuss designated initialization, let me show more about aggregate initialization. Here is a straightforward example:

struct Point2D{
int x;
int y;
};
class Point3D{
public:
int x;
int y;
int z;
};
int main(){
std::cout << "\n";
Point2D point2D {1, 2};
Point3D point3D {1, 2, 3};
std::cout << "point2D: " << point2D.x << " " << point2D.y << "\n";
std::cout << "point3D: " << point3D.x << " "
<< point3D.y << " " << point3D.z << "\n";
std::cout << '\n';
}

This is the output of the program:

The aggregate initialization is quite error-prone, because you can swap the constructor arguments, and you will never notice. Explicit is better than implicit. Let’s see what that means. Take a look at how designated initializers from C99, now part of the C++ standard, kick in.

struct Point2D {
int x;
int y;
};
class Point3D {
public:
int x;
int y;
int z;
};
int main() {
Point2D point2D {.x = 1, .y = 2};
// Point2D point2d {.y = 2, .x = 1}; //Error
Point3D point3D {.x = 1, .y = 2, .z = 2};
// Point3D point3D {.x = 1, .z = 2} //{1, 0, 2}
std::cout << "point2D: " << point2D.x << " " << point2D.y << "\n";
std::cout << "point3D: " << point3D.x << " " << point3D.y << " " << point3D.z
<< "\n";
std::cout << '\n';
}

The arguments for the instances of Point2 and Point3D are explicitly named. The output of the program will be identical to the output of the previous one.

The commented out line 15 and line 17 are quite interesting. Line 15 would give an error because the order of the designators does not match the declaration order of the data members. As for line 17, the designator for y is missing. In this case, y is initialized to 00, such as when using a braced initialization list: {1, 0, 2}.

consteval and constinit

The new consteval specifier, which was added in C++20, creates an immediate function. For an immediate function, every call of the function must produce a compile-time constant expression. An immediate function is implicitly a constexpr function but not necessarily the other way around.

consteval int sqr(int n) {
return n*n;
}
constexpr int r = sqr(100); // OK
int x = 100;
int r2 = sqr(x); // Error

The final assignment gives an error because x is not a constant expression and, therefore, sqr(x) cannot be performed at compile time.

The constinit specifier ensures that a variable with static storage duration is initialized at compile time. Static storage duration means that the object is allocated when the program begins and is deallocated when the program ends. Objects declared at namespace scope (global objects) and objects declared as static or extern have static storage duration.

Template improvements

C++20 offers various improvements to programming with templates. A generic constructor is a catch-all constructor because you can invoke it with any type.

struct Implicit {
template <typename T>
Implicit(T t) {
std::cout << t << '\n';
}
};
struct Explicit {
template <typename T>
explicit Explicit(T t) {
std::cout << t << '\n';
}
};
Explicit exp1 = "implicit"; // Error
Explicit exp2{"explicit"};

The generic constructor of the class Implicit is way too generic. By putting the keyword explicit in front of the constructor, as for Explicit, the constructor becomes explicit. This means that implicit conversions are not valid anymore.

Lambda improvements

Lambdas get many improvements in C++20. They can have template parameters, can be used in unevaluated contexts, and stateless lambdas can also be default-constructed and copy-assigned. Furthermore, the compiler can now detect when you implicitly copy the this pointer, which means a significant cause of undefined behaviorAll bets are open. Your program can produce the correct result, the wrong result, crashes during run-time, or may not even compile. That behavior might change when porting to a new platform, upgrading to a new compiler or as a result of an unrelated code change. with lambdas is gone.

If you want to define a lambda that accepts only a std::vector, template parameters for lambdas enable this:

auto foo = []<typename T>(std::vector<T> const& vec) {
// do vector-specific stuff
};

New attributes

C++20 has the two new attributes [[likely]] and [[unlikely]]. Both attributes allow us to give the optimizer a hint, specifying which path of execution is more or less likely.

for(size_t i=0; i < v.size(); ++i) {
if (v[i] < 0) [[likely]] sum -= sqrt(-v[i]);
else sum += sqrt(v[i]);
}