C++20: Designated Initializers

Modernes C++  –  105 Kommentare

Designated Initialization ist eine Erweiterung von Aggregate Initialization und ermöglicht es, die Mitglieder einer Klasse direkt mithilfe ihres Namens zu initialisieren

Desingated Initialization ist ein Spezialfall der Aggregate Initialization. Daher wird dieser Artikel mit Letzerer beginnen.

Aggregate Initialization

Zuerst gilt es zu klären, was Aggregate sind. Aggregate sind Arrays und Klassentypen. Ein Klassentyp ist eine class, struct oder union.

Mit C++20 muss ein Klassentyp die folgenden Bedingungen erfüllen:

  • keine private- oder protected-nichtstatischen Mitglieder
  • kein benutzerdefinierter oder geerbter Konstruktor
  • keine virtual-, private- oder protected-Basisklasse
  • keine virtuellen Memberfunktionen

Das nächste Programm stellt Aggregate genauer vor:

// aggregateInitialization.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}; // (1)
Point3D point3D{1, 2, 3}; // (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;

}

(1) und (2) initialisieren die Aggregate direkt durch Einsatz geschweifter Klammern. Die Reihenfolge der Initialisierer in den geschweiften Klammern muss der Deklarationsreihenfolge der Mitglieder entsprechen.

Basierend auf der Aggregat Initiaization in C++11 erhalten wir mit C++20 Designed Initializers. Bisher unterstützt nur der Microsoft-Compiler diese neuen Feature vollständig.

Designated Initializers

Dank Designated Initalizers ist es möglich, Mitglieder der Klasse direkt mithilfe ihres Namens zu initialisieren. Für eine union darf nur ein Initializer verwendet werden. Wie es bereits für Aggregate Initialization gilt, so muss die Reihenfolge der Initialisierer der Deklarationsreihenfolge der Mitglieder entsprechen:

// 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}; // (1)
Point3D point3D{.x = 1, .y = 2, .z = 3}; // (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;

}

(1) und (2) verwenden Designated Initializers zum Initialisieren der Mitglieder. Initialisierer wie .x und .y werden auch gerne Designatoren (designators) genannt.

Die Mitglieder des Aggregates können bereits einen Defaultwert besitzen. Dieser wird dann verwendet, wenn der Initialisierer nicht angegeben wird. Diese Regel gilt nicht für Unions.

// designatedInitializersDefaults.cpp

#include <iostream>

class Point3D{
public:
int x;
int y = 1;
int z = 2;
};

void needPoint(Point3D p) {
std::cout << "p: " << p.x << " " << p.y << " " << p.z << std::endl;
}

int main(){

std::cout << std::endl;

Point3D point1{.x = 0, .y = 1, .z = 2}; // (1)
std::cout << "point1: " << point1.x << " " << point1.y << " " << point1.z << std::endl;

Point3D point2; // (2)
std::cout << "point2: " << point2.x << " " << point2.y << " " << point2.z << std::endl;

Point3D point3{.x = 0, .z = 20}; // (3)
std::cout << "point3: " << point3.x << " " << point3.y << " " << point3.z << std::endl;

// Point3D point4{.z = 20, .y = 1}; ERROR // (4)

needPoint({.x = 0}); // (5)

std::cout << std::endl;

}

(1) initialisiert alle Mitglieder. Das gilt aber nicht für (2), das keinen Initialisierer für x besitzt. Konsequenterweise wird x nicht initialisiert. Es ist zulässig, wenn du nur die Mitglieder initialisierst, die wie (3) oder (5) keinen Defaultwert besitzen. Der Ausdruck (4) lässt sich nicht übersetzen, denn z und y besitzen die falsche Reihenfolge.

Designated Initialisierer erkennen Narrowing Conversion. Dies ist die Konvertierung eines Werts mit dem einhergehenden Verlust der Datengenauigkeit:

// designatedInitializerNarrowingConversion.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.5}; // (1)
Point3D point3D{.x = 1, .y = 2, .z = 3.5f}; // (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;

}

(1) und (2) verursachen einen Fehler zur Compile-Zeit, da die Initialisierung .y = 2.5 und .z = 3.5f Narrowing Conversion verursachen.

Interessanterweise verhalten sich Designated Initializers in C++ anders als in C.

Unterschiede zwischen C++ und C

C unterstützt Anwendungsfälle mit Designated Initialisers, die C++ nicht anbietet. So erlaubt C,

  • die Mitglieder des Aggregates in einer anderen Reihenfolge als der Deklarationsreihenfolge zu initialisieren.
  • die Mitglieder eines verschachtelten Aggregates zu initialisieren.
  • Designated Initializers und reguläre Initialisierer zu vermischen.
  • die Designated Initializations eines Arrays.

Das Proposal P0329R4 bietet ein selbsterklärendes Beispiel für diese Anwendungsfälle an:

struct A { int x, y; };
struct B { struct A a; };
struct A a = {.y = 1, .x = 2}; // valid C, invalid C++ (out of order)
int arr[3] = {[1] = 5}; // valid C, invalid C++ (array)
struct B b = {.a.x = 0}; // valid C, invalid C++ (nested)
struct A a = {.x = 1, 2}; // valid C, invalid C++ (mixed)

Die Begründung für den Unterschied zwischen C und C++ ist Teil des Proposals: "In C++, members are destroyed in reverse construction order and the elements of an initializer list are evaluated in lexical order, so field initializers must be specified in order. Array designators conflict with ​lambda-expression​ syntax. Nested designators are seldom used." Das Dokument argumentiert weiter, dass nur eines der ausschließlich in C unterstützten Feature gerne zum Einsatz kommt: das Initialisieren der AggregateMmitglieder in einer Reihenfolge, die nicht der Deklarationsreihenfolge entspricht.

Wie geh't weiter?

Seit C++98 besitzen wir const, mit C++11 constexpr und mit C++20 consteval und constinit. In meinem nächsten Artikel werde ich einerseits auf die neuen C++20-Spezifizier consteval und constinit genauer eingehen und andererseits deren Unterschiede zu const und constexpr analysieren.