C++20: Überblick zur Kernsprache

Modernes C++  –  135 Kommentare

Mein letzter Artikel "C++20: Die vier großen Neuerungen" hat einen ersten Überblick zu Concepts, der Ranges-Bibliothek, Coroutinen und Module gegeben. Natürlich hat C++20 mehr zu bieten. Heute möchte ich meinen Überblick mit der Kernsprache fortsetzen.

Auf dem Bild sind die Features dieses Artikels dargestellt:

Der Drei-Weg-Vergleichsoperator <=>

Der Drei-Weg-Vergleichsoperator <=> wird auch gerne Spaceship Operator genannt. Er bestimmt für zwei Werte A und B, ob A < B, A = B oder A > B ist.

Der Compiler kann den Drei-Weg-Vergleichsoperator automatisch erzeugen. Dazu muss er nur freundlich mit default aufgefordert werden. In diesem Fall spendiert er alle sechs Vergleichsoperatoren: ==, !=, <, <=, > und >=.

#include <compare>

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

Der angeforderte Operator <=> vergleicht lexikografisch, indem er zuerst die Basisklassen von links nach rechts und dann die nichtstatischen Mitglieder in ihrer Deklarationsreihenfolge berücksichtigt. Hier ist ein deutlich anspruchsvolleres Programm aus dem Microsoft-Blog: "Simplify Your Code with Rocket Science: C++ 20's Spaceship Operator".

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);
}

Ich nehme an, der komplizierte Teil des Codeschnipsels ist nicht der Spaceship Operator, sondern die Initialisierung von Base mittels Aggregate-Initialisierung. Diese bedeutet im Wesentlichen, dass sich die Mitglieder eines Klassentyps (class, struct oder union) direkt initialisieren lassen, wenn diese all public sind. In diesem Fall ist eine geschweift-geklammerte Initialisierungsliste (brace-initialisation list) einsetzbar. Natürlich war dies eine Vereinfachung. Die Details lassen sich schön hier nachlesen: "Aggregate Initialisation".

String-Literale als Template-Parameter

Vor C++20 konntest du keinen String als Nicht-Typ-Template-Parameter einsetzen. Mit C++20 ist dies möglich. Dazu bietet es sich an, den im Standard definierten basic_fixed_string zu verwenden, da dieser einen constexpr-Konstruktor besitzt. Dank des Konstruktors ist es möglich, den String zur Übersetzungszeit zu instanziieren:

template<std::basic_fixed_string T>
class Foo {
static constexpr char const* Name = T;
public:
void hello() const;
};

int main() {
Foo<"Hello!"> foo;
foo.hello();
}

constexpr virtuelle Funktionen

Virtuelle Funktionen konnten nicht in konstaten Ausdrücken aufgerufen werden, da der dynamische Typ nicht bekannt. Diese Einschränkung wird mit C++20 fallen.

Designated Initialisierer

Beginnen möchte ich mit der bereits zitierten Aggregat-Initialisierung. Hier ist ein einfaches Beispiel:

// aggregateInitialisation.cpp

#include <iostream>

struct Point2D{
int x;
int y;
};

class Point3D{
public:
int x;
int y;
int z;
};

int main(){

std::cout << std::endl;

Point2D point2D {1, 2};
Point3D point3D {1, 2, 3};

std::cout << "point2D: " << point2D.x << " " << point2D.y << std::endl;
std::cout << "point3D: " << point3D.x << " " << point3D.y << " " << point3D.z << std::endl;

std::cout << std::endl;

}

Ich denke, es ist nicht notwendig, das Programm zu erklären. Hier ist bereits seine Ausgabe:

Die Initialisierung in dem Programm aggregateInitialisation.cpp ist sehr fehleranfällig, da es leicht passieren kann, dass der Aufrufer die Argumente vertauscht. Leider fällt dieser Fehler nicht so schnell auf. In diesem Fall helfen Designated Initialisierer aus C99, denn explizit ist besser als implizit:

// designatedInitializer.cpp

#include <iostream>

struct Point2D{
int x;
int y;
};

class Point3D{
public:
int x;
int y;
int z;
};

int main(){

std::cout << std::endl;

Point2D point2D {.x = 1, .y = 2};
// Point2D point2d {.y = 2, .x = 1}; // (1) error
Point3D point3D {.x = 1, .y = 2, .z = 2};
// Point3D point3D {.x = 1, .z = 2} // (2) {1, 0, 2}


std::cout << "point2D: " << point2D.x << " " << point2D.y << std::endl;
std::cout << "point3D: " << point3D.x << " " << point3D.y << " " << point3D.z << std::endl;

std::cout << std::endl;

}

Die Namen der Argumente für Point2d und Point3d besitzen explizite Namen. Die Ausgabe des Programms ist identisch zu der des Programms aggregateInitialisation.cpp. Die auskommentierte Zeilen (1) und (2) sind sehr interessant. Zeile (1) würde einen Fehler hervorrufen, da die Reihenfolge der Designatoren nicht ihrer Deklarationsreihenfolge entspricht. In Zeile (3) fehlt hingegen der Designator für y. In diesem Fall wird y auf 0 initialisiert. Dies entspricht einer geschweift-geklammerten Initialisierungsliste {1, 0, 3}.

Verbesserungen rund um Lambdas

Lambdas erfahren viele Verbesserungen in C++20. Falls du mehr Details als in diesem Artikel benötigst, lies "Bartoks Artikel zu Lambda-Verbesserung in C++17 und C++20 durch oder warte auf meinen tiefergehenden Artikel. Hier sind zwei interessante Änderungen, die wir erhalten werden.

  • Verbiete die implizite Bindung von this mit [=]
struct Lambda {
auto foo() {
return [=] { std::cout << s << std::endl; };
}

std::string s;
};

struct LambdaCpp20 {
auto foo() {
return [=, this] { std::cout << s << std::endl; };
}

std::string s;
};

Die implizite Bindung von this mit [=] in dem Lambda-Ausdruck wird in C++20 eine Deprecation-Warnung provozieren. Die Warnung gibt es nicht, wenn du explizt mit Copy [=, this] bindest.

  • Template-Lambdas

Du stellst dir sicherlich auch die Frage: Warum benötigen wir Template-Lambdas? Wenn ein generisches Lambda mit C++14 instanziiert wird, erzeugt der Compiler automatisch einen Aufrufoperator, der selbst ein Template ist:

template <typename T>
T operator(T x) const {
return x;
}

Manchmal ist es notwendig, eine Lambda-Funktion zu definieren, die nur für bestimmte Datentypen wie zum Beispiel std::vector verwendet werden kann. In diesem Fall heißt die Rettung Template-Lambdas. Statt eines Typeparameters kann auch ein Concept angewandt werden.

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

Neue Attribute [[likely]] and [[unlikely]]

Mit C++20 erhalten wir die neuen Attribute [[likely]] und [[unlikely]]. Beide Attribute erlauben es dem Optimierer, einen Hinweis zu geben, welcher Codepfad mit höherer Wahrscheinlichkeit ausgeführt wird oder nicht.

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

consteval- und consinit-Bezeichner

Der neue Bezeichner consteval erzeugt eine sogenannte Immediate-Funktion. Für sie gilt, dass jeder Aufruf einen konstanten Ausdruck erzeugt, der zur Compilezeit ausgeführt wird. Eine Immediate-Funktion ist implizit eine constexpr-Funktion.

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

int x = 100;
int r2 = sqr(x); // Error

Die letzte Zuweisung führt zu einem Fehler, da x kein konstanter Ausdruck ist. Damit kann sqr(x) nicht zur Compilezeit ausgeführt werden.

constinit gibt die Garantie, dass eine Variable mit statischer Speicherdauer (static storage duration) zur Compilezeit initialisiert wird. Statische Speicherdauer bedeutet, dass das Objekt zum Programmstart allokiert und zum Programende deallokiert wird. Globale Objekte (namespace scope) oder Objekte, die mit static oder extern deklariert sind, besitzen statische Speicherdauer.

std::source_location

C++11 besitzt die zwei Makros __LINE__ und __FILE__, um notwendige Information zur Zeilennummer und dem Dateinamen dann zu erhalten, wenn die Makros eingesetzt werden. Mit C++20 gibt die Klasse source_location den Dateinamen, die Zeilen- und die Spaltennummer und den Funktionsnamen zurück. Das einfache Beispiel von cppreference.com zeigt eine erste Anwendung.

#include <iostream>
#include <string_view>
#include <source_location>

void log(std::string_view message,
const std::source_location& location = std::source_location::current())
{
std::cout << "info:"
<< location.file_name() << ":"
<< location.line() << " "
<< message << '\n';
}

int main()
{
log("Hello world!"); // info:main.cpp:15 Hello world!
}

Wie geht's weiter?

Dieser Artikel gab den ersten Überblick der unbekannteren Feature der neuen Kernsprache. Mein nächster Artikel setzt den Überblick zu C++20 fort und widmet sich inbesondere den verbesserten Bibliotheken.