Por que {} como argumento de função não leva à ambiguidade?

20

Considere este código:

#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

Isso imprime:

vector overload
int overload

Por que isso não produz um erro de compilação devido à resolução ambígua de sobrecarga? Se o segundo construtor for removido, obteremos vector overloadduas vezes. Como / por qual métrica é intuma correspondência inequivocamente melhor do {}que std::vector<int>?

A assinatura do construtor certamente pode ser melhorada, mas acabei de ser enganado por um trecho de código equivalente e quero garantir que nada de importante seja perdido para esta pergunta.

Max Langhof
fonte
Se eu me lembro corretamente {}como um bloco de código, atribui 0 a variáveis ​​- exemplo: const char x = {}; é definido como 0 (caractere nulo), o mesmo para int etc.
Seti
2
@ Seti Isso é o que {}efetivamente faz em certos casos especiais, mas geralmente não é correto (para iniciantes, std::vector<int> x = {};funciona, std::vector <int> x = 0;não). Não é tão simples como " {}atribui zero".
Max Langhof
Certo, não é tão simples, mas ainda assim atribui zero - pensei que eu acho que esse castor é bastante confuso e não deveria ser usado realmente
Seti
2
O @Seti struct A { int x = 5; }; A a = {};não atribui zero em nenhum sentido, ele constrói um Acom a.x = 5. Isso é diferente A a = { 0 };, que inicializa a.xcomo 0. O zero não é inerente {}, é inerente à forma como cada tipo é construído por padrão ou inicializado por valor. Veja aqui , aqui e aqui .
Max Langhof 23/03
Eu ainda acho que o valor construído por padrão é confuso (requer que você verifique o comportamento ou mantenha muito conhecimento o tempo todo)
Seti

Respostas:

12

Está em [over.ics.list] , ênfase minha

6 Caso contrário, se o parâmetro for uma classe X não agregada e a resolução de sobrecarga por [over.match.list] escolher um único melhor construtor C de X para executar a inicialização de um objeto do tipo X na lista de inicializadores de argumentos:

  • Se C não for um construtor da lista de inicializadores e a lista de inicializadores tiver um único elemento do tipo cv U, em que U é X ou uma classe derivada de X, a sequência de conversão implícita terá classificação de Correspondência exata se U for X ou classificação de conversão se U é derivado de X.

  • Caso contrário, a sequência de conversão implícita é uma sequência de conversão definida pelo usuário e a segunda sequência de conversão padrão é uma conversão de identidade.

9 Caso contrário, se o tipo de parâmetro não for uma classe:

  • [...]

  • se a lista de inicializadores não tiver elementos, a sequência implícita de conversão será a conversão de identidade. [Exemplo:

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

    exemplo final]

O std::vectoré inicializado pelo construtor e o marcador em negrito considera uma conversão definida pelo usuário. Enquanto isso, para um int, essa é a conversão de identidade, por isso supera a classificação do primeiro c'tor.

Contador de Histórias - Monica Sem Calúnia
fonte
Sim, parece preciso.
Columbo
Interessante ver que esta situação é explicitamente considerada na norma. Eu realmente esperava que fosse ambíguo (e parece que poderia ter sido facilmente especificado dessa maneira). Não consigo seguir o raciocínio em sua última frase - 0tem tipo, intmas não tipo std::vector<int>, como isso é "como se" escrevesse na natureza "não digitada" de {}?
Max Langhof 03/03
@ MaxLanghof - A outra maneira de ver isso é que, para tipos que não são de classe, não é uma conversão definida pelo usuário de forma alguma. Em vez disso, é um inicializador direto para o valor padrão. Daí uma identidade neste caso.
StoryTeller - Unslander Monica 03/03
Essa parte está clara. Estou surpreso com a necessidade de uma conversão definida pelo usuário para std::vector<int>. Como você disse, eu esperava que "o tipo do parâmetro acabasse decidindo qual é o tipo do argumento", e um {}"do tipo" (por assim dizer) std::vector<int>não deveria precisar de conversão (sem identidade) para inicializar a std::vector<int>. O padrão obviamente diz que sim, então é isso, mas não faz sentido para mim. (Lembre-se, eu não estou argumentando que você ou o padrão está errado, apenas tentando conciliar isso com meus modelos mentais.)
Max Langhof
Ok, essa edição não foi a resolução que eu esperava, mas bastante justa. : D Obrigado pelo seu tempo!
Max Langhof 03/03