2015-08-08 4 views
9

Diese Frage betrifft std :: initializer_list und warum es erlaubt ist, primitive Typen zu initialisieren. Betrachten Sie die folgenden zwei Funktionen:Warum erlaubt C++ std :: initializer_list, auf primitive Typen umgestellt zu werden und sie zu initialisieren?

void foo(std::string arg1, bool arg2 = false); 
void foo(std::string arg1, std::deque<std::string> arg2, bool arg3 = false); 

Warum ist es, dass, wenn foo wie dieser Aufruf:

foo("some string", { }); 

Die erste Überlast aufgenommen wird, anstatt die zweite? Nun, eigentlich nicht warum es ausgewählt ist, ist es, weil { } verwendet werden kann, um alles initialisieren, einschließlich primitiver Typen. Meine Frage ist die Begründung dafür.

std :: initializer_list dauert { args... } und kann daher zum Zeitpunkt der Kompilierung keine unbestimmte Länge haben. Der Versuch, etwas wie bool b = { true, true } zu tun, ergibt error: scalar object 'b' requires one element in initialiser.

Während es vielleicht eine gute Idee gewesen wäre, eine einheitliche Initialisierung zuzulassen, ist die Tatsache, dass dies ein verwirrendes und völlig unerwartetes Verhalten ist. In der Tat, wie ist der Compiler in der Lage, dies zu tun, ohne etwas Magie im Hintergrund std :: initializer_list Sachen zu tun?

Es sei denn, { args... } ist ein lexikalisches Konstrukt C++, in diesem Fall steht mein Punkt immer noch: Warum darf es bei der Initialisierung primitiver Typen verwendet werden?

Danke. Ich hatte hier ziemlich viel Spaß, bevor ich erkannte, dass die falsche Überladung aufgerufen wurde. Verbrachte 10 Minuten, um herauszufinden, warum.

+5

'{}' ist eine * verspannt-init-Liste *; Es hat keinen Typ. 'std :: initializer_list' ist eine andere Sache. –

+0

Ich weiß nicht, wie interessiert Sie daran sind, aber die erste wird ausgewählt, weil '{}' -> 'bool' die Identitätskonvertierung ist und' {} '->' std :: deque 'ist ein Benutzer- definierte Konvertierungssequenz 'arg3' spielt bei der Überladungsauflösung keine Rolle, und der Parameter' bool arg2 = false' wird wie 'bool arg2' behandelt, da es zwei Argumente gibt. – chris

Antwort

5

Die {} Syntax ist eine verspannt-init-Liste, und da es als ein Argument in einem Funktionsaufruf verwendet wird, ist es copy-Liste initialisiert ein entsprechender Parameter.

§ 8.5 [dcl.init]/p17:

(17,1) - Wenn der Initialisierer eine (nicht eingeklammerten) ist verspannt-init-Liste, das Objekt oder die Referenz-Liste -initialisiert (8.5.4).

§ 8.5.4 [dcl.init.list]/p1:

List-Initialisierung ist die Initialisierung eines Objekts oder eine Referenz von einer verspannt-init-Liste. Ein solcher Initialisierer ist , der als Initialisierungsliste bezeichnet wird, und die durch Kommas getrennten Initialisierungsklauseln der Liste werden die Elemente der Initialisierungsliste genannt. Eine Initialisierungsliste ist möglicherweise leer. Listeninitialisierung kann in Direktinitialisierung auftreten oder Kopierinitialisierung Kontexte; [...]

Für einen Klasse-Typ-Parameter, mit list-Initialisierung, Überladungsauflösung sieht in zwei Phasen für einen lebensfähigen Konstruktor up:

§ 13.3.1.7 [over.match.list]/p1:

Wenn Objekte von nicht-aggregierte Klassentyp T Liste initialisiert (8.5.4) sind, wählt die Überladungsauflösung des Konstruktors in zwei Phasen:

- Anfänglich ar die Kandidatenfunktionen e die Initialisiererlisten-Konstruktoren (8.5.4) der Klasse T und die Argumentliste besteht aus der Initialisiererliste als einzelnes Argument.

- Wenn kein ausführbarer Initialisierungslistenkonstruktor gefunden wird, wird die Überladungsauflösung erneut ausgeführt, wobei die Kandidatenfunktionen alle Konstruktoren der Klasse T sind und die Argumentliste aus den Elementen der Initialisierungsliste besteht.

aber:

Wenn die Initialisiererliste keine Elemente hat und T einen Standardkonstruktor hat, wird die erste Phase weggelassen.

Da std::deque<T> definiert einen nicht-expliziten Standardkonstruktors wird ein für die Überladungsauflösung auf einen Satz von lebensfähigen Funktionen hinzugefügt. Initialisierung durch einen Konstruktor wird klassifiziert als benutzerdefinierten Umwandlungs (§ 13.3.3.1.5 [over.ics.list]/p4):

Andernfalls, wenn der Parameter eine nicht-aggregierte Klasse ist X und Überladungsauflösung gemäß 13.3.1.7 wählt einen einzigen besten Konstruktor von X aus, um die Initialisierung eines Objekts vom Typ X aus der Argumentinitialisierungsliste auszuführen. Die implizite Konvertierungssequenz ist eine benutzerdefinierte Konvertierungssequenz mit der zweiten Standardkonvertierung Sequenz einer Identitätskonvertierung.

Weiter zu gehen, wird eine leere verspannt-init-Liste kann seinen entsprechenden Parameter (§ 8.5.4 [dcl.init.list]/p3), der für literal Typen steht für Null-Initialisierungswert initialisieren:

(3.7) - Andernfalls, wenn die Initialisierungsliste keine Elemente enthält, wird das Objekt initialisiert.

Dies, für wörtliche Typen wie bool, erfordert keine Umwandlung und wird als Standardkonvertierung klassifiziert (13.3.3.1.5 [over.ics.list]/p7 §):

Andernfalls

, wenn der Parametertyp keine Klasse ist:

(7,2) - wenn die Initialisiererliste keine Elemente aufweist, die implizite Umwandlungsfolge ist die Identitätsumwandlung.

[Beispiel:

void f(int); 
f({ }); 
// OK: identity conversion 

- Ende Beispiel]

Überlastungs Auflösung Kontrollen in erster Linie, wenn es ein Argument vorhanden ist, für die eine Umwandlungsfolge auf einen entsprechenden Parameter ist besser als in einer anderen Überlast (§ 13.3.3 [over.match.best]/p1):

[...] diese Definitionen gegeben, eine tragfähige Funktion F1 definierte eine bessere Funktion als eine andere tragfähige Funktion F2 sein, wenn für alle Argumente i, ICSi(F1) keine schlechte Umwandlungsfolge als ICSi(F2) ist, und dann:

(1.3) - für einige Argument j ist ICSj(F1) eine bessere Conversion-Sequenz als ICSj(F2), oder, wenn nicht, dass [...]

Conversion-Sequenzen gemäß § 13.3.3.2 rangieren [über .ics.rank]/P2:

Wenn die Grundformen von impliziten Konvertierungssequenzen zu vergleichen

(wie in 13.3.3.1 definiert)

(2,1) - eine Standardkonvertierungssequenz (13.3.3.1.1) ist ein besseres Umwandlungsfolge als eine benutzerdefinierte Konvertierungssequenz oder einer Ellipsen-Umwandlungsfolge, und [...]

als solche wird die erste Überlast mit bool mit {} initialisiert wird als bessere Übereinstimmung betrachtet.

+0

Ich bin immer verwirrt, in welcher Reihenfolge die Unterabschnitte der Überladungsauflösung gelten, aber ich denke, dass alle wichtigen Aspekte für das OP jetzt in deiner Antwort sind. Obwohl ich immer noch nicht ganz verstehe warum * hat das Komitee * '{}' -> class type * als benutzerdefinierte Konvertierung klassifiziert. – dyp

+0

Okay, ich akzeptiere das als Antwort; Es ist das vollständigste und gründlichste. Wie auch immer, vielen Dank für die Eingabe. Ich bekomme es jetzt, einheitliche Initialisierungsliste! = Std :: initialiser_list. – zhiayang

3

Leider zeigt {} nicht tatsächlich eine std::initializer_list an. Es wird auch für eine einheitliche Initialisierung verwendet. Eine einheitliche Initialisierung sollte die Probleme der Stapel verschiedener Arten beheben, in denen C++ - Objekte initialisiert werden konnten, aber letztendlich nur die Dinge verschlimmerten, und der syntaktische Konflikt mit std::initializer_list ist ziemlich schrecklich.

Endergebnis ist, dass {} eine std::initializer_list und {} bezeichnen uniforme Initialisierung sind zwei verschiedene Dinge, außer wenn sie nicht sind.

der Tat, wie wird der Compiler in der Lage, dies zu tun, ohne eine gewisse Magie in der Hintergrund tun std :: initialiser_list Dinge?

Die oben erwähnte Magie existiert am sichersten. { args... } ist einfach ein lexikalisches Konstrukt und die semantische Interpretation hängt vom Kontext ab - es ist sicherlich kein std::initializer_list, es sei denn, der Kontext sagt es ist.

Warum darf es bei der Initialisierung primitiver Typen verwendet werden?

Da das Standards Committee nicht richtig überlegte, wie kaputt es war, die gleiche Syntax für beide Funktionen zu verwenden.

Letztendlich ist uniform init durch Design gebrochen und sollte realistisch verboten werden.

+2

Das ist nicht korrekt, '{}' ist immer eine _initializer-Liste_ (siehe [dcl.init.list]), aber das ist nicht dasselbe wie 'std :: initializer_list'. –

+0

Das ... ist einfach eine unglückliche Art, dass die Konstrukte benannt werden. Ich wollte auf 'std :: initializer_list' verweisen. Es war mir nicht eingefallen, dass das lexikalische Konstrukt auch Initialisierungsliste genannt wurde. – Puppy

+1

Es ist nicht nur Zufall und "Initialisiererliste" ist ein semantisches Konstrukt, nicht nur ein Grammatikbegriff. Die Klassenvorlage 'std :: initializer_list' ist das Dienstprogramm, mit dem Sie eine Initialisierungsliste erfassen und sie als Objekt der ersten Klasse behandeln können. Einige Initialisiererlisten werden in ein "std :: initialiser_list" -Objekt konvertiert, aber nicht alle tun dies, manche führen gerade eine Initialisierung direkt durch, z. durch Aufrufen eines Konstruktors. –

0

Meine Frage ist die Begründung dafür.

Die Begründung dahinter ist einfach (wenn auch fehlerhaft). Die Initialisierung der Liste initialisiert alles.
Insbesondere {} steht für "Standard" Initialisierung des Objekts, das es entspricht; Ob dies bedeutet, dass sein initializer_list-Konstruktor mit einer leeren Liste aufgerufen wird oder dass sein Standardkonstruktor aufgerufen wird oder dass er value-initialisiert wird oder dass alle Aggregatunterobjekte mit {} usw. initialisiert werden, ist irrelevant: Es ist soll als ein universeller Initialisierer für jedes Objekt dienen, auf das das obige angewendet werden kann.

Wenn Sie die zweite Überladung aufrufen möchten, müssten Sie z. std::deque<std::string>{} (oder übergeben Sie drei Argumente an erster Stelle). Das ist der aktuelle Modus operandi.

Während es wie eine gute Idee scheint haben könnte einheitliche Initialisierung zu ermöglichen, ist die Tatsache, dass dies verwirrend ist und völlig unerwartetes Verhalten.

Ich würde es nicht "völlig unerwartet" nennen. Was ist verwirrend, wenn primitive Typen initialisiert werden? Es ist absolut wichtig für Aggregate - aber es gibt keinen großen Schritt von Aggregattypen zu arithmetischen, da in beiden Fällen keine initializer_list beteiligt ist. Vergiss nicht, dass es z.B. nützlich sein, um eine Verengung zu verhindern.

std::initialiser_list nimmt { args... }, und als solche nicht unbestimmter Länge zum Zeitpunkt der Zusammenstellung haben kann.

Nun, technisch gesehen,

std::initializer_list<int> f(bool b) { 
    return b? std::initializer_list<int>{} : std::initializer_list<int>{1}; 
} 
+1

Ich verstehe nicht, warum *** uniform ** Initialisierung * bevorzugt Nicht-Klassen-Typen zu initialisieren, wenn die Liste leer ist. Zumal [over.ics.list] p2 list-init von Klassentypen als * Identitätskonvertierung * einstufen kann. Dies wurde durch [CWG 1467] (https://github.com/cplusplus/draft/commit/6513c26cc9af918377ebc8733e57bc3e3d48d963) eingeführt, das auch den Wortlaut von dcl.init.list überarbeitet hat. OTOH [CWG 1301] (https://github.com/cplusplus/draft/commit/7eed4036396d73f7c75719808119d993e840ce8c) hat nur den Wortlaut von dcl.init.list überarbeitet, aber nicht over.ics.list. Warum? – dyp

+0

@dyp Vielleicht Einfachheit. Überladungsauflösung bevorzugt eine einfache Skalarinitialisierung zu einem Funktionsaufruf (Konstruktor)? – Columbo

+0

Ich bin mir nicht wirklich sicher. Das Initialisieren eines Klassentyps mit einem trivialen Standard-CTor über '{}' ruft keinen ctor überhaupt auf, was [over.ics.list] p6 merkwürdig IMO macht. Es sieht eher so aus, als würde dieser Fall einfach fehlen. Ich werde versuchen, morgen in den Vorschlägen etwas zu finden (habe keine relevanten DRs gefunden), sonst werde ich eine std-Diskussion stellen. – dyp

Verwandte Themen