Урок №160. Список ініціалізації std::initializer_list

  Юрій  | 

  Оновл. 26 Вер 2021  | 

 164

На цьому уроці ми розглянемо список ініціалізації std::initializer_list.

Списки ініціалізації

Розглянемо фіксований масив цілих чисел в мові С++:

Для ініціалізації цього масиву ми можемо використати список ініціалізації:

Результат:

7 6 5 4 3 2 1

Це також працює і з динамічно виділеними масивами:

На попередньому уроці ми розглядали контейнерні класи на прикладі класу-масиву цілих чисел ArrayInt:

Що відбудеться, якщо ми спробуємо використати список ініціалізації з цим контейнерним класом?

Цей код не скомпілюється, тому що клас ArrayInt не має конструктора, який би знав, що робити зі списком ініціалізації, тому кожен елемент потрібно ініціалізувати в індивідуальному порядку:

Якось не дуже, правда?

До 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:

Результат виконання програми:

7 6 5 4 3 2 1

Працює! Тепер розглянемо це все детально.

Ось наш конструктор, який приймає std::initializer_list<int>:

Рядок №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), щоб він не міг використовуватися для неявних конвертацій.

Чому? Розглянемо вищенаведений клас (який не має перевантаження оператора присвоювання чи копіюючого присвоювання) з наступним стейтментом:

По-перше, компілятор бачить, що функції присвоювання, яка приймає 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, реалізуйте перевантаження оператора присвоювання, який прийматиме список ініціалізації. Наступний код:

Повинен видавати наступний результат:

7 6 5 4 3 2 1
1 4 9 12 15 17 19 21

Відповідь

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (2 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *