Initialisierung in modernem C ++


Es ist bekannt, dass die Initialisierungssemantik einer der komplexesten Teile von C ++ ist. Es gibt viele Arten der Initialisierung, die durch unterschiedliche Syntax beschrieben werden, und alle interagieren auf komplexe und herausfordernde Weise. C ++ 11 brachte das Konzept der "universellen Initialisierung". Leider führte sie noch komplexere Regeln ein, die wiederum in C ++ 14, C ++ 17 blockiert und in C ++ 20 erneut geändert wurden.


Unter dem Schnitt - Video und Übersetzung von Timur Doumlers Bericht von der C ++ Russia Konferenz. Timur fasst zunächst die historischen Ergebnisse der Entwicklung der Initialisierung in C ++ zusammen, gibt einen systematischen Überblick über die aktuelle Version der Initialisierungsregel, typische Probleme und Überraschungen, erklärt, wie all diese Regeln effektiv verwendet werden können, und spricht schließlich über neue Vorschläge im Standard, die eine Initialisierungssemantik ermöglichen C ++ 20 ist etwas praktischer. Weiter ist die Geschichte in seinem Namen.



Inhaltsverzeichnis




Das GIF, das Sie jetzt sehen, vermittelt die Hauptbotschaft des Berichts sehr gut. Ich habe es vor ungefähr sechs Monaten im Internet gefunden und auf meinem Twitter gepostet . In den Kommentaren zu ihr sagte jemand, dass drei weitere Arten der Initialisierung fehlen. Es begann eine Diskussion, in der ich eingeladen wurde, darüber zu berichten. Und so fing alles an.


Über die Initialisierung hat Nikolay Yossutis bereits berichtet . Sein Bericht enthielt eine Folie mit 19 verschiedenen Möglichkeiten zum Initialisieren eines int:


int i1; //undefined value int i2 = 42; //note: inits with 42 int i3(42); //inits with 42 int i4 = int(); //inits with 42 int i5{42}; //inits with 42 int i6 = {42}; //inits with 42 int i7{}; //inits with 0 int i8 = {}; //inits with 0 auto i9 = 42; //inits with 42 auto i10{42}; //C++11: std::initializer_list<int>, C++14: int auto i11 = {42}; //inits std::initializer_list<int> with 42 auto i12 = int{42}; //inits int with 42 int i13(); //declares a function int i14(7, 9); //compile-time error int i15 = (7, 9); //OK, inits int with 9 (comma operator) int i16 = int(7, 9); //compile-time error int i17(7, 9); //compile-time error auto i18 = (7, 9); //OK, inits int with 9 (comma operator) auto i19 = int(7, 9); //compile-time error 

Es scheint mir, dass dies eine einzigartige Situation für eine Programmiersprache ist. Das Initialisieren einer Variablen ist eine der einfachsten Aktionen, aber in C ++ ist dies überhaupt nicht einfach. Es ist unwahrscheinlich, dass diese Sprache einen anderen Bereich hat, in dem in den letzten Jahren ebenso viele Berichte über Abweichungen vom Standard, Korrekturen und Änderungen vorliegen würden. Die Initialisierungsregeln ändern sich von Standard zu Standard, und im Internet gibt es unzählige Beiträge zum Initialisieren in C ++. Eine systematische Überprüfung ist daher keine triviale Aufgabe.


Ich werde das Material in chronologischer Reihenfolge präsentieren: Zuerst werden wir darüber sprechen, was von C geerbt wurde, dann über C ++ 98, dann über C ++ 03, C ++ 11, C ++ 14 und C ++ 17. Wir werden häufige Fehler diskutieren und meine Empfehlungen zur ordnungsgemäßen Initialisierung geben. Ich werde auch über Innovationen in C ++ 20 sprechen. Eine Übersichtstabelle wird ganz am Ende des Berichts angezeigt.



Standardinitialisierung (C)


In C ++ werden viele Dinge von C geerbt, deshalb werden wir damit beginnen. Es gibt verschiedene Möglichkeiten, Variablen in C zu initialisieren. Sie werden möglicherweise überhaupt nicht initialisiert. Dies wird als Standardinitialisierung bezeichnet . Meiner Meinung nach ist dies ein unglücklicher Name. Tatsache ist, dass keiner Variablen ein Standardwert zugewiesen wird, sondern einfach nicht initialisiert wird. Wenn Sie sich in C ++ und C einer nicht initialisierten Variablen zuwenden, erhalten Sie ein undefiniertes Verhalten:


 int main() { int i; return i; // undefined behaviour } 

Gleiches gilt für benutzerdefinierte Typen: Wenn in einer struct nicht initialisierte Felder vorhanden sind, tritt beim Zugriff auf diese Felder auch undefiniertes Verhalten auf:


 struct Widget { int i; int j; }; int main() { Widget widget; return widget.i; //   } 

C ++ wurden viele neue Konstrukte hinzugefügt: Klassen, Konstruktoren, öffentliche, private Methoden, aber nichts davon beeinflusst das gerade beschriebene Verhalten. Wenn ein Element in der Klasse nicht initialisiert ist, tritt beim Zugriff darauf ein undefiniertes Verhalten auf:


 class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); // Undefined behaviour! } 

Es gibt keine magische Möglichkeit, ein Klassenelement in C ++ standardmäßig zu initialisieren. Dies ist ein interessanter Punkt, und in den ersten Jahren meiner Karriere bei C ++ wusste ich das nicht. Weder der Compiler noch die IDE, die ich damals benutzte, erinnerten mich in irgendeiner Weise daran. Meine Kollegen haben diese Funktion bei der Überprüfung des Codes nicht berücksichtigt. Ich bin mir ziemlich sicher, dass mein Code in diesen Jahren einige ziemlich seltsame Fehler enthält. Mir schien klar, dass Klassen ihre Variablen initialisieren sollten.


In C ++ 98 können Sie Variablen mithilfe der Member-Initialisiererliste initialisieren. Eine solche Lösung des Problems ist jedoch nicht optimal, da sie in jedem Konstruktor durchgeführt werden muss, und dies ist leicht zu vergessen. Darüber hinaus erfolgt die Initialisierung in der Reihenfolge, in der die Variablen deklariert sind, und nicht in der Reihenfolge der Mitgliederinitialisierungsliste:


 // C++98: member initialiser list class Widget { public: Widget() : i(0), j(0) {} // member initialiser list int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); } 

In C ++ 11 wurden direkte Elementinitialisierer hinzugefügt, die viel bequemer zu verwenden sind. Mit ihnen können Sie alle Variablen gleichzeitig initialisieren. Dies gibt Ihnen die Sicherheit, dass alle Elemente initialisiert werden:


 // C++11: default member initialisers class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i = 0; // default member initialisers int j = 0; }; int main() { Widget widget; return widget.get_i(); } 

Meine erste Empfehlung: Verwenden Sie immer DMI (Direct Member Initialisierer), wann immer Sie können. Sie können sowohl mit integrierten Typen ( float und int ) als auch mit Objekten verwendet werden. Die Gewohnheit, Elemente zu initialisieren, lässt uns dieses Problem bewusster angehen.



Kopierinitialisierung (C)


Die erste von C geerbte Initialisierungsmethode ist also standardmäßig die Initialisierung und sollte nicht verwendet werden. Der zweite Weg ist die Kopierinitialisierung . In diesem Fall geben wir die Variable und durch das Gleichheitszeichen ihren Wert an:


 // copy initialization int main() { int i = 2; } 

Die Kopierinitialisierung wird auch verwendet, wenn ein Argument wertmäßig an eine Funktion übergeben wird oder wenn ein Objekt wertmäßig von einer Funktion zurückgegeben wird:


 // copy initialization int square(int i) { return i * i; } 

Ein Gleichheitszeichen kann den Eindruck erwecken, dass ein Wert zugewiesen wird, dies ist jedoch nicht der Fall. Die Kopierinitialisierung ist keine Wertzuweisung. In diesem Bericht wird nichts über die Aneignung gesagt.


Eine weitere wichtige Eigenschaft der Kopierinitialisierung: Wenn die Wertetypen nicht übereinstimmen, wird eine Konvertierungssequenz ausgeführt. Eine Konvertierungssequenz hat bestimmte Regeln, z. B. ruft sie keine expliziten Konstruktoren auf, da sie keine Konstruktoren transformieren. Wenn Sie eine Kopierinitialisierung für ein Objekt durchführen, dessen Konstruktor als explizit markiert ist, tritt daher ein Kompilierungsfehler auf:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; // ERROR 

Wenn es einen anderen Konstruktor gibt, der nicht explizit ist, dessen Typ jedoch schlechter ist, wird er von der Kopierinitialisierung aufgerufen, wobei der explizite Konstruktor ignoriert wird:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) 


Aggregierte Initialisierung (C)


Die dritte Art der Initialisierung, über die ich sprechen möchte, ist die aggregierte Initialisierung . Es wird ausgeführt, wenn das Array mit einer Reihe von Werten in geschweiften Klammern initialisiert wird:


 int i[4] = {0, 1, 2, 3}; 

Wenn Sie die Größe des Arrays nicht angeben, wird es aus der Anzahl der in Klammern eingeschlossenen Werte abgeleitet:


 int j[] = {0, 1, 2, 3}; // array size deduction 

Dieselbe Initialisierung wird für Aggregatklassen verwendet, d. H. Klassen, die nur eine Sammlung öffentlicher Elemente sind (die Definition von Aggregatklassen enthält einige weitere Regeln, auf die wir jetzt jedoch nicht näher eingehen werden):


 struct Widget { int i; float j; }; Widget widget = {1, 3.14159}; 

Diese Syntax funktionierte sogar in C und C ++ 98, und ab C ++ 11 können Sie das Gleichheitszeichen darin überspringen:


 Widget widget{1, 3.14159}; 

Die Aggregatinitialisierung verwendet tatsächlich die Kopierinitialisierung für jedes Element. Wenn Sie daher versuchen, die aggregierte Initialisierung (sowohl mit als auch ohne Gleichheitszeichen) für mehrere Objekte mit expliziten Konstruktoren zu verwenden, wird für jedes Objekt eine Kopierinitialisierung durchgeführt, und es tritt ein Kompilierungsfehler auf:


 struct Widget { explicit Widget(int) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; // ERROR Thingy thingy {3, 4}; // ERROR } 

Und wenn es für diese Objekte einen anderen Konstruktor gibt, der nicht explizit ist, wird er aufgerufen, auch wenn er für die Eingabe schlechter geeignet ist:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; //  Widget(double) Thingy thingy {3, 4}; //  Widget(double) } 

Betrachten wir eine weitere Eigenschaft der aggregierten Initialisierung. Frage: Welchen Wert gibt dieses Programm zurück?


 struct Widget { int i; int j; }; int main() { Widget widget = {1}; return widget.j; } 

Versteckter Text

Das stimmt, Null. Wenn Sie während der Aggregatinitialisierung einige Elemente in einem Array von Werten überspringen, werden die entsprechenden Variablen auf Null gesetzt. Dies ist eine sehr nützliche Eigenschaft, da es dank dieser Eigenschaft niemals nicht initialisierte Elemente geben kann. Es funktioniert mit Aggregatklassen und mit Arrays:


 //     int[100] = {}; 

Eine weitere wichtige Eigenschaft der Aggregatinitialisierung ist das Weglassen von Klammern (Klammerelision). Welchen Wert gibt dieses Programm Ihrer Meinung nach zurück? Es hat ein Widget , das aus zwei int Werten besteht, und Thingy , ein Aggregat aus Widget und int . Was erhalten wir, wenn wir zwei Initialisierungswerte übergeben: {1, 2} ?


 struct Widget { int i; int j; }; struct Thingy { Widget w; int k; }; int main() { Thingy t = {1, 2}; return tk; //   ? } 

Versteckter Text

Die Antwort ist Null. Hier handelt es sich um ein Unteraggregat, dh um eine verschachtelte Aggregatklasse. Solche Klassen können mit verschachtelten Klammern initialisiert werden. Sie können jedoch eines dieser Klammerpaare überspringen. In diesem Fall wird eine rekursive Durchquerung des Unteraggregats durchgeführt, und {1, 2} stellt sich als äquivalent zu {{1, 2}, 0} . Zugegeben, diese Eigenschaft ist nicht ganz offensichtlich.



Statische Initialisierung (C)


Schließlich wird die statische Initialisierung auch von C geerbt: Statische Variablen werden immer initialisiert. Dies kann auf verschiedene Arten erfolgen. Eine statische Variable kann mit einem konstanten Ausdruck initialisiert werden. In diesem Fall erfolgt die Initialisierung zur Kompilierungszeit. Wenn Sie der Variablen keinen Wert zuweisen, wird sie auf Null initialisiert:


 static int i = 3; //   statit int j; //   int main() { return i + j; } 

Dieses Programm gibt 3 zurück, obwohl j nicht initialisiert ist. Wenn die Variable nicht durch eine Konstante, sondern durch ein Objekt initialisiert wird, können Probleme auftreten.


Hier ist ein Beispiel aus einer realen Bibliothek, an der ich gearbeitet habe:


 static Colour red = {255, 0, 0}; 

Es gab eine Farbklasse, und die Primärfarben (rot, grün, blau) wurden als statische Objekte definiert. Dies ist eine gültige Aktion, aber sobald ein anderes statisches Objekt in dem Initialisierer erscheint, dessen red verwendet wird, tritt Unsicherheit auf, da es keine starre Reihenfolge gibt, in der die Variablen initialisiert werden. Ihre Anwendung kann auf eine nicht initialisierte Variable zugreifen und stürzt dann ab. Glücklicherweise wurde es in C ++ 11 möglich, den Konstruktor constexpr verwenden, und dann haben wir es mit ständiger Initialisierung zu tun. In diesem Fall gibt es keine Probleme mit der Initialisierungsreihenfolge.


Daher werden vier Arten der Initialisierung von der C-Sprache geerbt: Standardinitialisierung, Kopieren, Aggregieren und statische Initialisierung.



Direkte Initialisierung (C ++ 98)


Fahren wir mit C ++ 98 fort. Das vielleicht wichtigste Merkmal, das C ++ von C unterscheidet, sind die Konstruktoren. Hier ist ein Beispiel für einen Konstruktoraufruf:


 Widget widget(1, 2); int(3); 

Mit derselben Syntax können Sie integrierte Typen wie int und float initialisieren. Diese Syntax wird als direkte Initialisierung bezeichnet . Es wird immer ausgeführt, wenn wir ein Argument in Klammern haben.


Bei integrierten Typen ( int , bool , float ) gibt es hier keinen Unterschied zur Kopierinitialisierung. Wenn es sich um Benutzertypen handelt, können Sie im Gegensatz zur Kopierinitialisierung bei der direkten Initialisierung mehrere Argumente übergeben. Aus diesem Grund wurde die direkte Initialisierung erfunden.


Außerdem führt eine direkte Initialisierung keine Konvertierungssequenz aus. Stattdessen wird der Konstruktor mit der Überlastungsauflösung aufgerufen. Die direkte Initialisierung hat dieselbe Syntax wie ein Funktionsaufruf und verwendet dieselbe Logik wie andere C ++ - Funktionen.


In der Situation mit einem expliziten Konstruktor funktioniert die direkte Initialisierung daher einwandfrei, obwohl die Kopierinitialisierung einen Fehler auslöst:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; //  Widget w2(1); //    

In einer Situation mit zwei Konstruktoren, von denen einer explizit ist und der zweite weniger geeignet ist, wird der erste mit direkter Initialisierung und der zweite mit der Kopie aufgerufen. In dieser Situation führt das Ändern der Syntax zu einem Aufruf eines anderen Konstruktors - dies wird häufig vergessen:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) Widget w2(1); //  Widget(int) 

Die direkte Initialisierung wird immer verwendet, wenn Klammern verwendet werden, einschließlich der Konstruktoraufrufnotation zum Initialisieren eines temporären Objekts sowie in new Ausdrücken mit einem Initialisierer in Klammern und in cast :


 useWidget(Widget(1, 2)); //   auto* widget_ptr = new Widget(2, 3); // new-expression with (args) static_cast<Widget>(thingy); // cast 

Diese Syntax existiert, solange C ++ selbst existiert, und sie hat einen wichtigen Fehler, den Nikolai in seiner Grundsatzrede erwähnt hat: die ärgerlichste Analyse . Dies bedeutet, dass alles, was der Compiler als Deklaration (Deklaration) lesen kann, genau als Deklaration gelesen wird.


Thingy Sie sich ein Beispiel vor, in dem es eine Widget Klasse und eine Thingy Klasse sowie einen Thingy Konstruktor gibt, der ein Widget empfängt:


 struct Widget {}; struct Thingy { Thingy(Widget) {} }; int main () { Thingy thingy(Widget()); } 

Auf den ersten Blick scheint es, dass bei der Initialisierung von Thingy das erstellte Standard- Widget an dieses übergeben wird, aber tatsächlich wird die Funktion hier deklariert. Dieser Code deklariert eine Funktion, die eine andere Funktion als Eingabe empfängt, die nichts als Eingabe empfängt und ein Widget zurückgibt, und die erste Funktion gibt Thingy . Der Code wird fehlerfrei kompiliert, aber es ist unwahrscheinlich, dass wir nach einem solchen Verhalten gesucht haben.



Wertinitialisierung (C ++ 03)


Fahren wir mit der nächsten Version fort - C ++ 03. Es ist allgemein anerkannt, dass es in dieser Version keine wesentlichen Änderungen gab, dies ist jedoch nicht der Fall. In C ++ 03 wurde eine Wertinitialisierung angezeigt, in die leere Klammern geschrieben sind:


 int main() { return int(); // UB  C++98, 0   C++03 } 

In C ++ 98 tritt hier ein undefiniertes Verhalten auf, da die Initialisierung standardmäßig erfolgt und dieses Programm ab C ++ 03 Null zurückgibt.


Die Regel lautet: Wenn es einen benutzerdefinierten Standardkonstruktor gibt, ruft die Initialisierung mit einem Wert diesen Konstruktor auf, andernfalls wird Null zurückgegeben.


Lassen Sie uns die Situation mit dem benutzerdefinierten Konstruktor genauer betrachten:


 struct Widget { int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; } 

In diesem Programm initialisiert die Funktion den Wert für das neue Widget und gibt ihn zurück. Wir rufen diese Funktion auf und greifen auf das Element i des Widget Objekts zu. Seit C ++ 03 ist der Rückgabewert hier Null, da es keinen benutzerdefinierten Standardkonstruktor gibt. Und wenn ein solcher Konstruktor existiert, aber i nicht initialisiert, erhalten wir undefiniertes Verhalten:


 struct Widget { Widget() {} //   int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //   ,  UB } 

Es ist erwähnenswert, dass "benutzerdefiniert" nicht "benutzerdefiniert" bedeutet. Dies bedeutet, dass der Benutzer den Körper des Konstruktors bereitstellen muss, d. H. Geschweifte Klammern. Wenn Sie im obigen Beispiel den Konstruktorkörper durch = default ersetzen (diese Funktion wurde in C ++ 11 hinzugefügt), ändert sich die Bedeutung des Programms. Jetzt haben wir einen Konstruktor, der vom Benutzer definiert (benutzerdefiniert), aber nicht vom Benutzer bereitgestellt (vom Benutzer bereitgestellt) wird, sodass das Programm Null zurückgibt:


 struct Widget { Widget() = default; // user-defined,   user-provided int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //  0 } 

Versuchen wir nun Widget() = default aus der Klasse zu verschieben. Die Bedeutung des Programms hat sich erneut geändert: Widget() = default wird als vom Benutzer bereitgestellter Konstruktor betrachtet, wenn er sich außerhalb der Klasse befindet. Das Programm gibt wieder undefiniertes Verhalten zurück.


 struct Widget { Widget(); int i; }; Widget::Widget() = default; //  ,  user-provided Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //    , UB } 

Es gibt eine bestimmte Logik: Ein außerhalb einer Klasse definierter Konstruktor kann sich innerhalb einer anderen Übersetzungseinheit befinden. Der Compiler sieht diesen Konstruktor möglicherweise nicht wie in einer anderen .cpp Datei. Daher kann der Compiler keine Schlussfolgerungen über einen solchen Konstruktor ziehen und einen Konstruktor mit einem Body nicht von einem Konstruktor mit = default .



Universelle Initialisierung (C ++ 11)


Es gab viele sehr wichtige Änderungen in C ++ 11. Insbesondere wurde eine universelle (einheitliche) Initialisierung eingeführt, die ich lieber als "Einhorninitialisierung" bezeichne, weil sie einfach magisch ist. Mal sehen, warum sie aufgetaucht ist.


Wie Sie bereits bemerkt haben, gibt es in C ++ viele verschiedene Initialisierungssyntaxen mit unterschiedlichem Verhalten. Die ärgerliche Analyse in Klammern verursachte viele Unannehmlichkeiten. Den Entwicklern gefiel auch nicht, dass die aggregierte Initialisierung nur mit Arrays verwendet werden konnte, nicht jedoch mit Containern wie std::vector . Stattdessen mussten Sie .reserve und .push_back ausführen oder alle möglichen gruseligen Bibliotheken verwenden:


 //    ,  : std::vector<int> vec = {0, 1, 2, 3, 4}; //   : std::vector<int> vec; vec.reserve(5); vec.push_back(0); vec.push_back(1); vec.push_back(2); vec.push_back(3); vec.push_back(4); 

Die Schöpfer der Sprache versuchten, all diese Probleme zu lösen, indem sie die Syntax mit geschweiften Klammern einführten, jedoch ohne Gleichheitszeichen. Es wurde angenommen, dass dies eine einzige Syntax für alle Typen ist, in denen geschweifte Klammern verwendet werden und es kein störendes Analyseproblem gibt. In den meisten Fällen erledigt diese Syntax ihre Aufgabe.


Diese neue Initialisierung wird als Listeninitialisierung bezeichnet und ist in zwei Typen erhältlich: Direkt und Kopieren. Im ersten Fall werden nur geschweifte Klammern verwendet, im zweiten Fall geschweifte Klammern mit Gleichheitszeichen:


 // direct-list-initialization Widget widget{1, 2}; // copy-list-initialization Widget widget = {1, 2}; 

Die zur Initialisierung verwendete Liste heißt Braced-Init-List . Es ist wichtig, dass diese Liste kein Objekt ist und keinen Typ hat. Der Wechsel von früheren Versionen zu C ++ 11 verursacht keine Probleme mit Aggregattypen, daher ist diese Änderung nicht kritisch. Aber jetzt hat die Liste in geschweiften Klammern neue Funktionen. Obwohl es keinen Typ hat, kann es versteckt in std::initializer_list konvertiert werden, es ist so ein spezieller neuer Typ. Und wenn es einen Konstruktor gibt, der std::initializer_list als Eingabe akzeptiert, heißt dieser Konstruktor:


 template <typename T> class vector { //... vector(std::initializer_list<T> init); //   initializer_list }; std::vector<int> vec{0, 1, 2, 3, 4}; //  ^  

Es scheint mir, dass std::initializer_list von der Seite des C ++ - Komitees nicht die erfolgreichste Lösung war. Von ihm mehr Schaden als Nutzen.


Zunächst ist std::initializer_list ein Vektor fester Größe mit const Elementen. Das heißt, es ist ein Typ, es hat begin und Endfunktionen, die Iteratoren zurückgeben, es hat einen eigenen Iteratortyp, und um ihn zu verwenden, müssen Sie einen speziellen Header einfügen. Da die Elemente std::initializer_list const , kann sie nicht verschoben werden. Wenn also T im obigen Code vom Typ move-only ist, wird der Code nicht ausgeführt.


Als nächstes ist std::initializer_list ein Objekt. Damit erstellen und übertragen wir tatsächlich Objekte. In der Regel kann der Compiler dies optimieren, aber aus semantischer Sicht beschäftigen wir uns immer noch mit unnötigen Objekten.


Vor einigen Monaten gab es auf Twitter eine Umfrage: Wenn Sie in die Vergangenheit reisen und etwas aus C ++ entfernen könnten, was würden Sie entfernen? Vor allem Stimmen haben genau initializer_list .


https://twitter.com/shafikyaghmour/status/1058031143935561728


, initializer_list . , .


, . , initializer_list , . :


 std::vector<int> v(3, 0); //   0, 0, 0 std::vector<int> v{3, 0}; //   3, 0 

vector int , , , — . . , initializer_list , 3 0.


:


 std::string s(48, 'a'); // "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" std::string s{48, 'a'}; // "0a" 

48 «», «0». , string initializer_list . 48 , . ASCII 48 — «0». , , , int char . . , , .


. , ? ?


 template <typename T, size_t N> auto test() { return std::vector<T>{N}; } int main () { return test<std::string, 3>().size(); } 

Versteckter Text

, — 3. string int , 1, std::vector<std::int> initializer_list . initializer_list , . string int float , , . , . , emplace , . , {} .


, .



.
— ( {a} )
( = {a} );
:


  1. «» , std::initializer_list .
    — .
  2. ,
    () .

.


1: = {a} , a ,
.


2: , {} .
, initializer_list .
Widget<int> widget{}\ ?


 template Typename<T> struct Widget { Widget(); Widget(std::initializer_list<T>); }; int main() { Widget<int> widget{}; //    ? } 

, , initializer_list , initializer_list . . , , initializer_list . , . , .


{} . , -, , Widget() = default Widget() {} — .


Widget() = default :


 struct Widget { Widget() = default; int i; }; int main() { Widget widget{}; //   (),   vexing parse return widget.i; //  0 } 

Widget() {} :


 struct Widget { Widget() {}; // user-provided  int i; }; int main() { Widget widget{}; //  ,    return widget.i; //  ,  UB } 

: , (narrowing conversions). int double , , :


 int main() { int i{2.0}; // ! } 

, double . C++11, , . :


 struct Widget { int i; int j; }; int main() { Widget widget = {1.0, 0.0}; //   ++11    C++98/03 } 

, , , , (brace elision). , , . , map . map , — :


 std::map<std::string, std::int> my_map {{"abc", 0}, {"def", 1}}; 

, . :


 std::vector<std::string> v1 {"abc", "def"}; // OK std::vector<std::string> v2 {{"abc", "def"}}; // ?? 

, , initializer_list . initializer_list , , , . , . , .


initializer_listinitializer_list , . , const char* . , string , char . . , , .


:


  • ;
  • .

. braced-init-list . :


 Widget<int> f1() { return {3, 0}; // copy-list    } void f2(Widget); f2({3, 0}); // copy-list   

, , braced-init-list . braced-init-list , .


, . StackOverflow , . , . , , :


 #include <iostream> struct A { A() {} A(const A&) {} }; struct B { B(const A&) {} }; void f(const A&) { std::cout << "A" << std::endl; } void f(const B&) { std::cout << "B" << std::endl; } int main() { A a; f( {a} ); // A f( {{a}} ); // ambiguous f( {{{a}}} ); // B f({{{{a}}}}); // no matching function } 


++14


, C++11 . , , . C++14. , .


, ++11 direct member initializers, . , direct member initializers . ++14, direct member initializers:


 struct Widget { int i = 0; int j = 0; }; Widget widget{1, 2}; //    C++14 

, auto . ++11 auto braced-init-list, std::initializer_list :


 int i = 3; // int int i(3); // int int i{3}; // int int i = {3}; // int auto i = 3; // int auto i(3); // int auto i{3}; //  ++11 — std::initializer_list<int> auto i = {3}; //  ++11 — std::initializer_list<int> 

: auto i{3} , int , std::initializer_list<int> . ++14 , auto i{3} int . , . , auto i = {3} std::initializer_list<int> . , : int , — initializer_list .


 auto i = 3; // int auto i(3); // int auto i{3}; //  ++14 — int,         auto i = {3}; //    std::initializer_list<int> 

, C++14 , , , , . , .


, ++14 :


  • , , std::initializer_list .


  • std::initializer_list move-only .


  • c , emplace make_unique .


  • , :


    • , -;
    • ;
    • auto .

  • , , .



: assert(Widget(2,3)) , assert(Widget{2,3}) . , , , . , . .



C++


, ++.


int , . . — , .


: , , std::initializer_list , direct member initializers. , .


, é . .


 struct Point { int x = 0; int y = 0; }; setPosition(Point{2, 3}); takeWidget(Widget{}); 

braced-init-list — .


 setPosition({2, 3}); takeWidget({}); 

, , . , — , . , , , , , . , , initializer_list . : , , .


:


  • = value


  • = {args} = {} :


    • std::initializer_list
    • direct member initialisation ( (args) )

  • {args} {} é


  • (args)



, (args) vexing parse. . 2013 , , auto . , : auto i; — . , :


 auto widget = Widget(2, 3); 

, . , , vexing parse:


 auto thingy = Thingy(); 

« auto» («almost always auto», AAA), ++11 ++14 , , , std::atomic<int> :


 auto count = std::atomic<int>(0); // C++11/14:  // std::atomic is neither copyable nor movable 

, atomic . , , , , . ++17 , , (guaranteed copy elision):


 auto count = std::atomic<int>(0); // C++17: OK, guaranteed copy elision 

auto . — direct member initializers. auto .


++17 CTAD (class template argument deduction). , . . , CppCon, CTAD , . , ++17 , ++11 ++14, , . , , , , .



(++20)


++20, . , , : (designated initialization):


 struct Widget { int a; int b; int c; }; int main() { Widget widget{.a = 3, .c = 7}; }; 

, . , , . , . , b .


, , , . , .


, , 99, :


  • , , . ++ , , . ::


     Widget widget{.c = 7, .a = 3}; //  

    , .


  • ++ , {.ce = 7}; , {.c{.e = 7}} :


     Widget widget{.ce = 7}; //  

  • ++ , , :


     Widget widget{.a = 3, 7}; //  

  • ++ . , -, , .


     int arr[3]{.[1] = 7}; //  



C++20


++20 , . ( wg21.link/p1008 ).


++17 , , . , , , :


 struct Widget { Widget() = delete; int i; int j; }; Widget widget1; //  Widget widget2{}; //   C++17,     C++20 

, , . ++20 . , . , . , , , .


( wg21.link/p1009 ). Braced-init-list new , : , ? — , : braced-init-list new :


 double a[]{1, 2, 3}; // OK double* p = new double[]{1, 2, 3}; //   C++17,   C++20 

, ++11 braced-init-list. ++ . , .



(C++20)


, ++20 . , . ++20 : ( wg21.link/p0960 ).


 struct Widget { int i; int j; }; Widget widget(1, 2); //   C++20 

. , emplace make_unique . . : auto , : 58.11 .


 struct Widget { int i; int j; }; auto widget = Widget(1, 2); 

, :


 int arr[3](0, 1, 2); 

, : uniform 2.0. . , , , , . — initializer_list : , , — . , . , - , — . .


, . direct member initializers. auto . direct member initializers — , . , . — , .


, , . — , — . , .



, , C++ Russia 2019 Piter «Type punning in modern C++» . , ++20, , , «» ++ , .

Source: https://habr.com/ru/post/de469465/


All Articles