Liebe Leserinnen, liebe Leser,
heute werde ich über ein neues Merkmal des C++17 Standards diskutieren. Wahrscheinlich ist dieses Merkmal schon ein Teil der anderen Sprachen wie Python, aber wegen der Einfügung dieses Merkmals freue ich mich so viel, dass ich über dieses Thema einen Beitrag schreiben möchte. Dieses Merkmal heißt “struktuierte Bindung” oder auf Comilersprache “Zersetzungsdeklaration” (decomposition declaration). Der erste Name ist leider nicht so beschreibend, es sei denn, man ist mit diesem Merkmal schon vertraut. Was bedeutet “struktuiert” in diesem Fall? Wird etwas auf eine struktuierte Weise gebunden, oder wird etwas zu einer Struktur gebunden? Wenn es die letztere Sache ist, was wird dann zu der Struktur gebunden? Die Compilersprache ist andererseits nicht so vernebelt, weil sie auf Anhieb eine Art von Deklaration durch eine Art von Zersetzung hinweist, und das ist genau, was hier passiert. Einfach gesagt ermöglicht dieses Merkmal den Zugriff auf öffentlichen Klassenvariablen von einfachen Datenstrukturen, ohne temporäre Variablen zu erstellen, und ohne den Zugriffsoperator zu verwenden. Hier gibt’s einige Beispiele:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ...
struct Lady
{
std::string maidenName;
std::string givenName;
int age;
};
// Pre C++17 style
void listLadiesClassic(const std::vector<Lady>& l) {
for (auto& lady : l) {
std::cout << lady.maidenName << ", " << lady.givenName
<< ": " << lady.age*0.9 << std::endl;
}
}
// C++17 style
void listLadiesNew(const std::vector<Lady>& l) {
for (auto& [mn,gn,a] : l) {
std::cout << mn << ", " << gn
<< ": " << a*0.9 << std::endl;
}
}
std::vector<Lady> ladies;
// fill vector with ladies
// ...
listLadiesClassic(ladies);
listLadiesNew(ladies);
Dieses Beispiel zeigt die alte und neue Nutzung dieser Strukturen, und der Unterschied ist sehr offensichtlich: bei der neuen Bezeichnung braucht man kein temporäres Lady-Objekt, um auf die Klassenvariablen zugreifen zu können, stattdessen ist es möglich, die Variablen durch die Nutzung von ‘auto’ zu deklarieren und auf sie zuzugreifen. Es ist wertvoll zu bemerken, dass ‘gn’ und ‘a’ verschiedene Typen (string und int) haben. Das bedeutet, dass auto in der Lage ist, verschiedene Typen gleichzeitig zu handhaben. Das hat zwei Folgen:
- Wenn die Klassenvariablen von Lady benutzt werden sollen, ist es möglich, durch die Unterlassung des Zugriffsoperators weniger zu tippen.
- Die Namen der Klassenvariablen braucht man nicht mehr kennen, um auf sie zugreifen zu können.
Die zweite Folge ist viel wichtiger, weil es durch die Verringerung von Kopplung bessere Codewiederverwendung ermöglicht. Nicht nur wird der Code durch die Nutzung von auto von Typen entkoppelt (die auch für das klassische Beispiel auch wahr ist), sondern auch wird der Code (ab C++17) von Namen auch entkoppelt.
Natürlich könnte man die Frage sofort stellen, ob dieses Merkmal nur mit Strukturen, die nichts anderes als Klassenvariablen enthalten, funktioniert. Lasst uns das untersuchen:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Gentleman
{
public:
void resetName() noexcept {surName = ""; givenName = "";}
void resetAge() noexcept {age = 0;}
std::string surName;
std::string givenName;
int age;
};
// Pre C++17 style
void listGentsClassic(const std::vector<Gentleman>& g) {
for (auto& gent : g) {
std::cout << gent.surName << ", " << gent.givenName
<< ": " << gent.age << std::endl;
}
}
// C++17 style
void listGentsNew(const std::vector<Gentleman>& g) {
for (auto& [sn,gn,a] : g) {
std::cout << sn << ", " << gn
<< ": " << a << std::endl;
}
}
std::vector<Gentleman> gents;
// fill vector with gentlemen
// ...
listGentsClassic(gents);
listGentsNew(gents);
Dieser Code ist kompilierbar, und das läuft genauso wie der letzte. Die Präsenz von Hilffunktionen stören die Zersetzungsdeklaration auch nicht, sofern die Klassenvariablen öffentlich sind. Die zweite Folge des vorigen Abschnitts wurde hier auch dargestellt, weil ungeachtet der Tatsache, dass diese Funktion mit einer Klasse mit anders genannten Variablen verwendet wurde, blieb das Innere dieser neuen Funktion gleich. Die altmodische Funktion brauchte andererseits Modifikationen. Solche Benennungsunstimmigkeiten sollten natürlich in echtem Code nicht auftreten (nicht die Funktion soll modifiziert werden, wenn das doch passiert), aber es funktioniert als ein anschauliches Beispiel sehr gut. Das ist bemerkenswert, dass Zersetzungsdeklaration funktioniert nur mit öffentlichen Klassenvariablen, andernfalls wird der Compiler klagen:
error: cannot decompose non-public member ‘Gentleman::surName’ of ‘const Gentleman’
Dieses Merkmal wird nicht nur für Programmschleifen begrenzt, sondern auch funktioniert es mit den Rückgabewerten der Struct-Typen auch:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
auto findPerfectMatch(const std::vector<Lady>& l, const std::vector<Gentleman>& g)
{
// logic for finding matches
// ...
auto perfectMatch = std::make_pair(l[x], g[y]);
return perfectMatch;
}
// ...
auto [lady,gent] = findPerfectMatch(ladies, gentlemen);
// output operators are assumed
std::cout << "Congratulations to " << lady << and << gent
<< " for finding their significant other!" << std::endl;
Ein kleines Plus war hier das Rückgabewert der Funktion, die auf auto gestellt war, und struktuierte Bindung funktionierte gleichwohl. Man könnte hier legitim die Frage stellen, ob die zu regelmäßige Verwendung von Zersetzungsdeklaration unter anderem an der Fehlersuche eine negative Auswirkung hat? Die Antwort könnte genauso wie mit anderen neuen Merkmale das Folgende sein: ein bisschen Selbstbeherrschung wird nicht schaden.
Die Lektion des Tages ist:
Zersetzungsdeklaration (anders genannt struktuierte Bindung) sollte verwendet werden, wenn es zur Verfügung steht, und sie die Übersichtlichkeit der Absicht nicht verdunkelt.
Vielen Dank fürs Lesen.