web-dev-qa-db-fra.com

Échec de conversion implicite de la liste d'initialisation

Considérez l'extrait de code:

#include <unordered_map>

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

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

Cela échoue avec GCC 4.9.2 avec le message:

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> >]’

Test avec d'autres implémentations de compilateur/bibliothèque:

  • GCC <4,9 accepte cela sans se plaindre,
  • Clang 3.5 avec libstdc ++ échoue avec un message similaire,
  • Clang 3.5 avec libc ++ accepte cela,
  • ICC 15. quelque chose accepte cela (je ne sais pas quelle bibliothèque standard il utilise).

Quelques autres points déconcertants:

  • remplacer std::unordered_map par std::map fait disparaître l'erreur,
  • remplacer foo({}) par foo foo({{}}) fait également disparaître l'erreur.

De plus, le remplacement de {} Par une liste d'initialisation non vide fonctionne comme prévu dans tous les cas.

Mes principales questions sont donc:

  • qui est ici? Le code ci-dessus est-il bien formé?
  • Que fait exactement la syntaxe à double accolade foo({{}}) pour que l'erreur disparaisse?

MODIFIER correction de quelques fautes de frappe.

41
bluescarni

La syntaxe d'initialisation indirecte avec un braced-init-list que votre code utilise s'appelle copy-list-initialization.

La procédure de résolution de surcharge sélectionnant le meilleur constructeur viable pour ce cas est décrite dans la section suivante de la norme C++:

§ 13.3.1.7 Initialisation par initialisation de liste [over.match.list]

  1. Lorsque des objets de type de classe non agrégé T sont initialisés par liste (8.5.4), la résolution de surcharge sélectionne le constructeur en deux phases:

    - Initialement, les fonctions candidates sont les constructeurs de listes d'initialiseurs (8.5.4) de la classe T et la liste d'arguments se compose de la liste d'initialiseurs comme argument unique.

    - Si aucun constructeur de liste d'initialisation viable n'est trouvé, la résolution de surcharge est exécutée à nouveau, où les fonctions candidates sont tous les constructeurs de la classe T et la liste d'arguments se compose des éléments de la liste d'initialisation.

Si la liste d'initialisation n'a aucun élément et que T a un constructeur par défaut, la première phase est omise. Dans la copie-liste-initialisation, si un constructeur explicite est choisi, l'initialisation est mal formée. [ Remarque: Cela diffère des autres situations (13.3.1.3, 13.3.1.4), où seuls les constructeurs de conversion sont pris en compte pour l'initialisation de la copie. Cette restriction s'applique uniquement si cette initialisation fait partie du résultat final de la résolution de surcharge. — note de fin].

Selon cela, un initializer-list-constructor (celui appelable avec un seul argument correspondant au paramètre du constructeur de type std::initializer_list<T>) Est généralement préféré aux autres constructeurs, mais pas si un constructeur par défaut est disponible , et le braced-init-list utilisé pour list-initialization est vide .

Ce qui est important ici, l'ensemble des constructeurs des conteneurs de la bibliothèque standard a changé entre C++ 11 et C++ 14 en raison de LWG problème 2193 . Dans le cas de std::unordered_map, Dans l'intérêt de notre analyse, nous nous intéressons à la différence suivante:

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 d'autres termes, il existe un autre constructeur par défaut (celui qui peut être appelé sans arguments) en fonction de la norme de langage (C++ 11/C++ 14), et ce qui est crucial, le constructeur par défaut en C++ 14 est désormais non -explicit.

Ce changement a été introduit pour que l'on puisse dire:

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

ou:

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

qui sont tous deux sémantiquement équivalents à votre code (en passant {} comme argument d'un appel de fonction pour initialiser std::unordered_map<int,int>).

Autrement dit, dans le cas d'une bibliothèque conforme à C++ 11, l'erreur est attendue , car le constructeur sélectionné (par défaut) est explicit, donc le code est mal formé:

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

Dans le cas d'une bibliothèque conforme à C++ 14, l'erreur est non attendue , car le constructeur sélectionné (par défaut) est pas explicit, et le code est bien formé:

unordered_map();

En tant que tel, le comportement différent que vous rencontrez est uniquement lié à la version de libstdc ++ et libc ++ que vous utilisez avec différents compilateurs/options de compilateur.


Le remplacement de std::unordered_map Par std::map Fait disparaître l'erreur. Pourquoi?

Je soupçonne que c'est simplement parce que std::map Dans la version libstdc ++ que vous utilisez a déjà été mis à jour pour C++ 14.


Le remplacement de foo({}) par foo({{}}) fait également disparaître l'erreur. Pourquoi?

Parce que maintenant c'est copie-liste-initialisation{{}} Avec un non vide braced-init-list (c'est-à-dire qu'il contient un élément à l'intérieur, initialisé avec un braced-init-list{}), donc la règle de la première phase du § 13.3.1.7 [over.match.list]/p1 (cité précédemment) qui préfère un initializer-list-constructor aux autres est appliqué. Ce constructeur n'est pas explicit, donc l'appel est bien formé.


Le remplacement de {} Par une liste d'initialisation non vide fonctionne comme prévu dans tous les cas. Pourquoi?

Comme ci-dessus, la résolution de surcharge se retrouve avec la première phase du § 13.3.1.7 [over.match.list]/p1.

36
Piotr Skotnicki

L'initialisation de liste pour les références est définie comme suit, [dcl.init.list]/3:

Sinon, si T est un type de référence, une valeur temporaire du type référencé par T est copie-liste-initialisée ou directe-liste-initialisée, selon le type d'initialisation de la référence et la référence est liée à celle temporaire.

Votre code échoue donc car

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

échoue. L'initialisation de liste pour ce cas est couverte par cette puce de [dcl.init.list]/3:

Sinon, si la liste d'initialisation ne contient aucun élément et que T est un type de classe avec un constructeur par défaut, l'objet est initialisé en valeur.

Ainsi, le constructeur par défaut de l'objet sera appelé1.
Passons maintenant aux bits cruciaux: en C++ 11, unordered_map avait ce constructeur par défaut2:

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

De toute évidence, appeler ce constructeur explicit par le biais de la copie-liste-initialisation est mal formé, [over.match.list]:

Dans copy-list-initialization, si un constructeur explicit est choisi, l'initialisation est mal formée.

Depuis C++ 14 unordered_map déclare un constructeur par défaut non explicite:

unordered_map();

Une implémentation de bibliothèque standard C++ 14 devrait donc compiler cela sans problème. Vraisemblablement, libc ++ est déjà mis à jour, mais libstdc ++ est à la traîne.


1)

To value-initialize un objet de type T signifie:
- si T est un type de classe (éventuellement qualifié par cv) (article 9) avec un constructeur fourni par l'utilisateur (12.1), alors le constructeur par défaut pour T est appelé […];

2) [class.ctor]/4:

Un constructeur par défaut pour une classe X est un constructeur de classe X qui peut être appelé sans argument.

5
Columbo