На цьому уроці ми розглянемо список ініціалізації std::initializer_list.
Списки ініціалізації
Розглянемо фіксований масив цілих чисел в мові С++:
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; } |