Ответ на вопрос
История: Введенный в C++11, std::initializer_list был разработан для устранения разрыва между инициализацией агрегатов в стиле C и современными конструкторами контейнеров C++. Он реализован как легковесный агрегат, содержащий два указателя (или указатель и размер), ссылающиеся на массив const элементов, сгенерированный компилятором. Этот дизайн приоритетно обеспечивает нулевые накладные расходы на передачу литеральных списков в функции, такие как конструктор std::vector.
Проблема: Основной массив является временным объектом, чей срок жизни связан с полной экспрессией, в которой создается std::initializer_list. Когда класс хранит сам std::initializer_list, а не копирует его содержимое, член просто сохраняет указатели на освобожденную стековую память. Любая последующая попытка доступа приводит к неопределенному поведению, проявляющемуся как мусорные данные или сбои, которые трудно воспроизвести.
Решение: Никогда не храните std::initializer_list в качестве члена класса; вместо этого заранее скопируйте элементы в контейнер-владелец, такой как std::vector или std::array. Если нулевая копия является критически важной, используйте std::span (C++20) с внешне управляемым хранилищем или принимайте диапазон через итераторы. Это гарантирует, что данные переживут вызов конструктора и останутся допустимыми на весь срок жизни объекта.
class Bad { std::initializer_list<int> list_; public: Bad(std::initializer_list<int> list) : list_(list) {} // ОПАСНО int sum() const { int s = 0; for (int i : list_) s += i; // НП: висячие указатели return s; } }; class Good { std::vector<int> vec_; public: Good(std::initializer_list<int> list) : vec_(list) {} // Безопасно: копирует данные int sum() const { return std::accumulate(vec_.begin(), vec_.end(), 0); } };
Ситуация из жизни
Мы столкнулись с этим в загрузчике конфигурации высокочастотной торговли, где класс MarketConfig принимал стандартные уровни цен через список инициализации в своем конструкторе, чтобы поддерживать такой синтаксис, как MarketConfig cfg{{1.0, 2.0, 3.0}}. Младший разработчик хранил std::initializer_list<double> непосредственно как член, чтобы "избежать аллокации в куче", намереваясь итерировать уровни позже во время обработки пакетов.
Одно из предлагаемых решений заключалось в том, чтобы хранить const std::vector<double>&, переданный вызывающим. Это устранило бы копии, если вызывающий поддерживал бы срок жизни вектора, но нарушало инкапсуляцию и заставляло вызывающих управлять постоянным хранилищем для временных списков. Другой вариант включал использование std::array<double, N> в качестве параметра шаблона, но это требовало знания числа уровней на этапе компиляции, что было невозможно, так как конфигурации загружались динамически из наложений JSON.
Выбранным подходом было немедленно скопировать список инициализации в член std::vector<double> сразу после создания. Хотя это повлекло за собой единичное выделение и копирование данных уровня, это гарантировало безопасность и неизменяемость состояния конфигурации. После изменения случайные сбои в производственных симуляционных средах исчезли, и Valgrind больше не сообщал о "использовании неинициализированного значения размера 8" во время агрегации уровней.
Что часто упускают кандидаты
Почему связывание std::initializer_list с const-ссылкой не предотвращает висячие указатели underlying массива, когда он хранится в члене?
Стандарт уточняет, что базовый массив std::initializer_list является временным, срок жизни которого продлевается только объектом initializer_list, связывающимся с ссылкой в текущей области видимости. Когда вы передаете std::initializer_list по значению в конструктор, временный массив живет до тех пор, пока конструктор не вернется; копирование списка в член просто дублирует пару указателей. Следовательно, член указывает на освобожденное пространство стека, как только заканчивается выражение создания, независимо от того, как была связана оригинальная аргументация.
Как правило "конструктор списка инициализации победит" взаимодействует с набором перегрузок конструктора std::vector, и почему std::vector<int>(5, 10) отличается от std::vector<int>{5, 10}?
Во время разрешения перегрузок для инициализации с прямым списком (фигурные скобки), C++ отдает приоритет конструкторам, принимающим std::initializer_list, над другими конструкторами, если список аргументов может быть неявно преобразован в тип элемента списка. Для std::vector<int>, {5, 10} выбирает конструктор initializer_list<int>, создавая вектор из двух элементов (5 и 10). В отличие от этого, скобки (5, 10) выбирают конструктор size_t, const int&, создавая вектор из пяти элементов, инициализированных значением 10. Кандидаты часто упускают, что этот приоритет применяется даже тогда, когда не спициальный конструктор был бы лучшим соответствием по нормальным правилам разрешения перегрузки.
Могут ли constexpr функции безопасно возвращать std::initializer_list, и если да, то при каких ограничениях на срок хранения?
Хотя constexpr функции могут возвращать std::initializer_list, базовый массив по-прежнему имеет автоматическую продолжительность хранения, если функция вызывается во время выполнения. Если функция вызывается в контексте постоянного выражения, массив обычно хранится в статической памяти только для чтения, что делает его безопасным. Однако возврат std::initializer_list из функции constexpr, вызванной с аргументами времени выполнения, приводит к висячим указателям, как и в случае с не-constexpr функциями. Кандидаты часто путают constexpr с "статическим хранилищем" и ошибочно предполагают, что возвращаемый список всегда действителен бесконечно.