¿Por qué {} como argumento de función no conduce a la ambigüedad?

20

Considera 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

Esto imprime:

vector overload
int overload

¿Por qué esto no produce un error de compilación debido a una resolución de sobrecarga ambigua? Si se elimina el segundo constructor, obtenemos vector overloaddos veces. ¿Cómo / por qué métrica es intuna combinación inequívocamente mejor {}que std::vector<int>?

La firma del constructor seguramente se puede recortar aún más, pero acabo de engañarme con un código equivalente y quiero asegurarme de que no se pierda nada importante para esta pregunta.

Max Langhof
fuente
Si recuerdo correctamente {}como un bloque de código, asigna 0 a las variables - ejemplo: const char x = {}; se establece en 0 (null char), lo mismo para int etc.
Seti
2
@Seti Eso es lo que {}efectivamente hace en ciertos casos especiales, pero generalmente no es correcto (para empezar, std::vector<int> x = {};funciona, std::vector <int> x = 0;no lo hace). No es tan simple como " {}asigna cero".
Max Langhof
Correcto, no es tan simple, pero aún así asigna cero: creo que creo que este comportamiento es bastante confuso y no debería usarse realmente
Seti
2
@Seti struct A { int x = 5; }; A a = {};no asigna cero en ningún sentido, construye un Acon a.x = 5. Esto es diferente A a = { 0 };, que se inicializa a.xa 0. El cero no es inherente a {}, es inherente a cómo cada tipo se construye por defecto o se inicializa en valor. Mira aquí , aquí y aquí .
Max Langhof
Todavía creo que el valor predeterminado es confuso (requiere que verifique el comportamiento o que tenga mucho conocimiento todo el tiempo)
Seti

Respuestas:

12

Está en [over.ics.list] , énfasis mío

6 De lo contrario, si el parámetro es una clase X no agregada y la resolución de sobrecarga por [over.match.list] elige un mejor constructor C de X para realizar la inicialización de un objeto de tipo X de la lista de inicializadores de argumentos:

  • Si C no es un constructor de lista de inicializadores y la lista de inicializadores tiene un solo elemento de tipo cv U, donde U es X o una clase derivada de X, la secuencia de conversión implícita tiene un rango de coincidencia exacta si U es X, o un rango de conversión si U se deriva de X.

  • De lo contrario, la secuencia de conversión implícita es una secuencia de conversión definida por el usuario con la segunda secuencia de conversión estándar una conversión de identidad.

9 De lo contrario, si el tipo de parámetro no es una clase:

  • [...]

  • Si la lista de inicializadores no tiene elementos, la secuencia de conversión implícita es la conversión de identidad. [Ejemplo:

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

    ejemplo final]

El std::vectorconstructor lo inicializa y la viñeta en negrita lo considera una conversión definida por el usuario. Mientras tanto, para una int, esta es la conversión de identidad, por lo que supera el rango del primer c'tor.

StoryTeller - Unslander Monica
fuente
Sí, parece exacto.
Columbo
Es interesante ver que esta situación se considera explícitamente en la norma. Realmente hubiera esperado que fuera ambiguo (y parece que podría haberse especificado fácilmente de esa manera). No puedo seguir el razonamiento en su última oración: 0tiene el tipo intpero no el tipo std::vector<int>, ¿cómo es eso un "como si" fuera de la naturaleza "sin tipo" {}?
Max Langhof
@MaxLanghof: la otra forma de verlo es que para los tipos que no son de clase, no es una conversión definida por el usuario de ninguna manera. En cambio, es un inicializador directo para el valor predeterminado. De ahí una identidad en este caso.
StoryTeller - Unslander Monica
Esa parte está clara. Me sorprende la necesidad de una conversión definida por el usuario std::vector<int>. Como dijiste, esperaba que "el tipo de parámetro termine decidiendo cuál es el tipo de argumento", y un {}"tipo" (por así decirlo) std::vector<int>no debería necesitar una conversión (no de identidad) para inicializar a std::vector<int>. El estándar obviamente dice que sí, así que eso es todo, pero no tiene sentido para mí. (Eso sí, no estoy argumentando que usted o el estándar están equivocados, solo estoy tratando de conciliar esto con mis modelos mentales.)
Max Langhof
Ok, esa edición no fue la resolución que esperaba, pero fue lo suficientemente justa. : D ¡Gracias por tu tiempo!
Max Langhof