Warum ist die Listeninitialisierung (mit geschweiften Klammern) besser als die Alternativen?

406
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

Warum?

Ich konnte auf SO keine Antwort finden, also lass mich meine eigene Frage beantworten.

Oleksiy
quelle
12
Warum nicht verwenden auto?
Mark Garcia
33
Das stimmt, es ist praktisch, aber es verringert meiner Meinung nach die Lesbarkeit - ich möchte gerne sehen, welchen Typ ein Objekt beim Lesen von Code hat. Wenn Sie zu 100% sicher sind, welcher Typ das Objekt ist, warum sollten Sie auto verwenden? Und wenn Sie die Listeninitialisierung verwenden (lesen Sie meine Antwort), können Sie sicher sein, dass sie immer korrekt ist.
Oleksiy
102
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratormöchte ein Wort mit dir.
Xeo
9
@Oleksiy Ich empfehle dieses GotW zu lesen .
Rapptz
17
@doc Ich würde sagen, using MyContainer = std::map<std::string, std::vector<std::string>>;ist noch besser (vor allem, wie Sie es Vorlage können!)
JAB

Antworten:

357

Grundsätzlich kopieren und einfügen aus Bjarne Stroustrups "The C ++ Programming Language 4th Edition" :

Die Listeninitialisierung erlaubt keine Verengung (§iso.8.5.4). Das ist:

  • Eine Ganzzahl kann nicht in eine andere Ganzzahl konvertiert werden, die ihren Wert nicht halten kann. Beispielsweise ist char to int zulässig, int to char jedoch nicht.
  • Ein Gleitkommawert kann nicht in einen anderen Gleitkommatyp konvertiert werden, der seinen Wert nicht halten kann. Zum Beispiel ist float to double erlaubt, aber nicht double to float.
  • Ein Gleitkommawert kann nicht in einen Ganzzahltyp konvertiert werden.
  • Ein ganzzahliger Wert kann nicht in einen Gleitkommatyp konvertiert werden.

Beispiel:

void fun(double val, int val2) {

    int x2 = val; // if val==7.9, x2 becomes 7 (bad)

    char c2 = val2; // if val2==1025, c2 becomes 1 (bad)

    int x3 {val}; // error: possible truncation (good)

    char c3 {val2}; // error: possible narrowing (good)

    char c4 {24}; // OK: 24 can be represented exactly as a char (good)

    char c5 {264}; // error (assuming 8-bit chars): 264 cannot be 
                   // represented as a char (good)

    int x4 {2.0}; // error: no double to int value conversion (good)

}

Die einzige Situation, in der = gegenüber {} bevorzugt wird, ist die Verwendungauto Schlüsselworts, um den vom Initialisierer bestimmten Typ abzurufen.

Beispiel:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

Fazit

Ziehen Sie die {} Initialisierung Alternativen vor, es sei denn, Sie haben einen starken Grund, dies nicht zu tun.

Oleksiy
quelle
52
Es gibt auch die Tatsache, dass using ()als Funktionsdeklaration analysiert werden kann. Es ist verwirrend und inkonsistent, dass man sagen kann, T t(x,y,z);aber nicht T t(). Und manchmal, Sie sicher x, können Sie nicht einmal sagen T t(x);.
Juanchopanza
84
Ich bin mit dieser Antwort überhaupt nicht einverstanden. Die Klammerinitialisierung wird zu einem völligen Durcheinander, wenn Sie Typen haben, bei denen ein Ctor a akzeptiert std::initializer_list. RedXIII erwähnt dieses Problem (und wischt es einfach ab), während Sie es vollständig ignorieren. A(5,4)und A{5,4}kann ganz andere Funktionen aufrufen, und dies ist eine wichtige Sache zu wissen. Es kann sogar zu Anrufen führen, die nicht intuitiv erscheinen. Wenn Sie sagen, dass Sie {}standardmäßig bevorzugen sollten, werden die Leute falsch verstehen, was los ist. Dies ist jedoch nicht deine Schuld. Ich persönlich denke, es ist eine äußerst schlecht durchdachte Funktion.
user1520427
13
@ user1520427 Deshalb gibt es den Teil "es sei denn, Sie haben einen starken Grund, dies nicht zu tun ".
Oleksiy
67
Obwohl diese Frage alt ist, hat sie einige Treffer, daher füge ich sie hier nur als Referenz hinzu (ich habe sie nirgendwo anders auf der Seite gesehen). Ab C ++ 14 mit den neuen Regeln für den automatischen Abzug von der Klammer-Init-Liste ist es nun möglich zu schreiben auto var{ 5 }und es wird intnicht mehr als abgeleitet std::initializer_list<int>.
Edoardo Sparkon Dominici
12
Haha, aus all den Kommentaren ist immer noch nicht klar, was zu tun ist. Klar ist, dass die C ++ - Spezifikation ein Chaos ist!
DrumM
113

Es gibt bereits gute Antworten auf die Vorteile der Listeninitialisierung. Meine persönliche Faustregel lautet jedoch, nach Möglichkeit KEINE geschweiften Klammern zu verwenden, sondern diese von der konzeptionellen Bedeutung abhängig zu machen:

  • Wenn das Objekt, das ich konzeptionell erstelle, die Werte enthält, die ich im Konstruktor übergebe (z. B. Container, POD-Strukturen, Atomics, Smart Pointer usw.), verwende ich die geschweiften Klammern.
  • Wenn der Konstruktor einem normalen Funktionsaufruf ähnelt (er führt einige mehr oder weniger komplexe Operationen aus, die durch die Argumente parametrisiert werden), verwende ich die normale Funktionsaufrufsyntax.
  • Für die Standardinitialisierung verwende ich immer geschweifte Klammern.
    Zum einen bin ich mir auf diese Weise immer sicher, dass das Objekt initialisiert wird, unabhängig davon, ob es sich beispielsweise um eine "echte" Klasse mit einem Standardkonstruktor handelt, der trotzdem aufgerufen wird, oder um einen eingebauten / POD-Typ. Zweitens stimmt es - in den meisten Fällen - mit der ersten Regel überein, da ein standardmäßig initialisiertes Objekt häufig ein "leeres" Objekt darstellt.

Nach meiner Erfahrung kann dieser Regelsatz viel konsistenter angewendet werden als die Verwendung von geschweiften Klammern, muss jedoch alle Ausnahmen explizit berücksichtigen, wenn sie nicht verwendet werden können oder eine andere Bedeutung haben als die "normale" Funktionsaufrufsyntax in Klammern (ruft eine andere Überlastung auf).

Es passt zB gut zu Standardbibliothekstypen wie std::vector:

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements
MikeMB
quelle
11
Stimmen Sie den meisten Ihrer Antworten voll und ganz zu. Denken Sie jedoch nicht, dass das Setzen leerer Klammern für den Vektor nur überflüssig ist? Ich meine, es ist in Ordnung, wenn Sie ein Objekt vom generischen Typ T wertinitialisieren müssen, aber was ist der Zweck, dies für nicht generischen Code zu tun?
Mikhail
8
@Mikhail: Es ist sicherlich überflüssig, aber es ist meine Gewohnheit, die Initialisierung lokaler Variablen immer explizit zu machen. Wie ich schrieb, geht es hauptsächlich um Konsistenz, also vergesse ich es nicht, wenn es darauf ankommt. Es ist sicherlich nichts, was ich in einer Codeüberprüfung oder in einem Styleguide erwähnen würde.
MikeMB
4
ziemlich sauberer Regelsatz.
Laike9m
5
Dies ist bei weitem die beste Antwort. {} ist wie Vererbung - leicht zu missbrauchen, was zu schwer verständlichem Code führt.
UKMonkey
2
@ MikeMB-Beispiel: const int &b{}<- versucht nicht, eine nicht initialisierte Referenz zu erstellen, sondern bindet sie an ein temporäres ganzzahliges Objekt. Zweites Beispiel: struct A { const int &b; A():b{} {} };<- versucht nicht (wie üblich ()), eine nicht initialisierte Referenz zu erstellen , sondern bindet sie an ein temporäres ganzzahliges Objekt und lässt sie dann baumeln. GCC -Wallwarnt auch mit nicht für das zweite Beispiel.
Johannes Schaub - Litb
91

Es gibt VIELE Gründe, die Klammerinitialisierung zu verwenden, aber Sie sollten sich bewusst sein, dass der initializer_list<>Konstruktor den anderen Konstruktoren vorgezogen wird , mit Ausnahme des Standardkonstruktors. Dies führt zu Problemen mit Konstruktoren und Vorlagen, bei denen der TTypkonstruktor entweder eine Initialisierungsliste oder ein einfacher alter Ctor sein kann.

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

Angenommen, Sie stoßen nicht auf solche Klassen, gibt es wenig Grund, die Intializer-Liste nicht zu verwenden.

Rot XIII
quelle
20
Dies ist ein sehr wichtiger Punkt in der generischen Programmierung. Verwenden Sie beim Schreiben von Vorlagen keine geschweiften Init-Listen (der Name des Standards für { ... }), es sei denn, Sie möchten die initializer_listSemantik (naja, und vielleicht zum Standardkonstruieren eines Objekts).
Xeo
82
Ich verstehe ehrlich gesagt nicht, warum die std::initializer_listRegel überhaupt existiert - sie bringt nur Verwirrung und Chaos in die Sprache. Was ist falsch daran, Foo{{a}}wenn Sie den std::initializer_listKonstruktor wollen ? Das scheint so viel einfacher zu verstehen, als std::initializer_listVorrang vor allen anderen Überlastungen zu haben.
user1520427
5
+1 für den obigen Kommentar, weil es ein echtes Durcheinander ist, denke ich !! es ist keine Logik; Foo{{a}}folgt einer Logik für mich weit mehr als die, Foo{a}die sich in die Priorität der Intializer-Liste verwandelt (Ups könnten der Benutzer denken, hm ...)
Gabriel
32
Grundsätzlich ersetzt C ++ 11 ein Durcheinander durch ein anderes Durcheinander. Oh, tut mir leid, es ersetzt es nicht - es trägt dazu bei. Wie können Sie wissen, ob Sie solchen Klassen nicht begegnen? Was ist, wenn Sie beginnen , ohne std::initializer_list<Foo> Konstruktor, aber es wird zu hinzugefügt , um die Fooan einem bestimmten Punkt Klasse seine Schnittstelle zu verlängern? Dann sind Benutzer der FooKlasse vermasselt.
Doc
10
.. was sind die "VIELEN Gründe für die Verwendung der Klammerinitialisierung"? Diese Antwort zeigt einen Grund ( initializer_list<>) auf, den es nicht wirklich qualifiziert, wer sagt , dass es bevorzugt wird, und erwähnt dann einen guten Fall, in dem es NICHT bevorzugt wird. Was vermisse ich, dass ~ 30 andere Personen (Stand 21.04.2016) hilfreich fanden?
Dwanderson
0

Es ist nur sicherer, solange Sie nicht mit -Wno-Engeing bauen, wie es Google in Chromium tut. Wenn Sie dies tun, ist es WENIGER sicher. Ohne dieses Flag werden die einzigen unsicheren Fälle jedoch von C ++ 20 behoben.

Hinweis: A) Geschweifte Klammern sind sicherer, da sie keine Verengung zulassen. B) Curly-Bracker sind weniger sicher, da sie private oder gelöschte Konstruktoren umgehen und explizit markierte Konstruktoren implizit aufrufen können.

Diese beiden kombinierten Mittel bedeuten, dass sie sicherer sind, wenn es sich um primitive Konstanten handelt, aber weniger sicher, wenn es sich um Objekte handelt (obwohl in C ++ 20 festgelegt).

Allan Jensen
quelle
Ich habe versucht, auf goldbolt.org herumzuschnüffeln, um "explizite" oder "private" Konstruktoren mit dem bereitgestellten Beispielcode zu umgehen und den einen oder anderen privat oder explizit zu machen, und wurde mit den entsprechenden Compilerfehlern belohnt. Möchten Sie das mit einem Beispielcode sichern?
Mark Storer
Dies ist die Lösung für das für C ++ 20 vorgeschlagene Problem: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen
1
Wenn Sie Ihre Antwort bearbeiten, um anzuzeigen, über welche Version (en) von C ++ Sie sprechen, würde ich gerne meine Stimme ändern.
Mark Storer