Warum führt {} als Funktionsargument nicht zu Mehrdeutigkeiten?

20

Betrachten Sie diesen Code:

#include <vector>
#include <iostream>

enum class A
{
  X, Y
};

struct Test
{
  Test(const std::vector<double>&, const std::vector<int>& = {}, A = A::X)
  { std::cout << "vector overload" << std::endl; }

  Test(const std::vector<double>&, int, A = A::X)
  { std::cout << "int overload" << std::endl; }
};

int main()
{
  std::vector<double> v;
  Test t1(v);
  Test t2(v, {}, A::X);
}

https://godbolt.org/z/Gc_w8i

Dies druckt:

vector overload
int overload

Warum führt dies nicht zu einem Kompilierungsfehler aufgrund einer mehrdeutigen Überlastungsauflösung? Wenn der zweite Konstruktor entfernt wird, erhalten wir vector overloadzwei Mal. Wie / nach welcher Metrik passt inteine eindeutig bessere Übereinstimmung {}als std::vector<int>?

Die Konstruktorsignatur kann sicherlich weiter gekürzt werden, aber ich wurde gerade von einem äquivalenten Code ausgetrickst und möchte sicherstellen, dass für diese Frage nichts Wichtiges verloren geht.

Max Langhof
quelle
Wenn ich mich korrekt {}als Codeblock erinnere , weist ich Variablen 0 zu - Beispiel: const char x = {}; wird auf 0 (null Zeichen) gesetzt, dasselbe gilt für int usw.
Seti
2
@Seti Das ist es, was {}in bestimmten Sonderfällen effektiv funktioniert, aber es ist im Allgemeinen nicht korrekt (für den Anfang, std::vector<int> x = {};funktioniert, std::vector <int> x = 0;funktioniert nicht). Es ist nicht so einfach wie " {}Null zuweisen".
Max Langhof
Richtig, es ist nicht so einfach, aber es weist immer noch Null zu - ich denke, dass dieser Beaviour ziemlich verwirrend ist und nicht wirklich verwendet werden sollte
Seti
2
@Seti struct A { int x = 5; }; A a = {};weist in keiner Weise Null zu, sondern erstellt eine Amit a.x = 5. Dies ist anders als A a = { 0 };bei der Initialisierung a.xauf 0. Die Null ist nicht inhärent {}, sondern inhärent, wie jeder Typ standardmäßig konstruiert oder wertinitialisiert ist. Sehen Sie hier , hier und hier .
Max Langhof
Ich denke immer noch, dass standardmäßig erstellte Werte verwirrend sind (erfordert, dass Sie das Verhalten überprüfen oder ständig viel Wissen behalten)
Seti

Antworten:

12

Es ist in [over.ics.list] , Hervorhebung von mir

6 Wenn der Parameter eine nicht aggregierte Klasse X ist und die Überladungsauflösung gemäß [over.match.list] einen einzelnen besten Konstruktor C von X auswählt, um die Initialisierung eines Objekts vom Typ X aus der Liste der Argumentinitialisierer durchzuführen:

  • Wenn C kein Konstruktor für die Initialisierungsliste ist und die Initialisiererliste ein einzelnes Element vom Typ cv U enthält, wobei U X oder eine von X abgeleitete Klasse ist, hat die implizite Konvertierungssequenz den Rang Exact Match, wenn U X ist, oder den Konvertierungsrang, wenn U leitet sich von X ab.

  • Andernfalls ist die implizite Konvertierungssequenz eine benutzerdefinierte Konvertierungssequenz, wobei die zweite Standardkonvertierungssequenz eine Identitätskonvertierung ist.

9 Andernfalls, wenn der Parametertyp keine Klasse ist:

  • [...]

  • Wenn die Initialisierungsliste keine Elemente enthält, ist die implizite Konvertierungssequenz die Identitätskonvertierung. [Beispiel:

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

    Ende Beispiel]

Das std::vectorwird vom Konstruktor initialisiert und das fettgedruckte Aufzählungszeichen hält es für eine benutzerdefinierte Konvertierung. In der Zwischenzeit ist intdies die Identitätsumwandlung, sodass der Rang des ersten C'tors übertroffen wird.

Geschichtenerzähler - Unslander Monica
quelle
Ja, scheint genau zu sein.
Columbo
Interessant zu sehen, dass diese Situation im Standard ausdrücklich berücksichtigt wird. Ich hätte wirklich erwartet, dass es mehrdeutig ist (und es sieht so aus, als hätte es leicht so spezifiziert werden können). Ich kann der Argumentation in Ihrem letzten Satz nicht folgen - 0hat Typ, intaber keinen Typ std::vector<int>, wie ist das ein "als ob" für die "untypisierte" Natur von {}?
Max Langhof
@MaxLanghof - Die andere Sichtweise ist, dass es sich bei Nicht-Klassentypen nicht um eine benutzerdefinierte Konvertierung handelt. Stattdessen ist es ein direkter Initialisierer für den Standardwert. Daher eine Identität in diesem Fall.
StoryTeller - Unslander Monica
Dieser Teil ist klar. Ich bin überrascht, dass eine benutzerdefinierte Konvertierung in erforderlich ist std::vector<int>. Wie Sie sagten, habe ich erwartet, dass "der Typ des Parameters letztendlich entscheidet, was der Typ des Arguments ist", und ein {}"Typ" (sozusagen) std::vector<int>sollte keine (Nichtidentitäts-) Konvertierung benötigen, um a zu initialisieren std::vector<int>. Der Standard sagt es offensichtlich, also ist das das, aber es macht für mich keinen Sinn. (Wohlgemerkt, ich behaupte nicht, dass Sie oder der Standard falsch sind, sondern versuche nur, dies mit meinen mentalen Modellen in Einklang zu bringen.)
Max Langhof
Ok, diese Bearbeitung war nicht die Lösung, die ich mir erhofft hatte, aber fair genug. : D Danke für deine Zeit!
Max Langhof