Ist C ++ 11 Uniform Initialization ein Ersatz für die alte Syntax?


172

Ich verstehe, dass die einheitliche Initialisierung von C ++ 11 einige syntaktische Unklarheiten in der Sprache behebt, aber in vielen Präsentationen von Bjarne Stroustrup (insbesondere in den Gesprächen mit GoingNative 2012) verwenden seine Beispiele diese Syntax jetzt hauptsächlich, wenn er Objekte konstruiert.

Wird jetzt empfohlen, in allen Fällen eine einheitliche Initialisierung zu verwenden ? Wie sollte der allgemeine Ansatz für diese neue Funktion hinsichtlich des Codierungsstils und der allgemeinen Verwendung aussehen? Was sind einige Gründe, es nicht zu benutzen?

Beachten Sie, dass ich in erster Linie an die Objektkonstruktion als meinen Anwendungsfall denke, aber wenn es andere zu berücksichtigende Szenarien gibt, lassen Sie es mich bitte wissen.


Dies könnte ein Thema sein, das auf Programmers.se besser diskutiert wird. Es scheint sich zur guten subjektiven Seite zu neigen.
Nicol Bolas

6
@NicolBolas: Andererseits könnte Ihre ausgezeichnete Antwort ein sehr guter Kandidat für das c ++ - faq-Tag sein. Ich glaube nicht, dass wir vorher eine Erklärung dafür hatten.
Matthieu M.

Antworten:


233

Der Codierungsstil ist letztendlich subjektiv und es ist sehr unwahrscheinlich, dass sich daraus wesentliche Leistungsvorteile ergeben. Aber hier ist, was ich sagen würde, dass Sie von der liberalen Verwendung der einheitlichen Initialisierung profitieren:

Minimiert redundante Typenamen

Folgendes berücksichtigen:

vec3 GetValue()
{
  return vec3(x, y, z);
}

Warum muss ich vec3zweimal tippen ? Gibt es einen Grund dafür? Der Compiler weiß gut und genau, was die Funktion zurückgibt. Warum kann ich nicht einfach sagen: "Rufen Sie den Konstruktor dessen auf, was ich mit diesen Werten zurückgebe, und geben Sie ihn zurück?" Mit der einheitlichen Initialisierung kann ich:

vec3 GetValue()
{
  return {x, y, z};
}

Funktioniert alles.

Noch besser ist es für Funktionsargumente. Bedenken Sie:

void DoSomething(const std::string &str);

DoSomething("A string.");

Das geht, ohne einen Typnamen eingeben zu müssen, denn std::stringman kann sich aus einem const char*implizit aufbauen . Das ist großartig. Aber was ist, wenn diese Zeichenfolge stammt, sagen Sie RapidXML. Oder eine Lua-Saite. Nehmen wir an, ich kenne die Länge der Saite von vorne. Der std::stringKonstruktor, der a annimmt const char*, muss die Länge der Zeichenfolge annehmen, wenn ich nur a übergebe const char*.

Es gibt jedoch eine Überladung, die explizit eine Länge hat. Aber , es zu benutzen, würde ich dies tun: DoSomething(std::string(strValue, strLen)). Warum ist dort der zusätzliche Typenname angegeben? Der Compiler kennt den Typ. Genau wie bei autokönnen wir zusätzliche Typnamen vermeiden:

DoSomething({strValue, strLen});

Es funktioniert einfach Keine Typnamen, keine Aufregung, nichts. Der Compiler macht seine Arbeit, der Code ist kürzer und alle sind glücklich.

Zugegeben, es gibt Argumente dafür, dass die erste Version ( DoSomething(std::string(strValue, strLen))) besser lesbar ist. Das heißt, es ist offensichtlich, was los ist und wer was tut. Das ist bis zu einem gewissen Grad wahr; Um den einheitlichen, auf der Initialisierung basierenden Code zu verstehen, muss der Funktionsprototyp betrachtet werden. Dies ist der gleiche Grund, warum manche sagen, Sie sollten Parameter niemals als Nicht-Konstanten-Referenz übergeben: Damit Sie auf der Aufruf-Site sehen können, ob ein Wert geändert wird.

Aber das Gleiche könnte man sagen auto; Um zu wissen, was Sie daraus machen, auto v = GetSomething();müssen Sie die Definition von GetSomething. Aber das hat nicht aufgehört auto, mit fast rücksichtsloser Hingabe verwendet zu werden, sobald Sie Zugriff darauf haben. Persönlich denke ich, dass es in Ordnung sein wird, wenn Sie sich daran gewöhnt haben. Vor allem mit einer guten IDE.

Erhalten Sie niemals das nervigste Parse

Hier ist ein Code.

class Bar;

void Func()
{
  int foo(Bar());
}

Pop Quiz: Was ist das foo? Wenn Sie mit "eine Variable" geantwortet haben, liegen Sie falsch. Es ist eigentlich der Prototyp einer Funktion, die eine Funktion als Parameter verwendet, die a zurückgibt Bar, und der fooRückgabewert der Funktion ist ein int.

Dies nennt man C ++ "Most Vexing Parse", weil es für einen Menschen absolut keinen Sinn ergibt. Aber die Regeln von C ++ verlangen dies leider: Wenn es möglicherweise als Funktionsprototyp interpretiert werden kann, dann ist es das auch. Das Problem ist Bar(): Das könnte eines von zwei Dingen sein. Es kann sich um einen Typ handeln Bar, der einen temporären Typ erstellt. Oder es könnte eine Funktion sein, die keine Parameter akzeptiert und a zurückgibt Bar.

Eine einheitliche Initialisierung kann nicht als Funktionsprototyp interpretiert werden:

class Bar;

void Func()
{
  int foo{Bar{}};
}

Bar{}schafft immer eine temporäre. int foo{...}Erstellt immer eine Variable.

Es gibt viele Fälle, die Sie verwenden möchten, Typename()aber aufgrund der Parsing-Regeln von C ++ einfach nicht können. Mit Typename{}gibt es keine Mehrdeutigkeit.

Gründe, nicht zu

Die einzige wirkliche Kraft, die Sie aufgeben, ist die Verengung. Sie können einen kleineren Wert nicht mit einem größeren Wert mit einheitlicher Initialisierung initialisieren.

int val{5.2};

Das wird nicht kompiliert. Sie können dies mit einer altmodischen Initialisierung tun, jedoch nicht mit einer einheitlichen Initialisierung.

Dies wurde teilweise durchgeführt, damit Initialisierungslisten tatsächlich funktionieren. Andernfalls würde es in Bezug auf die Typen von Initialisierungslisten viele mehrdeutige Fälle geben.

Natürlich könnten einige argumentieren, dass ein solcher Code es verdient , nicht kompiliert zu werden. Ich persönlich stimme dem zu. Einengung ist sehr gefährlich und kann zu unangenehmem Verhalten führen. Es ist wahrscheinlich am besten, diese Probleme frühzeitig in der Compiler-Phase zu erkennen. Zumindest deutet die Einschränkung darauf hin, dass jemand nicht zu sehr über den Code nachdenkt.

Beachten Sie, dass Compiler Sie in der Regel vor solchen Situationen warnen, wenn Ihre Warnstufe hoch ist. Das alles macht die Warnung zu einem erzwungenen Fehler. Einige könnten sagen, dass Sie das trotzdem tun sollten;)

Es gibt einen weiteren Grund, nicht:

std::vector<int> v{100};

Was macht das? Es könnte ein vector<int>mit einhundert vorkonstruiertes Objekt entstehen. Oder es könnte ein vector<int>mit 1 Element erstellt werden, dessen Wert ist 100. Beides ist theoretisch möglich.

In Wirklichkeit tut es das letztere.

Warum? Initialisierungslisten verwenden dieselbe Syntax wie die einheitliche Initialisierung. Es muss also einige Regeln geben, die erklären, was im Falle von Mehrdeutigkeiten zu tun ist. Die Regel ist ziemlich einfach: Wenn der Compiler kann eine Initialisiererliste Konstruktor mit einem Doppelpack initialisierte Liste verwendet, dann es wird . Da vector<int>es einen Initialisiererlistenkonstruktor gibt, der benötigt initializer_list<int>und {100} gültig sein könnte initializer_list<int>, muss dies der Fall sein .

Um den Sizing-Konstruktor zu erhalten, müssen Sie ()anstelle von verwenden {}.

Beachten Sie, dass vectordies nicht passieren würde , wenn dies von etwas wäre, das nicht in eine Ganzzahl konvertierbar wäre. Eine initializer_list würde nicht in den Initializer-List-Konstruktor dieses vectorTyps passen , und daher kann der Compiler frei aus den anderen Konstruktoren auswählen.


11
+1 Genagelt. Ich lösche meine Antwort, da Ihre alle dieselben Punkte ausführlicher behandelt.
R. Martinho Fernandes

21
Der letzte Punkt ist, warum ich wirklich möchte std::vector<int> v{100, std::reserve_tag};. Ähnlich ist es mit std::resize_tag. Derzeit sind zwei Schritte erforderlich, um Vektorraum zu reservieren.
Xeo

6
@NicolBolas - Zwei Punkte: Ich dachte, das Problem mit der lästigen Analyse sei foo (), nicht Bar (). Mit anderen Worten, wenn Sie dies tun int foo(10)würden, würden Sie nicht auf dasselbe Problem stoßen? Zweitens scheint ein weiterer Grund, es nicht zu verwenden, eher eine Frage der Überentwicklung zu sein. Was ist, wenn wir alle unsere Objekte mithilfe von konstruieren {}, aber einen Tag später einen Konstruktor für Initialisierungslisten hinzufügen? Jetzt werden alle meine Konstruktionsanweisungen zu Initialisiererlistenanweisungen. Scheint sehr fragil in Bezug auf Refactoring. Irgendwelche Kommentare dazu?
void.pointer

7
@RobertDailey: "Wenn du es tun int foo(10)würdest, würdest du nicht auf dasselbe Problem stoßen ?" Nr. 10 ist ein Integer-Literal und ein Integer-Literal kann niemals ein Typname sein. Die Bar()lästige Analyse ergibt sich aus der Tatsache, dass es sich um einen Typnamen oder einen temporären Wert handeln kann. Das schafft die Mehrdeutigkeit für den Compiler.
Nicol Bolas

8
unpleasant behavior- Es gibt einen neuen Standardbegriff, den man sich merken sollte:>
sehe

64

Ich werde Nicol Bolas 'Antwort widersprechen . Minimiert redundante Typenamen . Da Code einmal geschrieben und mehrmals gelesen wird, sollten wir versuchen, die Zeit zu minimieren, die zum Lesen und Verstehen von Code erforderlich ist, und nicht die Zeit, die zum Schreiben von Code erforderlich ist. Der Versuch, nur das Tippen zu minimieren, ist der Versuch, das Falsche zu optimieren.

Siehe folgenden Code:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

Jemand, der den obigen Code zum ersten Mal liest, wird die return-Anweisung wahrscheinlich nicht sofort verstehen, da er den return-Typ vergessen hat, wenn er diese Zeile erreicht. Jetzt muss er zur Funktionssignatur zurückblättern oder eine IDE-Funktion verwenden, um den Rückgabetyp zu sehen und die return-Anweisung vollständig zu verstehen.

Und auch hier ist es für jemanden, der den Code zum ersten Mal liest, nicht einfach zu verstehen, was tatsächlich erstellt wird:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

Der obige Code bricht ab, wenn jemand beschließt, dass DoSomething auch einen anderen Zeichenfolgentyp unterstützen soll, und fügt diese Überladung hinzu:

void DoSomething(const CoolStringType& str);

Wenn CoolStringType zufällig einen Konstruktor hat, der ein const char * und ein size_t (wie es std :: string tut), führt der Aufruf von DoSomething ({strValue, strLen}) zu einem Mehrdeutigkeitsfehler.

Meine Antwort auf die eigentliche Frage:
Nein, die einheitliche Initialisierung sollte nicht als Ersatz für die Konstruktorsyntax des alten Stils betrachtet werden.

Und meine Argumentation ist folgende:
Wenn zwei Aussagen nicht die gleiche Absicht haben, sollten sie nicht gleich aussehen. Es gibt zwei Arten von Begriffen für die Objektinitialisierung:
1) Nehmen Sie alle diese Elemente und gießen Sie sie in dieses Objekt, das ich initialisiere.
2) Konstruieren Sie dieses Objekt mit den Argumenten, die ich als Richtlinie angegeben habe.

Beispiele für die Verwendung von Begriff # 1:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Beispiel für die Verwendung von Begriff # 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Ich finde es schlecht, dass der neue Standard es Leuten erlaubt, Treppen wie folgt zu initialisieren:

Stairs s {2, 4, 6};

... weil das die Bedeutung des Konstruktors verschleiert. Eine solche Initialisierung sieht genauso aus wie die erste, ist es aber nicht. Es werden nicht drei verschiedene Stufenhöhenwerte in das Objekt eingegeben, auch wenn es so aussieht. Und, was noch wichtiger ist, wenn eine Bibliotheksimplementierung von Stairs wie oben veröffentlicht wurde und Programmierer sie verwendet haben und der Bibliotheksimplementierer später einen Konstruktor initializer_list zu Stairs hinzufügt, dann den gesamten Code, der Stairs mit einheitlicher Initialisierung verwendet hat Die Syntax wird brechen.

Ich denke, dass die C ++ - Community einer gemeinsamen Konvention zustimmen sollte, wie Uniform Initialization verwendet wird, dh einheitlich bei allen Initialisierungen, oder, wie ich stark empfehle, diese beiden Begriffe der Initialisierung zu trennen und damit dem Leser die Absicht des Programmierers zu verdeutlichen der Code.


AFTERTHOUGHT:
Ein weiterer Grund, warum Sie Uniform Initialization nicht als Ersatz für die alte Syntax betrachten sollten und warum Sie die geschweifte Klammer nicht für alle Initialisierungen verwenden können:

Angenommen, Ihre bevorzugte Syntax zum Erstellen einer Kopie lautet:

T var1;
T var2 (var1);

Jetzt sollten Sie alle Initialisierungen durch die neue geschweifte Syntax ersetzen, damit Sie konsistenter (und der Code wird konsistenter) aussehen. Die Syntax mit geschweiften Klammern funktioniert jedoch nicht, wenn Typ T ein Aggregat ist:

T var2 {var1}; // fails if T is std::array for example

48
Wenn Sie "<sehr viel Code hier>" haben, ist Ihr Code ungeachtet der Syntax schwer zu verstehen.
Kevin Cline

8
Neben IMO ist es Aufgabe Ihrer IDE, Sie darüber zu informieren, welchen Typ er zurückgibt (z. B. durch Schweben). Natürlich, wenn Sie keine IDE verwenden, haben Sie die Last auf sich genommen :)
abergmeier

4
@TommiT Ich stimme einigen Ihrer Aussagen zu. Doch im gleichen Geist wie die autogegen explizite Typdeklaration Debatte, würde ich für ein Gleichgewicht argumentieren: einheitliche initializers in ziemlich große Zeit - Rock - Vorlage Meta-Programmierung Situationen , in denen die Art der Regel ziemlich offensichtlich ohnehin ist. Es wird -> decltype(....)vermieden, dass Sie Ihre verdammt komplizierten Beschwörungsformeln wiederholen, z. B. für einfache Online-Funktionsvorlagen (die mich zum Weinen gebracht haben).
Siehe

5
Msgstr "" " Die Syntax mit geschweiften Klammern funktioniert jedoch nicht, wenn Typ T ein Aggregat ist: " "Beachten Sie, dass dies eher ein gemeldeter Fehler des Standardverhaltens als ein beabsichtigtes erwartetes Verhalten ist.
Nicol Bolas

5
"Jetzt muss er zurück zur Funktionssignatur scrollen." Wenn Sie scrollen müssen, ist Ihre Funktion zu groß.
Miles Rout

-3

Wenn Ihre Konstruktoren merely copy their parametersin den jeweiligen Klassenvariablen in exactly the same orderdeklariert sind, in denen sie innerhalb der Klasse deklariert sind, kann die Verwendung einer einheitlichen Initialisierung möglicherweise schneller (aber auch absolut identisch) sein als der Aufruf des Konstruktors.

Dies ändert natürlich nichts an der Tatsache, dass Sie den Konstruktor immer deklarieren müssen.


2
Warum kann es schneller gehen?
JBCOE

Das ist falsch. Es gibt keine Anforderung einen Konstruktor zu deklarieren: struct X { int i; }; int main() { X x{42}; }. Es ist auch falsch, dass eine einheitliche Initialisierung schneller sein kann als eine Wertinitialisierung.
Tim
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.