Selon la norme C++ (spécifiquement [over.ics.list]), lors de l'initialisation par liste, le compilateur tente de faire correspondre la liste d'initialisation entre accolades avec des constructeurs acceptant std::initializer_list<T>. Cette liaison constitue une conversion d'identité (correspondance exacte), qui prévaut sur les conversions définies par l'utilisateur nécessaires pour faire correspondre les éléments individuels aux constructeurs non-initializer_list. Par conséquent, un constructeur comme Container(size_t count, T value) perd face à Container(std::initializer_list<T>) lorsqu'il est appelé avec {10, 20}, car ce dernier ne nécessite aucune conversion pour l'argument de la liste d'initialisation, indépendamment d'un rapprochement élément par élément.
Nous concevions une classe Matrix pour un moteur graphique qui fournissait à la fois un constructeur de remplissage Matrix(size_t rows, size_t cols, double val) et un constructeur de style agrégat Matrix(std::initializer_list<std::initializer_list<double>>) pour l'initialisation de tables littérales. Un développeur junior a écrit Matrix m{1080, 1920, 0.0} s'attendant à une matrice zero-initialisée de 1080x1920, mais à la place, le programme a créé une matrice 1x3 contenant les trois valeurs scalaires, provoquant un subtil plantage lors du rendu à l'exécution qui était difficile à tracer pendant les sessions de débogage.
Nous avions initialement envisagé d'imposer la syntaxe des parenthèses Matrix(1080, 1920, 0.0) pour le constructeur de remplissage afin de contourner la surcharge std::initializer_list. Cependant, cela violait la préférence de notre norme de codage pour l'initialisation uniforme C++11 et créait une API incohérente où certains constructeurs nécessitaient des parenthèses tandis que d'autres utilisaient des accolades.
Ensuite, nous avons exploré le dispatching par étiquette en ajoutant un paramètre fill_tag_t au constructeur de remplissage, obligeant effectivement les utilisateurs à écrire Matrix{fill_tag, 1080, 1920, 0.0}. Bien que cela ait clarifié l'appel, cela a encombré l'interface publique et a confondu les développeurs qui s'attendaient à des signatures de constructeur intuitives sans types d'étiquettes artificiels.
Troisièmement, nous avons tenté de restreindre le constructeur std::initializer_list pour n'activer que les accolades imbriquées via SFINAE sur le paramètre de template. Cette approche a cassé des cas d'utilisation légitimes comme Matrix{{1.0, 2.0}, {3.0, 4.0}} et a introduit une métaprogrammation de template fragile qui a augmenté les temps de compilation et la complexité des messages d'erreur.
En fin de compte, nous avons choisi d'introduire une fonction de fabrique statique Matrix::filled(rows, cols, val) et de rendre le constructeur de remplissage à trois paramètres privé, dirigeant les utilisateurs vers une syntaxe explicite pour la construction basée sur les dimensions tout en maintenant le constructeur std::initializer_list public pour la syntaxe agrégée. Cela a préservé l'initialisation par accolades intuitive pour les tables littérales sans risquer d'interprétations erronées accidentelles des arguments de dimension.
L'API refactorisée a empêché le bug original en faisant de Matrix{1080, 1920, 0.0} une erreur de compilation sans constructeur public correspondant. Les développeurs étaient désormais contraints d'utiliser soit Matrix::filled(1080, 1920, 0.0) pour les opérations de remplissage, soit Matrix{{...}} pour les listes d'initialisation, ce qui a considérablement amélioré la clarté et la sécurité du code.
Comment le compilateur classe-t-il la séquence de conversion d'une liste d'initialisation entre accolades vers un constructeur non-initializer_list par rapport à la correspondance d'identité d'un constructeur initializer_list ?
Selon les règles de résolution de surcharge de la norme C++ pour l'initialisation par liste, lier une liste d'initialisation entre accolades à un paramètre std::initializer_list<T> constitue une conversion d'identité (correspondance exacte) avec le rang le plus élevé. En revanche, faire correspondre la même liste d'initialisation entre accolades à un autre constructeur nécessite que le compilateur considère la liste comme une liste d'expressions parenthésées et effectue des conversions définies par l'utilisateur ou standard sur chaque élément. Étant donné que les conversions d'identité l'emportent sur toutes les autres séquences de conversion, le constructeur initializer_list l'emporte même si ses types d'éléments sont un moins bon match logique que ceux requis par un constructeur alternatif.
Pourquoi auto x = {1, 2, 3}; déduit std::initializer_list<int> en C++11 et C++14, tandis que auto x{1, 2, 3} devient mal formé en C++17 et plus tard ?
Avant C++17, l'initialisation par liste de copie utilisant le token = avec auto déduisait toujours std::initializer_list pour les listes d'initialisation entre accolades. Cependant, C++17 a introduit de nouvelles règles pour l'initialisation par liste directe avec auto (sans =) qui effectuent la déduction d'argument de template standard : si la liste d'initialisation entre accolades contient plusieurs éléments, la déduction échoue car auto ne peut pas représenter une std::initializer_list dans ce contexte, rendant le programme mal formé. Ce changement élimine le piège du "secret std::initializer_list" pour l'initialisation directe, mais les candidats négligent souvent que la syntaxe de copie (auto x = {...}) déduit toujours std::initializer_list même dans le C++ moderne, créant une incohérence subtile entre les styles d'initialisation.
Dans quel scénario une classe avec à la fois un constructeur initializer_list et un constructeur de template variadique peut-elle résoudre de manière ambiguë, et comment std::in_place_t peut-il les désambiguïser ?
Lorsqu'une classe fournit à la fois Container(std::initializer_list<T>) et template<typename... Args> Container(Args&&... args), le pack variadique peut correspondre aux mêmes arguments que le constructeur initializer_list via la déduction d'argument de template. Pour Container c{1, 2, 3}, les deux constructeurs sont viables : le premier via la conversion d'identité de la liste d'initialisation entre accolades, et le second via la déduction de Args comme int, int, int. Bien que le constructeur initializer_list non-template l'emporte généralement en cas d'égalité, l'ajout d'un type d'étiquette comme std::in_place_t au constructeur variadique (par exemple, Container(std::in_place_t, Args&&... args)) oblige les utilisateurs à écrire Container{std::in_place, 1, 2, 3}, garantissant que la version variadique est seulement invoquée explicitement tandis que le constructeur initializer_list gère par défaut les listes entre accolades homogènes.