На этом уроке мы рассмотрим список инициализации std::initializer_list, его использование и нюансы в языке С++.
Списки инициализации
Рассмотрим фиксированный массив целых чисел в языке C++:
1 |
int array[7]; |
Для инициализации этого массива мы можем использовать список инициализации:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { int array[7] { 7, 6, 5, 4, 3, 2, 1 }; // список инициализации for (int count=0; count < 7; ++count) std::cout << array[count] << ' '; return 0; } |
Результат:
7 6 5 4 3 2 1
Это также работает и с динамически выделенными массивами:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { int *array = new int[7] { 7, 6, 5, 4, 3, 2, 1 }; // список инициализации for (int count = 0; count < 7; ++count) std::cout << array[count] << ' '; delete[] array; return 0; } |
На предыдущем уроке мы рассматривали контейнерные классы на примере класса-массива целых чисел ArrayInt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <cassert> // для assert() class ArrayInt { private: int m_length; int *m_data; public: ArrayInt(): m_length(0), m_data(nullptr) { } ArrayInt(int length): m_length(length) { m_data = new int[length]; } ~ArrayInt() { delete[] m_data; // Нам не нужно здесь присваивать значение null для m_data или выполнять m_length = 0, так как объект будет уничтожен сразу же после выполнения этой функции } int& operator[](int index) { assert(index >= 0 && index < m_length); return m_data[index]; } int getLength() { return m_length; } }; |
Что произойдет, если мы попытаемся использовать список инициализации с этим контейнерным классом?
1 2 3 4 5 6 7 8 |
int main() { ArrayInt array { 7, 6, 5, 4, 3, 2, 1 }; // эта строка вызовет ошибку компиляции for (int count=0; count < 7; ++count) std::cout << array[count] << ' '; return 0; } |
Этот код не скомпилируется, так как класс ArrayInt не имеет конструктора, который бы знал, что делать со списком инициализации, поэтому каждый элемент нужно инициализировать в индивидуальном порядке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int main() { ArrayInt array(7); array[0] = 7; array[1] = 6; array[2] = 5; array[3] = 4; array[4] = 3; array[5] = 2; array[6] = 1; for (int count=0; count < 7; ++count) std::cout << array[count] << ' '; return 0; } |
Как-то не очень, правда?
До C++11 списки инициализации могли использоваться только со статическими или динамически выделенными массивами. Однако в C++11 появилось решение этой проблемы.
Инициализация классов через std::initializer_list
Когда компилятор C++11 видит список инициализации, то он автоматически конвертирует его в объект типа 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<int>
или std::initializer_list<std::string>
.
Во-вторых, std::initializer_list имеет функцию size(), которая возвращает количество элементов списка. Это полезно, когда нам нужно знать длину получаемого списка.
Обновим наш класс-массив ArrayInt, добавив конструктор, который принимает std::initializer_list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <iostream> #include <cassert> // для assert() #include <initializer_list> // для std::initializer_list class ArrayInt { private: int m_length; int *m_data; public: ArrayInt() : m_length(0), m_data(nullptr) { } ArrayInt(int length) : m_length(length) { m_data = new int[length]; } ArrayInt(const std::initializer_list<int> &list): // позволяем инициализацию ArrayInt через список инициализации ArrayInt(list.size()) // используем концепцию делегирования конструкторов для создания начального массива, в который будет выполняться копирование элементов { // Инициализация нашего начального массива значениями из списка инициализации int count = 0; for (auto &element : list) { m_data[count] = element; ++count; } } ~ArrayInt() { delete[] m_data; // Нам не нужно здесь присваивать значение null для m_data или выполнять m_length = 0, так как объект будет уничтожен сразу же после выполнения этой функции } int& operator[](int index) { assert(index >= 0 && index < m_length); return m_data[index]; } int getLength() { return m_length; } }; int main() { ArrayInt array { 7, 6, 5, 4, 3, 2, 1 }; // список инициализации for (int count = 0; count < array.getLength(); ++count) std::cout << array[count] << ' '; return 0; } |
Результат выполнения программы:
7 6 5 4 3 2 1
Работает! Теперь рассмотрим это всё детально.
Вот наш конструктор, который принимает std::initializer_list<int>:
1 2 3 4 5 6 7 8 9 10 11 |
ArrayInt(const std::initializer_list<int> &list): // позволяем инициализацию ArrayInt через список инициализации ArrayInt(list.size()) // используем концепцию делегирования конструкторов для создания начального массива, в который будет выполняться копирование элементов { // Инициализируем наш начальный массив значениями из списка инициализации int count = 0; for (auto &element : list) { m_data[count] = element; ++count; } } |
Строка №1: Как мы уже говорили, обязательно нужно указывать используемый тип данных в угловых скобках std::initializer_list. В этом случае, поскольку это ArrayInt, то ожидается, что список будет заполнен значениями типа int. Обратите внимание, мы передаем список по константной ссылке, дабы избежать его копирования при передаче в конструктор.
Строка №2: Мы делегируем выделение памяти для начального объекта ArrayInt, в который будем выполнять копирование элементов, другому конструктору, используя концепцию делегирующих конструкторов, чтобы сократить лишний код. Этот другой конструктор должен знать длину выделяемого объекта, поэтому мы передаем ему list.size()
, который указывает на количество элементов списка.
В теле нашего конструктора мы выполняем копирование элементов из списка инициализации в класс ArrayInt. По каким-то необъяснимым причинам std::initializer_list не предоставляет доступ к своим элементам через оператор индексации []
. Об этом много говорили, но официального решения так и не предоставили.
Тем не менее, есть способы, чтобы это обойти. Самый простой — использовать цикл foreach. Цикл foreach перебирает каждый элемент списка, и мы, таким образом, копируем каждый элемент в наш внутренний массив.
Присваивание значений и std::initializer_list
Вы также можете использовать std::initializer_list для присваивания новых значений классу, перегрузив оператор присваивания для получения std::initializer_list в качестве параметра. Пример того, как это делается, будет в тестовом задании ниже.
Обратите внимание, если вы создаете конструктор, который принимает std::initializer_list, то вы должны проследить, чтобы хоть одно из следующих действий было выполнено:
Перегрузка оператора присваивания.
Корректное глубокое копирование для оператора присваивания.
Наличие явного конструктора (с ключевым словом explicit), чтобы он не мог использоваться для неявных преобразований.
Почему? Рассмотрим вышеприведенный класс (который не имеет перегрузки оператора присваивания или копирующего присваивания) со следующим стейтментом:
1 |
array = { 1, 3, 5, 7, 9, 11 }; // перезаписываем значения array значениями из списка инициализации |
Во-первых, компилятор видит, что функции присваивания, которая принимает std::initializer_list в качестве параметра, не существует. Затем он ищет другие функции, которые он мог бы использовать, и находит неявно предоставленный копирующий оператор присваивания. Однако эта функция может использоваться, только если она сможет преобразовать список инициализации в ArrayInt, а поскольку у нас есть конструктор, который принимает std::initializer_list, и он не помечен как explicit, то компилятор будет использовать этот конструктор для преобразования списка инициализации во временный ArrayInt. Затем вызовется неявный оператор присваивания, который используется в конструкторе и который будет выполнять поверхностное копирование временного объекта ArrayInt в наш объект array
.
И тогда m_data
временного объекта ArrayInt, и m_data
объекта array
будут указывать на один и тот же адрес (из-за поверхностного копирования). Вы уже можете догадаться, к чему это приведет.
В конце стейтмента присваивания временный ArrayInt уничтожается. Вызывается деструктор, который удаляет временный m_data
класса ArrayInt. Это оставляет наш объект array
с висячим указателем m_data
. Когда мы попытаемся использовать m_data
объекта array
для любых целей (в том числе, когда массив будет выходить из области видимости и деструктору нужно будет уничтожить m_data
), то мы получим неопределенные результаты (или сбой).
Заключение
Реализация конструктора, который принимает std::initializer_list в качестве параметра (используется передача по ссылке для предотвращения копирования), позволяет нам использовать список инициализации с нашими пользовательскими классами. Мы также можем использовать std::initializer_list для реализации других функций, которым необходим список инициализации (например, для перегрузки оператора присваивания).
Тест
Используя вышеприведенный класс ArrayInt, реализуйте перегрузку оператора присваивания, который будет принимать список инициализации. Следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int main() { ArrayInt array { 7, 6, 5, 4, 3, 2, 1 }; // список инициализации for (int count = 0; count < array.getLength(); ++count) std::cout << array[count] << ' '; std::cout << '\n'; array = { 1, 4, 9, 12, 15, 17, 19, 21 }; for (int count = 0; count < array.getLength(); ++count) std::cout << array[count] << ' '; return 0; } |
Должен выдавать следующий результат:
7 6 5 4 3 2 1
1 4 9 12 15 17 19 21
Ответ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
#include <iostream> #include <cassert> // для assert() #include <initializer_list> // для std::initializer_list class ArrayInt { private: int m_length; int *m_data; public: ArrayInt() : m_length(0), m_data(nullptr) { } ArrayInt(int length) : m_length(length) { m_data = new int[length]; } ArrayInt(const std::initializer_list<int> &list) : // позволяем инициализацию ArrayInt через список инициализации ArrayInt(list.size()) // используем концепцию делегирования конструкторов для создания начального массива, в который будет выполняться копирование элементов { // Инициализируем наш начальный массив значениями из списка int count = 0; for (auto &element : list) { m_data[count] = element; ++count; } } ~ArrayInt() { delete[] m_data; // Нам не нужно здесь присваивать значение null для m_data или выполнять m_length = 0, так как объект будет уничтожен сразу же после выполнения этой функции } ArrayInt& operator=(const std::initializer_list<int> &list) { // Если новый список имеет другой размер, то перевыделяем его if (list.size() != static_cast<size_t>(m_length)) { // Удаляем все существующие элементы delete[] m_data; // Перевыделяем массив m_length = list.size(); m_data = new int[m_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() { return m_length; } }; int main() { ArrayInt array { 7, 6, 5, 4, 3, 2, 1 }; // список инициализации for (int count = 0; count < array.getLength(); ++count) std::cout << array[count] << ' '; std::cout << '\n'; array = { 1, 4, 9, 12, 15, 17, 19, 21 }; for (int count = 0; count < array.getLength(); ++count) std::cout << array[count] << ' '; return 0; } |
С std::initializer_list перестает работать uniform-инициализация.
Например, если мы хотим вызвать конструктор с длиной через uniform
Выведет 1
подскажите, что это за цикл
не понятно, что за параметры в скобках
Это цикл foreach.
а почему нельзя просто использовать вот такой конструктор:
Здравствуйте. Уместна ли такая реализация?
В целом да, только выполняется цепочка ненужных вызовов (reallocate -> erase), да и к тому же у вас не стоит проверка на равенство размеров массива и списка инициализации, зачем проводить лишние операции? Мое мнение — проще написать все ручками без лишних вызовов, чтобы весь код можно было посмотреть на месте, не такой уж это и большой объем.
Объясните, пожалуйста, почему без const в параметре перегрузки оператора присваивания первые два элемента равны нулю в новом массиве? С const все норм.
Список представляет собой r-value, поэтому для его передачи по ссылке, должна использоваться константная ссылка. Либо передавать без ссылки, через копирование. Если же убрать const, то компилятор вместо нашего перегруженного оператора попытается сделать приведение списка к ArrayInt и вызвать неявный оператор присваивания, в общем получим то, что описано в уроке выше.
Мой вариант:
Если я все правильно понял, то нет смысла в проверке "list.size() == 0", ибо список инициализации не может быть пустым. Также имеет смысл пересоздавать массив только в случае, если его размер не равен размеру list, т.е. желательна проверка, чтобы исключить лишние действия
"По каким-то необъяснимым причинам std::initializer_list не предоставляет доступ к своим элементам через оператор индексации []. Об этом много говорили, но официального решения так и не предоставили."
Не совсем точно. Покопался над этим вопросом и нашел следующее:
Доступ к элементам списка можно получить через list.begin()[]
Не совсем напрямую, но вполне себе приемлемо.
Итого получается более читаемый код с циклом for вместо foreach:
Результат:
7 6 5 4 3 2 1
Работает!
Цитата из Бьярне Страуструп "Программирование: принципы и практика с использованием С++" 2-е издание
Обратите внимание, что мы передаем initializer_list<douЬle> по значению. Это сделано сознательно и требуется правилами языка: тип initializer_list по сути является обработчиком для элементов, находящихся "где-то" (см. раздел §Б.6.4).
Насколько я понял, при передаче initializer_list по значению, в конечном счете он все равно передается по ссылке / через указатель, поэтому передача его по ссылке сама по себе достаточно бессмысленна. В стандартной библиотеке все передачи initializer_list выполнены по значению.