Gemäß dem C++-Standard (insbesondere [over.ics.list]) versucht der Compiler bei der Listeninitialisierung, die geschweifte Initialisierungsliste mit Konstruktoren abzugleichen, die std::initializer_list<T> akzeptieren. Diese Bindung stellt eine Identitätskonversion (exakte Übereinstimmung) dar, die vor den benutzerdefinierten Konversionen rangiert, die erforderlich sind, um einzelne Elemente mit Nicht-initializer_list-Konstruktoren abzugleichen. Infolgedessen verliert ein Konstruktor wie Container(size_t count, T value) gegenüber Container(std::initializer_list<T>), wenn er mit {10, 20} aufgerufen wird, da letzterer keine Konversion für das Argument der geschweiften Initialisierungsliste erfordert, unabhängig von der elementweisen Verengung.
Wir haben eine Matrix-Klasse für eine Grafik-Engine entworfen, die sowohl einen Füllkonstruktor Matrix(size_t rows, size_t cols, double val) als auch einen aggregierten Konstruktor Matrix(std::initializer_list<std::initializer_list<double>>) für die initialisierung von literalen Tabellen bietet. Ein Junior-Entwickler schrieb Matrix m{1080, 1920, 0.0}, in der Erwartung, dass eine 1080x1920 null-initialisierte Matrix erstellt wird, jedoch erzeugte das Programm stattdessen eine 1x3-Matrix, die die drei Skalarwerte enthielt, was zu einem subtilen Laufzeitabsturz beim Rendern führte, der während der Debugging-Sitzungen schwer zu verfolgen war.
Zunächst überlegten wir, die Verwendung der Klammernsyntax Matrix(1080, 1920, 0.0) für den Füllkonstruktor vorzuschreiben, um die std::initializer_list Überlastung zu umgehen. Dies verletzte jedoch die Präferenz unseres Codierungsstandards für die einheitliche Initialisierung in C++11 und führte zu einer inkonsistenten API, bei der einige Konstruktoren Klammern erforderten, während andere geschweifte Klammern verwendeten.
Als Nächstes erforschten wir die Tag-Dispatching-Technik, indem wir einen fill_tag_t Parameter zum Füllkonstruktor hinzufügten, was die Benutzer effektiv dazu zwang, Matrix{fill_tag, 1080, 1920, 0.0} zu schreiben. Obwohl dies den Aufruf entkleidete, machte es die öffentliche Schnittstelle unordentlich und verunsicherte Entwickler, die intuitive Konstruktor-Signaturen ohne künstliche Tag-Typen erwarteten.
Drittens versuchten wir, den std::initializer_list-Konstruktor so einzuschränken, dass er nur bei geschachtelten Klammern aktiviert wird, indem wir SFINAE für den Template-Parameter verwendeten. Dieser Ansatz brach jedoch legitime Anwendungsfälle wie Matrix{{1.0, 2.0}, {3.0, 4.0}} und führte zu bruchanfälligem Template-Metaprogrammieren, das die Kompilierzeiten und die Komplexität der Fehlermeldungen erhöhte.
Letztendlich entschieden wir uns, eine statische Fabrikmethode Matrix::filled(rows, cols, val) einzuführen und machten den dreiparametrigen Füllkonstruktor privat, um die Benutzer auf eine explizite Syntax für dimensionsbasierte Konstruktionen zu lenken, während wir den std::initializer_list-Konstruktor für aggregierte Syntax öffentlich hielten. Dadurch blieb die intuitive Klammerinitialisierung für literale Tabellen erhalten, ohne das Risiko einer versehentlichen Fehlinterpretation der Dimensionsargumente einzugehen.
Die überarbeitete API verhinderte den ursprünglichen Fehler, indem Matrix{1080, 1920, 0.0} einen Kompilierfehler erzeugte, da kein passender öffentlicher Konstruktor vorhanden war. Entwickler waren nun gezwungen, entweder Matrix::filled(1080, 1920, 0.0) für Fülloperationen oder Matrix{{...}} für Initialisierungslisten zu verwenden, was die Klarheit und Sicherheit des Codes erheblich verbesserte.
Wie bewertet der Compiler die Konversionssequenz von einer geschweiften Initialisierungsliste zu einem Nicht-initializer_list-Konstruktor im Vergleich zu den Identitätsübereinstimmungen eines initializer_list-Konstruktors?
Gemäß den Überlastungsauflösungsregeln des C++-Standards für die Listeninitialisierung stellt die Bindung einer geschweiften Initialisierungsliste an einen std::initializer_list<T>-Parameter eine Identitätskonversion (exakte Übereinstimmung) mit dem höchsten Rang dar. Im Gegensatz dazu erfordert das Abgleichen derselben geschweiften Initialisierungslist mit einem anderen Konstruktor, dass der Compiler die Liste als eine in Klammern gesetzte Ausdrücke behandelt und benutzerdefinierte oder standardisierte Konversionen für jedes Element durchführt. Da Identitätskonversionen alle anderen Konversionssequenzen übersteigen, gewinnt der initializer_list-Konstruktor, selbst wenn die Elementtypen eine schlechtere logische Übereinstimmung aufweisen als die, die von einem alternativen Konstruktor verlangt werden.
Warum leitet auto x = {1, 2, 3}; in C++11 und C++14 std::initializer_list<int> ab, während auto x{1, 2, 3} in C++17 und später ungültig wird?
Vor C++17 leitete die Kopierlisteninitialisierung mit dem =-Token und auto immer std::initializer_list für geschweifte Initialisierungslisten ab. Allerdings führte C++17 neue Regeln für die direkte Listeninitialisierung mit auto (ohne =) ein, die die Standard-Template-Argumentableitung durchführen: Wenn die geschweifte Initialisierungsliste mehrere Elemente enthält, schlägt die Ableitung fehl, da auto in diesem Kontext kein std::initializer_list darstellen kann, was das Programm ungültig macht. Diese Änderung beseitigt die "geheime std::initializer_list"-Falle für die direkte Initialisierung, doch Kandidaten übersehen oft, dass die Kopiersyntax (auto x = {...}) immer noch std::initializer_list sogar im modernen C++ ableitet, was eine subtile Inkonsistenz zwischen den Initialisierungsstilen schafft.
In welchem Szenario kann eine Klasse mit sowohl einem initializer_list-Konstruktor als auch einem variadischen Template-Konstruktor mehrdeutig aufgelöst werden, und wie kann std::in_place_t diese aufklären?
Wenn eine Klasse sowohl Container(std::initializer_list<T>) als auch template<typename... Args> Container(Args&&... args) anbietet, kann der variadische Paket die gleichen Argumente wie der initializer_list-Konstruktor über die Template-Argumentableitung abgleichen. Für Container c{1, 2, 3} sind beide Konstruktoren verwendbar: der erste über die Identitätskonversion der geschweiften Initialisierungsliste und der zweite über die Ableitung von Args als int, int, int. Obwohl der Nicht-Template-initializer_list-Konstruktor normalerweise die Entscheidung gewinnt, zwingt das Hinzufügen eines Tag-Typs wie std::in_place_t zum variadischen Konstruktor (z.B. Container(std::in_place_t, Args&&... args)) die Benutzer, Container{std::in_place, 1, 2, 3} zu schreiben, was sicherstellt, dass die variadische Version nur explizit aufgerufen wird, während der initializer_list-Konstruktor standardmäßig homogene geschweifte Listen behandelt.