16.7 – Список инициализаторов std::initializer_list
Рассмотрим фиксированный массив чисел int
в C++:
int array[5];
Если мы хотим инициализировать этот массив значениями, мы можем сделать это напрямую с помощью синтаксиса списка инициализаторов:
#include <iostream>
int main()
{
int array[] { 5, 4, 3, 2, 1 }; // список инициализаторов
for (auto i : array)
std::cout << i << ' ';
return 0;
}
Этот код печатает:
5 4 3 2 1
Это также работает и для динамически размещаемых массивов:
#include <iostream>
int main()
{
auto *array{ new int[5]{ 5, 4, 3, 2, 1 } }; // список инициализаторов
for (int count{ 0 }; count < 5; ++count)
std::cout << array[count] << ' ';
delete[] array;
return 0;
}
В предыдущем уроке мы представили концепцию контейнерных классов и показали пример класса IntArray
, который содержит массив чисел int
:
#include <cassert> // для assert()
#include <iostream>
class IntArray
{
private:
int m_length{};
int *m_data{};
public:
IntArray() = default;
IntArray(int length):
m_length{ length },
m_data{ new int[length]{} }
{
}
~IntArray()
{
delete[] m_data;
// здесь нам не нужно устанавливать m_data в значение null
// или m_length в 0, так как объект в любом случае будет
// уничтожен сразу после этой функции
}
int& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
int main()
{
// Что произойдет, если мы попытаемся использовать список
// инициализаторов с этим контейнерным классом?
IntArray array { 5, 4, 3, 2, 1 }; // эта строка не компилируется
for (int count{ 0 }; count < 5; ++count)
std::cout << array[count] << ' ';
return 0;
}
Этот код не компилируется, потому что у класса IntArray
нет конструктора, который знает, что делать со списком инициализаторов. В результате нам остается инициализировать элементы массива по отдельности:
int main()
{
IntArray array(5);
array[0] = 5;
array[1] = 4;
array[2] = 3;
array[3] = 2;
array[4] = 1;
for (int count{ 0 }; count < 5; ++count)
std::cout << array[count] << ' ';
return 0;
}
Это не так уж и хорошо.
Инициализация класса с помощью std::initializer_list
Когда компилятор видит список инициализаторов, он автоматически преобразует его в объект типа std::initializer_list
. Следовательно, если мы создадим конструктор, который принимает параметр типа std::initializer_list
, мы сможем создавать объекты, используя список инициализаторов в качестве входных данных.
std::initializer_list
находится в заголовке <initializer_list>
.
Есть несколько вещей, которые нужно знать о std::initializer_list
. Подобно std::array
или std::vector
, если вы сразу не инициализируете std::initializer_list
, то с помощью угловых скобок вы должны сказать объекту std::initializer_list
, какой тип данных содержит список. Следовательно, вы почти никогда не увидите простой std::initializer_list
. Вместо этого вы увидите что-то вроде std::initializer_list<int>
или std::initializer_list<std::string>
.
Во-вторых, std::initializer_list
имеет (не совсем правильную) функцию size()
, которая возвращает количество элементов в списке. Она полезна, когда нам нужно знать длину переданного списка.
Давайте посмотрим, как обновить наш класс IntArray
с помощью конструктора, который принимает std::initializer_list
.
#include <cassert> // для assert()
#include <initializer_list> // для std::initializer_list
#include <iostream>
class IntArray
{
private:
int m_length{};
int *m_data{};
public:
IntArray() = default;
IntArray(int length) :
m_length{ length },
m_data{ new int[length]{} }
{
}
// позволяет инициализировать IntArray с помощью списка инициализаторов
IntArray(std::initializer_list<int> list) :
// используем делегирующий конструктор для начальной установки массива
IntArray(static_cast<int>(list.size()))
{
// Теперь инициализируем наш массив из списка
int count{ 0 };
for (auto element : list)
{
m_data[count] = element;
++count;
}
}
~IntArray()
{
delete[] m_data;
// здесь нам не нужно устанавливать m_data в значение null
// или m_length в 0, так как объект в любом случае будет
// уничтожен сразу после этой функции
}
// чтобы избежать поверхностного копирования
IntArray(const IntArray&) = delete;
// чтобы избежать поверхностного копирования
IntArray& operator=(const IntArray& list) = delete;
int& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
int main()
{
IntArray array{ 5, 4, 3, 2, 1 }; // список инициализаторов
for (int count{ 0 }; count < array.getLength(); ++count)
std::cout << array[count] << ' ';
return 0;
}
Этот код дает ожидаемый результат:
5 4 3 2 1
Всё работает! Теперь давайте рассмотрим этот код более подробно.
Вот наш конструктор IntArray
, который принимает std::initializer_list<int>
.
// позволяет инициализировать IntArray с помощью списка инициализаторов
IntArray(std::initializer_list<int> list) :
// используем делегирующий конструктор для начальной установки массива
IntArray(static_cast<int>(list.size()))
{
// Теперь инициализируем наш массив из списка
int count{ 0 };
for (auto element : list)
{
m_data[count] = element;
++count;
}
}
Строка 2: Как отмечалось выше, мы должны использовать угловые скобки, чтобы обозначить, какой тип элементов мы ожидаем внутри списка. В этом случае, поскольку это IntArray
, мы ожидаем, что список будет заполнен значениями int
. Обратите внимание, что мы не передаем список по константной ссылке. Как и std::string_view
, std::initializer_list
очень легкий, и его копии обычно дешевле, чем косвенное обращение.
Строка 4: Мы делегируем выделение памяти для IntArray
другому конструктору через делегирующий конструктор (для уменьшения избыточного кода). Этот другой конструктор должен знать длину массива, поэтому мы передаем ему значение list.size()
, которое содержит количество элементов в списке. Обратите внимание, что list.size()
возвращает size_t
(без знака), поэтому здесь нам нужно привести это значение к signed int
. Мы используем прямую инициализацию, а не инициализацию с фигурными скобками, потому что для инициализации с фигурными скобками предпочтительны конструкторы со списком инициализаторов. Хотя конструктор будет разрешен правильно, для инициализации классов с конструкторами со списком инициализаторов безопаснее использовать прямую инициализацию, если мы не хотим использовать конструктор со списком инициализаторов.
Тело конструктора зарезервировано для копирования элементов из списка в наш класс IntArray
. По какой-то необъяснимой причине std::initializer_list
не предоставляет доступ к элементам списка через индексирование (operator[]
). Об этом упущении много раз сообщали комитету по стандартам.
Однако есть простые способы обойти отсутствие индексов. Самый простой способ – использовать здесь цикл for
-each. Цикл for
-each проходит по всем элементам списка инициализаторов, и мы можем вручную скопировать эти элементы в наш внутренний массив.
Одно предостережение: списки инициализаторов всегда будут отдавать предпочтение соответствующему конструктору с initializer_list
по сравнению с другими потенциально подходящими конструкторами. Таким образом, это определение переменной:
IntArray array { 5 };
будет соответствовать IntArray(std::initializer_list<int>)
, а не IntArray(int)
. Если после определения конструктора со списком вы хотите сопоставить этот код с IntArray(int)
, вам нужно будет использовать копирующую или прямую инициализацию. То же самое происходит с std::vector
и другими контейнерными классами, которые имеют конструктор со списком и конструктор с параметром аналогичного типа.
// Вызывает std::vector::vector(std::vector::size_type),
// 5 элементов, инициализированных значениями: 0 0 0 0 0
std::vector<int> array(5);
// Вызывает std::vector::vector(std::initializer_list<int>),
// 1 элемент: 5
std::vector<int> array{ 5 };
Присваивание объектам класса с использованием std::initializer_list
Вы также можете использовать std::initializer_list
для присваивания новых значений объектам класса, перегрузив оператор присваивания, чтобы он принимал параметр std::initializer_list
. Это работает аналогично предыдущему случаю. Пример того, как это сделать, будет показан в приведенном ниже решении на вопрос теста.
Обратите внимание, что если вы реализуете конструктор, который принимает std::initializer_list
, вы должны убедиться, что выполняете хотя бы одно из следующих действий:
- предоставить перегруженный оператор присваивания со списком инициализаторов;
- предоставить правильный оператор присваивания для глубокого копирования.
Почему? Рассмотрим приведенный выше класс (который не имеет перегруженного присваивания со списком инициализаторов или копирующего присваивания) вместе со следующей инструкцией:
array = { 1, 3, 5, 7, 9, 11 }; // перезаписываем элементы массива элементами из списка
Во-первых, компилятор заметит, что функции присваивания, принимающей std::initializer_list
, не существует. Затем он будет искать другие функции присваивания, которые он сможет использовать, и обнаружит неявно предоставленный оператор копирующего присваивания. Однако эту функцию можно использовать только в том случае, если она может преобразовать список инициализаторов в IntArray
. Поскольку {1, 3, 5, 7, 9, 11}
является списком std::initializer_list
, компилятор будет использовать конструктор со списком инициализаторов для преобразования списка инициализаторов во временный массив IntArray
. Затем он вызовет неявный оператор присваивания, который выполнит поверхностное копирование временного массива IntArray
в наш объект массива.
На этом этапе и m_data
временного IntArray
, и array->m_data
указывают на один и тот же адрес (из-за поверхностного копирования). Вы уже можете догадаться, к чему это идет.
В конце инструкции присваивания временный массив IntArray
уничтожается. Это вызывает деструктор, который удаляет m_data
временного массива IntArray
. Это оставляет нашу переменную массива с висячим указателем m_data
. Когда вы попытаетесь использовать array->m_data
для чего-либо (в том числе, когда массив выходит за пределы области видимости, а деструктор переходит к удалению m_data
), вы получите неопределенное поведение (и, возможно, сбой).
Правило
Если вы обеспечиваете создание объекта с использованием списка инициализаторов, неплохо также предоставить присваивание с использованием списка инициализаторов.
Резюме
Реализация конструктора, который принимает параметр std::initializer_list
, позволяет нам использовать инициализацию списком с нашими пользовательскими классами. Мы также можем использовать std::initializer_list
для реализации других функций, которые должны использовать список инициализаторов, например, оператора присваивания.
Небольшой тест
Вопрос 1
Используя приведенный выше класс IntArray
, реализуйте перегруженный оператор присваивания, который принимает список инициализаторов.
Должен запуститься следующий код:
int main()
{
IntArray array { 5, 4, 3, 2, 1 }; // список инициализаторов
for (int count{ 0 }; count < array.getLength(); ++count)
std::cout << array[count] << ' ';
std::cout << '\n';
array = { 1, 3, 5, 7, 9, 11 };
for (int count{ 0 }; count < array.getLength(); ++count)
std::cout << array[count] << ' ';
std::cout << '\n';
return 0;
}
Эта программа должна напечатать:
5 4 3 2 1
1 3 5 7 9 11
Ответ
#include <cassert> // для assert() #include <initializer_list> // для std::initializer_list #include <iostream> class IntArray { private: int m_length {}; int *m_data {}; public: IntArray() = default; IntArray(int length) : m_length{ length }, m_data{ new int[length]{} } { } // позволяет инициализировать IntArray с помощью инициализацию списком IntArray(std::initializer_list<int> list) : // используем делегирующий конструктор для начальной установки массива IntArray(static_cast<int>(list.size())) { // Теперь инициализируем наш массив из списка int count{ 0 }; for (auto element : list) { m_data[count] = element; ++count; } } ~IntArray() { delete[] m_data; // здесь нам не нужно устанавливать m_data в значение null // или m_length в 0, так как объект в любом случае будет // уничтожен сразу после этой функции } // чтобы избежать поверхностного копирования IntArray(const IntArray&) = delete; // чтобы избежать поверхностного копирования IntArray& operator=(const IntArray& list) = delete; IntArray& operator=(std::initializer_list<int> list) { // Если новый список другого размера, размещаем его заново int length{ static_cast<int>(list.size()) }; if (length != m_length) { delete[] m_data; m_length = length; m_data = new int[length]{}; } // Теперь инициализируем наш массив из списка int count{ 0 }; for (auto element : list) { m_data[count] = element; ++count; } return *this; } int& operator[](int index) { assert(index >= 0 && index < m_length); return m_data[index]; } int getLength() const { return m_length; } }; int main() { IntArray array { 5, 4, 3, 2, 1 }; // список инициализаторов for (int count{ 0 }; count < array.getLength(); ++count) std::cout << array[count] << ' '; std::cout << '\n'; array = { 1, 3, 5, 7, 9, 11 }; for (int count{ 0 }; count < array.getLength(); ++count) std::cout << array[count] << ' '; std::cout << '\n'; return 0; }