Error de conversión implícito de la lista del inicializador


Considere el fragmento:

#include <unordered_map>

void foo(const std::unordered_map<int,int> &) {}

int main()
{
        foo({});
}

Esto falla con GCC 4.9.2 con el mensaje:

map2.cpp:7:19: error: converting to ‘const std::unordered_map<int, int>’ from initializer list would use explicit constructor ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::unordered_map(std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::size_type, const hasher&, const key_equal&, const allocator_type&) [with _Key = int; _Tp = int; _Hash = std::hash<int>; _Pred = std::equal_to<int>; _Alloc = std::allocator<std::pair<const int, int> >; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::size_type = long unsigned int; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::hasher = std::hash<int>; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::key_equal = std::equal_to<int>; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::allocator_type = std::allocator<std::pair<const int, int> >]’

Probando con otras implementaciones de compiladores / bibliotecas:

  • GCC
  • Clang 3.5 con libstdc++ falla con un mensaje similar,
  • Clang 3.5 con libc++ acepta esto,
  • ICC 15.algo acepta esto (no está seguro de qué biblioteca estándar está usando).

Un par de cosas más desconcertantes puntos:

  • reemplazar std::unordered_map con std::map hace que el error desaparezca,
  • reemplazar foo({}) con foo foo({{}}) también hace que el error desaparezca.

Además, reemplazar {} con una lista de inicializadores no vacía funciona como se espera en todos los casos.

Así que mis preguntas principales son:

  • ¿quién está aquí? ¿Está bien formado el código anterior?
  • ¿Qué hace la sintaxis con llaves dobles foo({{}}) exactamente para hacer que el error vaya ¿lejos?

EDIT se han corregido un par de errores tipográficos.

Author: bluescarni, 2014-11-15

2 answers

La sintaxis de inicialización indirecta con una lista de inicio entre corchetes su código está utilizando se llama inicialización de lista de copia.

El procedimiento de resolución de sobrecarga seleccionando el mejor constructor viable para ese caso se describe en la siguiente sección del Estándar C++:

§ 13.3.1.7 Inicialización por lista-inicialización [over.match.list]

  1. Cuando los objetos de tipo de clase no agregado T se inicializan en una lista (8.5.4), se sobrecarga resolución selecciona el constructor en dos fases:

    - Inicialmente, las funciones candidatas son los constructores inicializador-lista (8.5.4) de la clase T y la lista de argumentos consiste en la lista inicializador como un solo argumento.

    - Si no se encuentra ningún constructor de inicializador-list viable, se vuelve a realizar la resolución de sobrecarga, donde las funciones candidatas son todos los constructores de la clase T y la lista de argumentos consiste en los elementos del inicializador lista.

Si la lista inicializador no tiene elementos y T tiene un constructor predeterminado, se omite la primera fase. En copy-list-initialization, si se elige un constructor explícito, la inicialización está mal formada. [Nota: Esto difiere de otras situaciones (13.3.1.3, 13.3.1.4), donde solo se consideran constructores de conversión para la inicialización de copia. Esta restricción solo se aplica si esta inicialización es parte del resultado final de la resolución de sobrecarga. - nota final ].

De acuerdo con eso, un inicializador-list-constructor (el que se puede llamar con un solo argumento que coincida con el parámetro del constructor de tipo std::initializer_list<T>) generalmente se prefiere a otros constructores, pero no si un constructor por defecto está disponible , y la lista de inicio entre corchetes utilizada para inicialización de lista está vacío .

Lo que es importante aquí, el conjunto de constructores de la biblioteca estándar los contenedores han cambiado entre C++11 y C++14 debido a Cuestión del Grupo de Trabajo 2193. En el caso de std::unordered_map, por el bien de nuestro análisis, estamos interesados en la siguiente diferencia:

C++11:

explicit unordered_map(size_type n = /* impl-defined */,
                     const hasher& hf = hasher(),
                     const key_equal& eql = key_equal(),
                     const allocator_type& alloc = allocator_type());

unordered_map(initializer_list<value_type> il,
            size_type n = /* impl-defined */,
            const hasher& hf = hasher(),
            const key_equal& eql = key_equal(),
            const allocator_type& alloc = allocator_type());

C++14:

unordered_map();

explicit unordered_map(size_type n,
                     const hasher& hf = hasher(),
                     const key_equal& eql = key_equal(),
                     const allocator_type& alloc = allocator_type());

unordered_map(initializer_list<value_type> il,
            size_type n = /* impl-defined */,
            const hasher& hf = hasher(),
            const key_equal& eql = key_equal(),
            const allocator_type& alloc = allocator_type());

En otras palabras, hay un constructor predeterminado diferente (el que se puede llamar sin argumentos) dependiendo del estándar del lenguaje (C++11 / C++14), y, lo que es crucial, el constructor predeterminado en C++14 ahora se hace non-explicit.

Ese cambio fue introducido para que uno pueda decir:{[32]]}

std::unordered_map<int,int> m = {};

O:

std::unordered_map<int,int> foo()
{
    return {};
}

Que son semánticamente equivalentes a su código (pasando {} como argumento de una llamada a una función para inicializar std::unordered_map<int,int>).

Es decir, en el caso de una biblioteca conforme a C++11, el error es esperado , ya que el constructor seleccionado (predeterminado) es explicit, por lo tanto el código es mal formado :

explicit unordered_map(size_type n = /* impl-defined */,
                     const hasher& hf = hasher(),
                     const key_equal& eql = key_equal(),
                     const allocator_type& alloc = allocator_type());

En caso de Biblioteca conforme a C++14, el error es no se espera , ya que el constructor seleccionado (predeterminado) es no explicit, y el código es bien formado :

unordered_map();

Como tal, el comportamiento diferente que se encuentra está únicamente relacionado con la versión de libstdc++ y libc++ que está utilizando con diferentes compiladores/opciones de compilador.


Reemplazar std::unordered_map por std::map hace que el error desaparezca. ¿Por qué?

Sospecho que es solo porque std::map en la versión libstdc++ que está utilizando ya se actualizó para C++14.


Reemplazar foo({}) con foo({{}}) también hace que el error desaparezca. ¿Por qué?

Porque ahora esto es copy-list-initialization {{}} con un no vacío braced-init-list (es decir, tiene un elemento dentro, inicializado con una lista vacía braced-init {}), así que la regla de la primera fase de § 13.3.1.7 [sobre.coincidir.list] / p1 (citado antes) que prefiere un inicializador -list-constructor a otros se aplica. Ese constructor no es explicit, por lo tanto la llamada es bien formada.


Reemplazar {} con una lista de inicializadores no vacía funciona como se espera en todos los casos. ¿Por qué?

Igual que arriba, la resolución de sobrecarga termina con la primera fase de § 13.3.1.7 [terminado.coincidir.list] / p1.

 34
Author: Piotr Skotnicki,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-13 22:01:46

La inicialización de lista para referencias se define como sigue, [dcl.init.list] / 3:

De lo contrario, si T es un tipo de referencia, un prvalue temporal del tipo referenciado por T es copy-list-initialized o direct-list-initialized, dependiendo del tipo de inicialización para la referencia, y el la referencia está vinculada a ese temporal.

Así que su código falla porque

std::unordered_map<int,int> m = {};

Falla. La inicialización de lista para este caso se cubre a través de esta bala de [dcl.init.list] / 3:

De lo contrario, si la lista inicializadora no tiene elementos y T es una clase escriba con un constructor predeterminado, el objeto se inicializa por valor.

Entonces se llamará al constructor predeterminado del objeto1.
Ahora a los bits cruciales: En C++11, unordered_map tenía este constructor predeterminado2:

explicit unordered_map(size_type n = /* some value */ ,
                       const hasher& hf = hasher(),
                       const key_equal& eql = key_equal(),
                       const allocator_type& a = allocator_type());

Claramente, llamar a este constructor explicit a través de la inicialización de la lista de copia está mal formado, [sobre.coincidir.lista]:

En copy-list-initialization, si se elige un constructor explicit, la inicialización está mal formada.

Desde que C++14 unordered_map declara un constructor predeterminado que no es explícito:

unordered_map();

Así que una implementación de biblioteca estándar de C++14 debería compilar esto sin problemas. Presumiblemente libc++ ya está actualizado, pero libstdc++ se está quedando atrás.


1) [dcl.init] / 7:

A valor de inicialización un objeto de tipo T significa:
si T es un tipo de clase (posiblemente calificado para CV) (Cláusula 9) con un usuario proporcionado constructor (12.1), entonces el constructor por defecto para T es llamado […];

2) [clase.ctor] / 4:

Un constructor predeterminado para una clase X es un constructor de la clase X que se puede llamar sin un argumento.

 4
Author: Columbo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-15 18:51:08