volatile und andere kleine Verbesserungen in C++20

Modernes C++ Rainer Grimm  –  7 Kommentare

Die Tour durch die C++20-Kernsprache geht nun zu Ende. Eine interessante der kleineren Verbesserungen ist, dass die Semantik von volatile deutlich eingeschränkt wird.

Das Abstrakt im Proposal P1152R0 umreißt kurz und bündig, welche Veränderungen das Schlüsselwort volatile erfährt: "The proposed deprecation preserves the useful parts of volatile, and removes the dubious / already broken ones. This paper aims at breaking at compile-time code which is today subtly broken at runtime or through a compiler update."

Bevor ich auf die Feature von volatile eingehe, die in C++20 erhalten bleiben, möchte ich zuerst die Features vorstellen, die in C++20 verworfen werden.

  1. Zusammengesetze volatile-Zuweisungen und Pre/Post-Inkrement/Dekrement-Operationen
  2. volatile-Funktionsparameter oder Rückgabetypen
  3. volatile-Auszeichnung in Structural Binding

Wenn du an allen anspruchsvollen Details zu den Änderungen interessiert bist, kann ich sehr die Präsentation "Deprecating volatile" (CppCon 2019) von JF Bastien empfehlen. Hier sind ein paar Beispiele aus der Präsentation, die sich auf die Punkte (1) bis (3) beziehen:

(1)
int neck, tail;
volatile int brachiosaur;
brachiosaur = neck; // OK, a volatile store
tail = brachiosaur; // OK, a volatile load

// deprecated: does this access brachiosaur once or twice
tail = brachiosaur = neck;

// deprecated: does this access brachiosaur once or twice
brachiosaur += neck;

// OK, a volatile load, an addition, a volatile store
brachiosau = brachiosaur + neck;

#########################################
(2)
// deprecated: a volatile return type has no meaning
volatile struct amber jurassic();

// deprecated: volatile parameters aren't meaningful to the
// caller, volatile only applies within the function
void trex(volatile short left_arm, volatile short right_arm);

// OK, the pointer isn't volatile, the data is opints to is
void fly(volatile struct pterosaur* pterandon);

########################################
(3)
struct linhenykus { volatile short forelimb; };
void park(linhenykus alvarezsauroid) {
// deprecated: doe the binding copy the foreelimbs?
auto [what_is_this] = alvarezsauroid;
// ...
}

Damit habe ich aber noch nicht die entscheidende Frage beantwortet: Wann sollte volatile eingesetzt werden? Dazu möchte ich gerne eine Bemerkung aus dem C++-Standard zitieren: "volatile is a hint to the implementation to avoid aggressive optimization involving the object because the value of the object might be changed by means undetectable by an implementation." Also für die Ausführungseinheit (thread of execution), dass der Compiler so oft Lade- und Speicher-Operationen ausführen muss, wie sie in dem Sourcecode verwendet werden. volatile-Operationen können somit weder entfernt noch umsortiert werden. Konsequenterweise lässt sich volatile für die Kommunikation mit Signal-Handlern, aber nicht für die Kommunikation mit einer anderen Ausführungseinheit verwenden.

Um es kurz zu machen: volatile verhindert aggressive Optimierung und besitzt keine Multithreading-Semantik.

Die folgenden kleinen, aber feinen Verbesserungen stelle ich mit einem Beispiel vor, das direkt im Compiler Explorer ausgeführt werden kann.

Range-basierte For-Schleife mit Initialisierer

Mit C++20 lässt sich die Range-basierte For-Schleife direkt mit einem Initalisierer verwenden:

// rangeBasedForLoopInitializer.cpp

#include <iostream>
#include <vector>

int main() {

for (auto vec = std::vector{1, 2, 3}; auto v : vec) { // (1)
std::cout << v << " ";
}

std::cout << "\n\n";

for (auto initList = {1, 2, 3}; auto e : initList) { // (2)
e *= e;
std::cout << e << " ";
}

std::cout << "\n\n";

using namespace std::string_literals;
for (auto str = "Hello World"s; auto c: str) { // (3)
std::cout << c << " ";
}

}

Die Range-basierte For-Schleife in Zeile (1) verwendet einen std::vector, die in Zeile (2) eine std::initalizer_list und die in Zeile (3) einen std::string. Zusätzlich habe ich in den Zeilen (1) und (2) die automatische Bestimmung der Typparameter für Klassen-Templates angewandt. Das ist seit C++17 möglich. Sonst hätte ich std::vector<int> und std::initializer_list<int> anstelle von std::vector und std::initializer_list schreiben müssen.

Mit dem GCC 10.2 und dem Compiler Explorer lässt sich das Programm ausführen.

virtual constexpr-Funktionen

Eine constexpr-Funktion hat das Potenzial zur Compilezeit, kann aber auch zur Laufzeit ausgeführt werden. Konsequenterweise können mit C++20 constexpr-Funktionen als virtual deklariert werden. Alle Kombinationen sind möglich. Eine virtuelle constexpr-Funktion kann eine Nicht-constexpr-Funktion und eine virtuelle Nicht-constexpr-Funktion kann eine virtuelle constexpr.Funktion überschreiben. Ich möchte betonen, dass Überschreiben impliziert, dass die entsprechende Methode einer Basisklasse virtuell ist:

// virtualConstexpr.cpp

#include <iostream>

struct X1 {
virtual int f() const = 0;
};

struct X2: public X1 {
constexpr int f() const override { return 2; }
};

struct X3: public X2 {
int f() const override { return 3; }
};

struct X4: public X3 {
constexpr int f() const override { return 4; }
};

int main() {

X1* x1 = new X4; // (1)
std::cout << "x1->f(): " << x1->f() << std::endl;

X4 x4;
X1& x2 = x4; // (2)
std::cout << "x2.f(): " << x2.f() << std::endl;

}

Zeile (1) verwendet den virtuellen Dispatch (späte Bindung) mithilfe eines Zeigers, Zeile (2) nutzt für den virtuellen Dispatch eine Referenz. Nochmals leisten mir der GCC 10.2 und der Compiler Explorer wertvolle Hilfe.

Der neue Zeichentyp für UTF-8 Strings: char8_t

Zusätzlich zu den C++11 Zeichentypen char16_t und char32_t erhält C++20 den neuen Zeichentyp char8_t. Er ist groß genug, um UTF-8 (8 Bits) zu repräsentieren. Er besitzt dieselbe Größe, dasselbe Vorzeichen und dasselbe Alignment wie unsigned char, ist aber ein neuer Datentyp.

In bekannter Manier erhält C++20 den neuen Typalias für den Zeichentyp char8_t (1) und einen neuen UTF-8 String Literal (2):

std::u8string: std::basic_string<char8_t> (1)
u8"Hello World" (2)

Das folgende Programm zeigt die Verwendung des neuen Zeichentyps char8_t:

// char8Str.cpp

#include <iostream>
#include <string>

int main() {

const char8_t* char8Str = u8"Hello world";
std::basic_string<char8_t> char8String = u8"helloWorld";
std::u8string char8String2 = u8"helloWorld";

char8String2 += u8".";

std::cout << "char8String.size(): " << char8String.size() << std::endl;
std::cout << "char8String2.size(): " << char8String2.size() << std::endl;

char8String2.replace(0, 5, u8"Hello ");

std::cout << "char8String2.size(): " << char8String2.size() << std::endl;

}

Ohne viele Worte folgt hier bereits die Ausgabe des Programms mit dem Compiler Explorer.

using enum in lokalen Bereichen

Eine using enum-Deklaration führt die Aufzähler in der benamsten Aufzählung in den lokalen Bereich ein:

// enumUsing.cpp

#include <iostream>
#include <string_view>

enum class Color {
red,
green,
blue
};

std::string_view toString(Color col) {
switch (col) {
using enum Color; // (1)
case red: return "red"; // (2)
case green: return "green"; // (2)
case blue: return "blue"; // (2)
}
return "unknown";
}

int main() {

std::cout << std::endl;

std::cout << "toString(Color::red): " << toString(Color::red) << std::endl;

using enum Color; // (1)

std::cout << "toString(green): " << toString(green) << std::endl; // (2)

std::cout << std::endl;

}

Die using enum-Deklaration (1) führt die Aufzähler mit Gültigkeitsbereich Color in den lokalen Bereich ein. Von diesem Punkt an lassen sich die Aufzähler ohne den Gültigkeitsbereich verwenden (2). Zum jetzigen Zeitpunkt ist der Microsoft-Compiler 19.24 der einzige Compiler, der using enum unterstützt.

Default-Initialisierung der Mitglieder eines Bitfelds

Zuerst einmal, stellt sich die Frage: Was ist ein Bitfeld? In diesem Fall hilft Wikipedia: "In der Informationstechnik und Programmierung bezeichnet ein Bitfeld ein vorzeichenloses Integer, in dem einzelne Bits oder Gruppen von Bits aneinandergereiht werden. Es stellt eine Art Verbunddatentyp auf Bit-Ebene dar. Im Gegensatz dazu steht der primitive Datentyp, bei dem der Wert aus allen Stellen gemeinsam gebildet wird."

Mit C++20 lassen sich die Mitglieder eines Bitfelds mit Default-Initialisierung nutzen:

// bitField.cpp

#include <iostream>

struct Class11 { // (1)
int i = 1;
int j = 2;
int k = 3;
int l = 4;
int m = 5;
int n = 6;
};

struct BitField20 { // (2)
int i : 3 = 1;
int j : 4 = 2;
int k : 5 = 3;
int l : 6 = 4;
int m : 7 = 5;
int n : 7 = 6;
};

int main () {

std::cout << std::endl;

std::cout << "sizeof(Class11): " << sizeof(Class11) << std::endl;
std::cout << "sizeof(BitField20): " << sizeof(BitField20) << std::endl;

std::cout << std::endl;

}

Und zwar entsprechend der Mitglieder einer Klasse (1) mit C++11, so lassen sich mit C++20 die Mitglieder eines Bitfelds Default-initialisieren (2). Zum Abschluss ist hier die Ausgabe des Programms mit dem Clang 10.0 Compiler:

Eine kurze Schreibpause

In den nächsten vierzehn Tagen werde ich in Italien sein und daher keinen Artikel schreiben.

Im Falle, dass du in der Zwischenzeit einen meiner mehr als 300 Artikel zu modernem C++ lesen möchtest, habe ich eine visuelle Tour durch meinen Blog aufgenommen. Diese Sie erklärt das Inhaltsverzeichnis, die Kategorien, die Tags, das Archiv und das Suche-System. Damit sollte es möglich sein, den gewünschten Artikel zu finden:

Wie geht's weiter?

Nach der kurzen Schreibpause, setze ich meine Reise durch C++20 mit der Bibliothek fort. Insbesondere werde ich mir std::span genauer anschauen.

CppCon 2020 class "Best Practices for Modern C++"

Wer "Best Practices für modernes C++" kennen lernen möchte, dem empfehle ich meinen dreitägigen Kurs auf der CppCon 2020: Auf der Grundlage meines aktuellen Buchs über die C++ Core Guidelines, das im Dezember veröffentlicht wird, habe ich einen neuen Kurs zu den Best Practices für modernes C++ geschaffen.