Урок 152. Список инициализации std::initializer_list

   | 

   | 

 Обновлено 8 Июл 2018  | 

 1138

Рассмотрим фиксированный массив целых чисел в C++:

Для инициализации этого массива мы можем использовать список инициализации:

Результат:

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. Обратите внимание, мы передаем список по константной ссылке, дабы избежать создания копии std::initializer_list при передаче в конструктор.

Строка №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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (8 оценок, среднее: 5,00 из 5)
Загрузка...
Подписаться на обновления:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ВОЛШЕБНАЯ ТАБЛЕТКА ПО С++