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.
Antworten:
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:
Warum muss ich
vec3
zweimal 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:Funktioniert alles.
Noch besser ist es für Funktionsargumente. Bedenken Sie:
Das geht, ohne einen Typnamen eingeben zu müssen, denn
std::string
man kann sich aus einemconst 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. Derstd::string
Konstruktor, der a annimmtconst char*
, muss die Länge der Zeichenfolge annehmen, wenn ich nur a übergebeconst 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 beiauto
können wir zusätzliche Typnamen vermeiden: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 vonGetSomething
. Aber das hat nicht aufgehörtauto
, 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.
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ückgibtBar
, und derfoo
Rü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 handelnBar
, der einen temporären Typ erstellt. Oder es könnte eine Funktion sein, die keine Parameter akzeptiert und a zurückgibtBar
.Eine einheitliche Initialisierung kann nicht als Funktionsprototyp interpretiert werden:
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. MitTypename{}
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.
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:
Was macht das? Es könnte ein
vector<int>
mit einhundert vorkonstruiertes Objekt entstehen. Oder es könnte einvector<int>
mit 1 Element erstellt werden, dessen Wert ist100
. 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ötigtinitializer_list<int>
und {100} gültig sein könnteinitializer_list<int>
, muss dies der Fall sein .Um den Sizing-Konstruktor zu erhalten, müssen Sie
()
anstelle von verwenden{}
.Beachten Sie, dass
vector
dies 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 diesesvector
Typs passen , und daher kann der Compiler frei aus den anderen Konstruktoren auswählen.quelle
std::vector<int> v{100, std::reserve_tag};
. Ähnlich ist es mitstd::resize_tag
. Derzeit sind zwei Schritte erforderlich, um Vektorraum zu reservieren.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?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. DieBar()
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.unpleasant behavior
- Es gibt einen neuen Standardbegriff, den man sich merken sollte:>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:
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:
Der obige Code bricht ab, wenn jemand beschließt, dass DoSomething auch einen anderen Zeichenfolgentyp unterstützen soll, und fügt diese Überladung hinzu:
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:
Beispiel für die Verwendung von Begriff # 2:
Ich finde es schlecht, dass der neue Standard es Leuten erlaubt, Treppen wie folgt zu initialisieren:
... 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:
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:
quelle
auto
gegen 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).Wenn Ihre Konstruktoren
merely copy their parameters
in den jeweiligen Klassenvariablenin exactly the same order
deklariert 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.
quelle
struct X { int i; }; int main() { X x{42}; }
. Es ist auch falsch, dass eine einheitliche Initialisierung schneller sein kann als eine Wertinitialisierung.