C++20: Ăśberblick zur Kernsprache
NatĂĽrlich hat C++20 mehr als Concepts, die Ranges-Bibliothek, Coroutinen und Module zu bieten. Das zeigt ein Ăśberblick zur Kernsprache.
- Rainer Grimm
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. ()