C++17: Kleinvieh macht auch Mist

Seite 3: Operatoren, Variablen, Namespaces, Exceptions

Inhaltsverzeichnis

Geht es wie im vorherigen Beispiel nur um das Aneinanderreihen einzelner Operationen für alle Elemente einer variabel langen Liste von Template-Parametern, gehen sogenannte "Fold-Expressions" noch einen Schritt weiter: Sie erlauben, alle Parameter einer variablen Liste von Template-Parametern (einem sogenannten "Parameter-Pack") hintereinander mit einem Operator zu verknüpfen.

Das obige Beispiel kann deshalb ab C++17 auch folgendermaßen aussehen:

template <typename... Args>
void print (Args&&... args) {
(std::cout << ... << args) << '\n';
}

Den Ausdruck

(std::cout << ... << args)

expandiert der Compiler zu folgendem Pseudo-Code, bei dem args1, args2 und so weiter für die Argumente von args stehen:

std::cout << args1 << args2 << args3 << ...

Den Aufruf

print(42, x, "hello");

expandiert der Compiler also folgendermaßen:

std::cout << 42 << x << "hello" << '\n';

Diesen Mechnismus können Entwickler für beliebige Operatoren verwenden. Wichtig ist aber die Klammerung um die zu expandierende Operation. Formal wird

( <init> OP ... OP <parameterpack> )

expandiert zu

<init> OP <arg1> OP <arg2> OP ...

wobei die Initialisierung wegfallen kann.

Ein Ausdruck wie (... + args) liefert also die Summe aller Werte einer variablen Liste von Parametern args.

Viele Entwickler stören sich daran, dass sie globale beziehungsweise statische Objekte nur einmal definieren dürfen und sie nur deshalb Source-Dateien benötigen, statt Klassen mit statischen Komponenten komplett im Header zu definieren. Auch wenn es einige Workarounds gibt, galt bisher die grundsätzliche Regel, dass eine Deklaration in MyClass.hpp wie folgende:

struct MyClass{
static const char* prefix;
static int counter;
};

eine Source-Datei (genau eine eigene Übersetzungseinheit) MyClass.cpp benötigt, die beispielsweise folgenden Inhalt haben kann:

const char* MyClass::prefix = "> ";
int MyClass::counter = 0;

Ab C++17 reicht die Verwendung des Schlüsselworts inline innerhalb der Header-Datei:

struct MyClass{
static inline const char* prefix = "> ";
static inline int counter = 0;
};

Damit können Entwickler in einer Header-Datei Datentypen und dazugehörige globale Objekte definieren:

class Monitor {
public:
Monitor() { ... }
void log(const std::string& msg) { ... }
};
inline Monitor globalMonitor;

Das globale Objekt wird von allen Modulen gemeinsam verwendet und bei seiner ersten Verwendung initialisiert.

Die Definition von Namespaces ist ab C++17 geschachtelt möglich. Statt

namespace A {
namespace B {
namespace C {
...
}
}
}

funktioniert nun folgende Schreibweise:

namespace A::B::C {
...
}

Ein kleines Feature sei noch erwähnt, das zwar streng genommen zur Bibliothek gehört, aber im Grunde ein Sprachmittel ist:

Die bisher vorhandene Funktion std::unexpected_exception() (ohne 's' am Ende) wurde durch std::unexpected_exceptions() (mit 's' am Ende) abgelöst. Damit können Entwickler nun endlich sauber formulieren, dass Objekte bei der Behandlung von Exceptions unterscheiden können, ob ein Stack-Unwinding sie "abräumt" oder die Behandlung einer Exception sie verwendet. Im letzteren Fall dürfen sie nämlich deutlich mehr machen (z.B. Exceptions werfen).

Der Trick ist, dass Entwickler nun ermitteln können, ob sie im Destruktor mehr Exceptions haben als im Konstruktor. Damit können sie eine Klasse, die Aufgaben im Destruktor je nach Sachverhalt erfüllen oder abbrechen soll, folgendermaßen sauber programmieren:

class C {
private:
int numInitialExceptions;
public:
C() : numInitialExceptions(std::uncaught_exceptions()) {
}
C (const C&) :
numInitialExceptions(std::uncaught_exceptions()) {
}
...
~C() {
if (numInitialExceptions
== std::uncaught_exceptions()) {
commit(); // Aufruf NICHT wegen Stack-Unwinding
}
else {
rollback(); // Aufruf wegen Stack-Unwinding
}
}
};

Beim Zerstören eines Objekts der Klasse C aufgrund eines unvorhergesehenen Ereignisses, also während der aktuelle Geltungsbereich im Rahmen des Stack-Unwinding verlassen wird, ruft der Code die Methode rollback() auf, weil im Destruktor eine Exception mehr zur Bearbeitung existiert als im Konstruktor.

Erfolgt die Verwendung eines Objekts C jedoch im Rahmen der Behandlung einer Exception (also in einer catch-Klausel), handelt es sich um ein "gewolltes" Objekt, das auch im Rahmen der Behandlung einer Exception seine Arbeit sauber zu Ende bringen kann. Da es im Rahmen von catch erzeugt und zerstört wird, sind die Anzahl der unbehandelten Exceptions im Konstruktor und Destruktor gleich. Es erfolgt somit commit().