Pourquoi {} comme argument de fonction ne conduit-il pas à l'ambiguïté?

20

Considérez ce 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

Cela imprime:

vector overload
int overload

Pourquoi cela ne produit-il pas une erreur de compilation en raison d'une résolution de surcharge ambiguë? Si le deuxième constructeur est supprimé, nous obtenons vector overloaddeux fois. Comment / selon quelle métrique est intune correspondance sans ambiguïté meilleure {}que std::vector<int>?

La signature du constructeur peut sûrement être coupée plus loin, mais je viens d'être piégé par un morceau de code équivalent et je veux m'assurer que rien d'important n'est perdu pour cette question.

Max Langhof
la source
Si je me rappelle correctement {}comme un bloc de code, assigne 0 aux variables - exemple: const char x = {}; est mis à 0 (caractère nul), même pour int etc.
Seti
2
@Seti C'est ce qui fonctionne {}effectivement dans certains cas particuliers, mais ce n'est généralement pas correct (pour commencer, std::vector<int> x = {};fonctionne, std::vector <int> x = 0;non). Ce n'est pas aussi simple que « {}attribue zéro».
Max Langhof
À droite, ce n'est pas si simple, mais il attribue toujours zéro - je pense que ce beaviour est assez déroutant et ne devrait pas être utilisé vraiment
Seti
2
@Seti struct A { int x = 5; }; A a = {};n'affecte aucun zéro, il construit un Aavec a.x = 5. C'est différent A a = { 0 };, qui s'initialise a.xà 0. Le zéro n'est pas inhérent à {}, il est inhérent à la façon dont chaque type est construit par défaut ou initialisé en valeur. Voyez ici , ici et ici .
Max Langhof
Je pense toujours que la valeur construite par défaut prête à confusion (vous oblige à vérifier le comportement ou à garder beaucoup de connaissances tout le temps)
Seti

Réponses:

12

C'est dans [over.ics.list] , c'est moi qui souligne

6 Sinon, si le paramètre est une classe X non agrégée et que la résolution de surcharge par [over.match.list] choisit un seul meilleur constructeur C de X pour effectuer l'initialisation d'un objet de type X dans la liste d'initialisation d'argument:

  • Si C n'est pas un constructeur de liste d'initialisation et que la liste d'initialisation a un seul élément de type cv U, où U est X ou une classe dérivée de X, la séquence de conversion implicite a le rang de correspondance exacte si U est X, ou le rang de conversion si U est dérivé de X.

  • Sinon, la séquence de conversion implicite est une séquence de conversion définie par l'utilisateur avec la deuxième séquence de conversion standard une conversion d'identité.

9 Sinon, si le type de paramètre n'est pas une classe:

  • [...]

  • si la liste d'initialisation ne contient aucun élément, la séquence de conversion implicite est la conversion d'identité. [ Exemple:

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

    exemple de fin]

Le std::vectorest initialisé par le constructeur et la puce en gras le considère comme une conversion définie par l'utilisateur. Pendant ce temps, pour un int, c'est la conversion d'identité, donc elle l'emporte sur le rang du premier c'tor.

Conteur - Unslander Monica
la source
Oui, semble exact.
Columbo
Il est intéressant de voir que cette situation est explicitement prise en compte dans la norme. Je m'attendais vraiment à ce qu'il soit ambigu (et il semble qu'il aurait pu être facilement spécifié de cette façon). Je ne peux pas suivre le raisonnement de votre dernière phrase - 0a du type intmais pas du type std::vector<int>, comment est-ce un "comme si" écrit à la nature "non typé" de {}?
Max Langhof
@MaxLanghof - L'autre façon de voir les choses est que pour les types non classe, ce n'est pas une conversion définie par l'utilisateur par un tronçon. Au lieu de cela, c'est un initialiseur direct pour la valeur par défaut. D'où une identité dans ce cas.
StoryTeller - Unslander Monica
Cette partie est claire. Je suis surpris par la nécessité d'une conversion définie par l'utilisateur vers std::vector<int>. Comme vous l'avez dit, je m'attendais à ce que "le type du paramètre finisse par décider quel est le type de l'argument", et un {}"de type" (pour ainsi dire) std::vector<int>ne devrait pas avoir besoin de conversion (sans identité) pour initialiser a std::vector<int>. La norme dit évidemment que c'est le cas, c'est donc ça, mais cela n'a pas de sens pour moi. (Attention, je ne prétends pas que vous ou la norme avez tort, essayez simplement de concilier cela avec mes modèles mentaux.)
Max Langhof
Ok, cette modification n'était pas la résolution que j'espérais, mais assez juste. : D Merci pour votre temps!
Max Langhof