Réponse à la question
Historique : Introduit dans C++11, std::initializer_list a été conçu pour combler le fossé entre l'initialisation agrégée de style C et les constructeurs de conteneurs C++ modernes. Il est implémenté comme un agrégat léger contenant deux pointeurs (ou un pointeur et une taille) référencant un tableau d'éléments const généré par le compilateur. Ce design privilégie le zéro-coût pour le passage de listes littérales à des fonctions comme le constructeur de std::vector.
Le problème : Le tableau sous-jacent est un objet temporaire dont la durée de vie est liée à l'expression complète dans laquelle le std::initializer_list est créé. Lorsqu'une classe stocke le std::initializer_list lui-même plutôt que de copier son contenu, le membre conserve simplement des pointeurs vers de la mémoire de pile désallouée. Tout accès ultérieur crée un comportement indéfini, se manifestant par des données corrompues ou des plantages difficiles à reproduire.
La solution : Ne jamais stocker std::initializer_list en tant que membre de classe ; au lieu de cela, copier les éléments avec empressement dans un conteneur propriétaire comme std::vector ou std::array. Si le zéro-copie est essentiel, utiliser std::span (C++20) avec un stockage géré de manière externe, ou accepter la plage via des itérateurs. Cela garantit que les données survivent à l'appel du constructeur et restent valides durant la durée de vie de l'objet.
class Bad { std::initializer_list<int> list_; public: Bad(std::initializer_list<int> list) : list_(list) {} // DANGER int sum() const { int s = 0; for (int i : list_) s += i; // UB : pointeurs pendants return s; } }; class Good { std::vector<int> vec_; public: Good(std::initializer_list<int> list) : vec_(list) {} // Sûr : copie des données int sum() const { return std::accumulate(vec_.begin(), vec_.end(), 0); } };
Situation de la vie
Nous avons rencontré cela dans un chargeur de configuration de trading haute fréquence où une classe MarketConfig acceptait des niveaux de prix par défaut via une liste d'initialisation dans son constructeur pour prendre en charge une syntaxe telle que MarketConfig cfg{{1.0, 2.0, 3.0}}. Un développeur junior a stocké le std::initializer_list<double> directement en tant que membre pour "éviter l'allocation sur le tas", dans l'intention d'itérer sur les niveaux plus tard lors du traitement des paquets.
Une solution proposée consistait à stocker une const std::vector<double>& passée par l'appelant. Cela supprimerait les copies si l'appelant maintenait la durée de vie du vecteur, mais cela violait l'encapsulation et obligeait les appelants à gérer le stockage persistant pour des listes temporaires. Une autre option impliquait d'utiliser std::array<double, N> comme paramètre de modèle, mais cela nécessitait de connaître le nombre de niveaux à la compilation, ce qui était impossible puisque les configurations étaient chargées dynamiquement à partir de superpositions JSON.
L'approche choisie était de copier la liste d'initialisation dans un membre std::vector<double> immédiatement lors de la construction. Bien que cela entraîne une seule allocation et une copie des données des niveaux, cela garantissait la sécurité et l'immuabilité de l'état de la configuration. Après ce changement, les plantages sporadiques dans les environnements de simulation de production ont disparu, et Valgrind ne signalait plus "utilisation d'une valeur non initialisée de taille 8" lors de l'agrégation des niveaux.
Ce que les candidats manquent souvent
Pourquoi le fait de lier un std::initializer_list à une référence const n'empêche-t-il pas le tableau sous-jacent de devenir suspendu lorsqu'il est stocké dans un membre ?
La norme précise que le tableau de soutien d'un std::initializer_list est un temporaire dont la durée de vie est prolongée uniquement par l'objet initializer_list lui-même étant lié à une référence dans le cadre actuel. Lorsque vous passez un std::initializer_list par valeur à un constructeur, le tableau temporaire vit jusqu'à ce que le constructeur retourne ; copier la liste dans un membre ne fait que dupliquer la paire de pointeurs. Par conséquent, le membre pointe vers un espace de pile récupéré une fois l'expression de construction terminée, quelle que soit la façon dont l'argument original a été lié.
Comment la règle "le constructeur de liste d'initialisation gagne" interagit-elle avec l'ensemble de surcharge de constructeur de std::vector et pourquoi std::vector<int>(5, 10) diffère-t-il de std::vector<int>{5, 10} ?
Lors de la résolution de surcharge pour l'initialisation de liste directe (accolades), C++ priorise les constructeurs prenant std::initializer_list par rapport à d'autres constructeurs si la liste des arguments peut être implicitement convertie au type d'élément de la liste. Pour std::vector<int>, {5, 10} sélectionne le constructeur initializer_list<int>, créant un vecteur de deux éléments (5 et 10). En revanche, les parenthèses (5, 10) sélectionnent le constructeur size_t, const int&, créant un vecteur de cinq éléments initialisés à 10. Les candidats manquent souvent que cette priorité s'applique même lorsque le constructeur non-liste serait un meilleur choix selon les règles normales de résolution de surcharge.
Les fonctions constexpr peuvent-elles retourner un std::initializer_list en toute sécurité, et si oui, sous quelles contraintes de durée de stockage ?
Bien que les fonctions constexpr puissent retourner std::initializer_list, le tableau sous-jacent possède toujours une durée de stockage automatique si la fonction est invoquée à l'exécution. Si la fonction est invoquée dans un contexte d'expression constante, le tableau est généralement stocké dans une mémoire statique en lecture seule, ce qui le rend sûr. Cependant, retourner un std::initializer_list d'une fonction constexpr appelée avec des arguments d'exécution entraîne des pointeurs pendants une fois que la portée de la fonction se termine, exactement comme avec les fonctions non-constexpr. Les candidats confondent souvent constexpr avec "stockage statique" et supposent à tort que la liste retournée est toujours valide indéfiniment.