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

  Юрий  | 

    | 

  Обновл. 24 Июн 2019  | 

 5431

 ǀ   3 

В этом уроке мы рассмотрим, что такое список инициализации std::initializer_list, его использование и нюансы.

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

Рассмотрим фиксированный массив целых чисел в 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Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (24 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 3

  1. Аватар kmish:

    Мой вариант:

  2. Аватар kmish:

    "По каким-то необъяснимым причинам std::initializer_list не предоставляет доступ к своим элементам через оператор индексации []. Об этом много говорили, но официального решения так и не предоставили."
    Не совсем точно. Покопался над этим вопросом и нашел следующее:
    Доступ к элементам списка можно получить через list.begin()[]
    Не совсем напрямую, но вполне себе приемлемо.
    Итого получается более читаемый код с циклом for вместо foreach:

    Результат:

    7 6 5 4 3 2 1

    Работает!

  3. Аватар Nick:

    Цитата из Бьярне Страуструп "Программирование: принципы и практика с использованием С++" 2-е издание

    Обратите внимание, что мы передаем initializer_list<douЬle> по зна­чению. Это сделано сознательно и требуется правилами языка: тип initializer_list по сути является обработчиком для элементов, находящихся "где-то" (см. раздел §Б.6.4).

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

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