Wann sollte der in Klammern eingeschlossene Initialisierer verwendet werden?

94

In C ++ 11 haben wir diese neue Syntax zum Initialisieren von Klassen, die uns eine Vielzahl von Möglichkeiten zum Initialisieren von Variablen bietet.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Für jede Variable, die ich deklariere, muss ich mir überlegen, welche Initialisierungssyntax ich verwenden soll, und dies verlangsamt meine Codierungsgeschwindigkeit. Ich bin sicher, das war nicht die Absicht, die geschweiften Klammern einzuführen.

Wenn es um Vorlagencode geht, kann das Ändern der Syntax zu unterschiedlichen Bedeutungen führen. Daher ist es wichtig, den richtigen Weg einzuschlagen.

Ich frage mich, ob es eine universelle Richtlinie gibt, welche Syntax man wählen sollte.

Helami
quelle
1
Ein Beispiel für unbeabsichtigtes Verhalten bei der {} Initialisierung: Zeichenfolge (50, 'x') gegen Zeichenfolge {50, 'x'} hier
P i

Antworten:

64

Ich denke, das Folgende könnte eine gute Richtlinie sein:

  • Wenn der (einzelne) Wert, mit dem Sie initialisieren, der genaue Wert des Objekts sein soll, verwenden Sie die Initialisierung copy ( =) (da Sie dann im Fehlerfall niemals versehentlich einen expliziten Konstruktor aufrufen, der den angegebenen Wert im Allgemeinen interpretiert anders). Überprüfen Sie an Stellen, an denen keine Kopierinitialisierung verfügbar ist, ob die Klammerinitialisierung die richtige Semantik aufweist, und verwenden Sie diese, falls dies der Fall ist. Andernfalls verwenden Sie die Klammerinitialisierung (wenn diese ebenfalls nicht verfügbar ist, haben Sie ohnehin kein Glück).

  • Wenn die Werte, mit denen Sie initialisieren, eine Liste von Werten sind, die im Objekt gespeichert werden sollen (wie die Elemente eines Vektors / Arrays oder des Real- / Imaginärteils einer komplexen Zahl), verwenden Sie die Initialisierung geschweifter Klammern, falls verfügbar.

  • Wenn die Werte Sie initialisieren nicht Werte gespeichert werden, sondern beschreiben den beabsichtigten Wert / Zustand des Objekts, verwenden Sie Klammern. Beispiele sind das Größenargument eines vectoroder das Dateinamenargument eines fstream.

Celtschk
quelle
4
@ user1304032: Ein Gebietsschema ist keine Zeichenfolge, daher würden Sie keine Kopierinitialisierung verwenden. Ein Gebietsschema enthält auch keine Zeichenfolge (diese Zeichenfolge wird möglicherweise als Implementierungsdetail gespeichert, dies ist jedoch nicht der Zweck). Daher würden Sie keine Klammerinitialisierung verwenden. Daher heißt es in der Richtlinie, die Initialisierung in Klammern zu verwenden.
Celtschk
2
Ich persönlich mochte diese Richtlinie am meisten und sie funktioniert auch gut mit generischem Code. Es gibt einige Ausnahmen ( T {}oder syntaktische Gründe wie die ärgerlichste Analyse ), aber im Allgemeinen denke ich, dass dies einen guten Rat gibt. Beachten Sie, dass dies meine subjektive Meinung ist, daher sollte man sich auch die anderen Antworten ansehen.
Helami
2
@celtschk: Das funktioniert nicht für nicht kopierbare, nicht bewegliche Typen. type var{};tut.
ildjarn
2
@celtschk: Ich sage nicht, dass es etwas ist, das häufig vorkommt, aber es ist weniger tippend und funktioniert in mehr Kontexten. Was ist also der Nachteil?
ildjarn
2
Meine Richtlinien fordern sicherlich niemals eine Initialisierung der Kopien. ; -]
ildjarn
26

Ich bin mir ziemlich sicher, dass es niemals eine universelle Richtlinie geben wird. Mein Ansatz ist es, immer geschweifte Klammern zu verwenden, die sich daran erinnern

  1. Konstruktoren der Initialisierungsliste haben Vorrang vor anderen Konstruktoren
  2. Alle Standardbibliothekscontainer und std :: basic_string verfügen über Initialisierungslistenkonstruktoren.
  3. Die Initialisierung der geschweiften Klammer erlaubt keine Einschränkung der Konvertierungen.

Runde und lockige Klammern sind also nicht austauschbar. Wenn ich jedoch weiß, wo sie sich unterscheiden, kann ich in den meisten Fällen die Initialisierung von geschweiften über runden Klammern verwenden (einige der Fälle, in denen ich dies nicht kann, sind derzeit Compiler-Fehler).

Juanchopanza
quelle
6
Geschweifte Klammern haben den Nachteil, dass ich den Listenkonstruktor versehentlich aufrufen kann. Runde Klammern nicht. Ist das nicht ein Grund, standardmäßig die runden Klammern zu verwenden?
Helami
4
@user: Auf der int i = 0;glaube ich nicht, dass jemand dort verwenden würde int i{0}, und es könnte verwirrend sein (auch, 0wenn Typ int, so würde es keine Verengung geben ). Für alles andere würde ich Juanchos Rat befolgen: lieber {}, hüte dich vor den wenigen Fällen, in denen du es nicht tun solltest. Beachten Sie, dass es nicht so viele Typen gibt, die Initialisiererlisten als Konstruktorargumente verwenden. Sie können davon ausgehen, dass Container und containerähnliche Typen (Tupel ...) diese haben, aber der meiste Code ruft den entsprechenden Konstruktor auf.
David Rodríguez - Dribeas
3
@ user1304032 es hängt davon ab, ob Sie sich für eine Verengung interessieren. Ich möchte, dass der Compiler mir sagt, dass dies int i{some floating point}ein Fehler ist, anstatt ihn stillschweigend abzuschneiden.
Juanchopanza
3
Bezüglich "bevorzugen {}, Vorsicht vor den wenigen Fällen, in denen Sie dies nicht tun sollten": Angenommen, zwei Klassen haben einen semantisch äquivalenten Konstruktor, aber eine Klasse hat auch eine Initialisiererliste. Sollten die beiden äquivalenten Konstruktoren unterschiedlich aufgerufen werden?
Helami
3
@helami: "Angenommen, zwei Klassen haben einen semantisch äquivalenten Konstruktor, aber eine Klasse hat auch eine Initialisiererliste. Sollten die beiden äquivalenten Konstruktoren unterschiedlich aufgerufen werden?" Angenommen, ich stoße auf die ärgerlichste Analyse. Das kann auf jedem Konstruktor für jede Instanz passieren . Es ist viel einfacher, dies zu vermeiden, wenn Sie nur {}"initialisieren" bedeuten, es sei denn, Sie können dies absolut nicht .
Nicol Bolas
16

Außerhalb von generischem Code (dh Vorlagen) können Sie (und ich) überall Klammern verwenden . Ein Vorteil ist, dass es überall funktioniert, zum Beispiel auch bei der Initialisierung in der Klasse:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

oder für Funktionsargumente:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

Bei Variablen, denen ich zwischen den Stilen T t = { init };oder nicht viel Aufmerksamkeit schenke T t { init };, ist der Unterschied gering und führt im schlimmsten Fall nur zu einer hilfreichen Compilermeldung über den Missbrauch eines explicitKonstruktors.

Für Typen, die akzeptieren, std::initializer_listobwohl offensichtlich manchmal die Nichtkonstruktoren std::initializer_listbenötigt werden (das klassische Beispiel ist std::vector<int> twenty_answers(20, 42);). Es ist in Ordnung, dann keine Zahnspangen zu verwenden.


Wenn es um generischen Code geht (dh in Vorlagen), sollte dieser allerletzte Absatz einige Warnungen auslösen. Folgendes berücksichtigen:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Dann auto p = make_unique<std::vector<T>>(20, T {});wird ein Vektor der Größe 2 erstellt, wenn Tz. B. intein Vektor der Größe 20, wenn dies der Fall Tist std::string. Ein sehr verräterisches Zeichen dafür, dass hier etwas sehr Falsches vor sich geht, ist, dass es keine Eigenschaft gibt, die Sie hier retten kann (z. B. bei SFINAE): std::is_constructibleIn Bezug auf die Direktinitialisierung, während wir die Klammerinitialisierung verwenden, die sich von der Direktinitialisierung unterscheidet. Initialisierung genau dann, wenn kein Konstruktor std::initializer_listeingreift. Ebenso std::is_convertiblehilft nichts.

Ich habe untersucht, ob es tatsächlich möglich ist, ein Merkmal von Hand zu rollen, das dies beheben kann, aber ich bin nicht allzu optimistisch. Auf jeden Fall denke ich nicht, dass wir viel vermissen würden, ich denke, dass die Tatsache, dass make_unique<T>(foo, bar)eine Konstruktion gleichbedeutend T(foo, bar)ist, sehr intuitiv ist; vor allem angesichts dessen make_unique<T>({ foo, bar })ist das ziemlich unähnlich und macht nur Sinn, wenn foound barhaben den gleichen Typ.

Daher verwende ich für generischen Code nur geschweifte Klammern für die Wertinitialisierung (z. B. T t {};oder T t = {};), was sehr praktisch ist und meiner Meinung nach der C ++ 03-Methode überlegen ist T t = T();. Andernfalls handelt es sich entweder um eine direkte Initialisierungssyntax (dh T t(a0, a1, a2);) oder manchmal um eine Standardkonstruktion (dies T t; stream >> t;ist der einzige Fall, in dem ich das verwende, was ich denke).

Das bedeutet jedoch nicht, dass alle Klammern schlecht sind. Betrachten Sie das vorherige Beispiel mit Korrekturen:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

Dies verwendet immer noch geschweifte Klammern zum Erstellen der std::unique_ptr<T>, obwohl der tatsächliche Typ vom Vorlagenparameter abhängt T.

Luc Danton
quelle
@interjay Einige meiner Beispiele müssen möglicherweise stattdessen vorzeichenlose Typen verwenden, z. B. make_unique<T>(20u, T {})um Tentweder unsignedoder zu sein std::string. Ich bin mir nicht sicher, was die Details angeht. (Beachten Sie, dass ich auch die Erwartungen bezüglich der direkten Initialisierung im Vergleich zur Klammerinitialisierung bezüglich z. B. Perfect-Forwarding-Funktionen kommentiert habe.) Es std::string c("qux");wurde nicht angegeben, dass sie als In-Class-Initialisierung funktionieren, um Mehrdeutigkeiten mit Elementfunktionsdeklarationen in der Grammatik zu vermeiden.
Luc Danton
@interjay Ich stimme Ihnen in dem ersten Punkt nicht zu. Sie können gerne 8.5.4 Listeninitialisierung und 13.3.1.7 Initialisierung durch Listeninitialisierung überprüfen. Was die zweite betrifft, müssen Sie sich genauer ansehen, was ich geschrieben habe (was die Initialisierung in der Klasse betrifft) und / oder die C ++ - Grammatik (z. B. Member-Deklarator , der auf Brace-or-Equal-Initialisierer verweist ).
Luc Danton
Hmm, Sie haben Recht - ich habe früher mit GCC 4.5 getestet, was zu bestätigen schien, was ich sagte, aber GCC 4.6 stimmt Ihnen zu. Und ich habe die Tatsache vermisst, dass Sie über die Initialisierung in der Klasse gesprochen haben. Entschuldigen Sie.
Interjay