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

  Юрий  | 

  |

  Обновл. 13 Сен 2021  | 

 48140

 ǀ   13 

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

Строка №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 (146 оценок, среднее: 4,93 из 5)
Загрузка...

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

  1. Павел:

    С std::initializer_list перестает работать uniform-инициализация.

    Например, если мы хотим вызвать конструктор с длиной через uniform

    Выведет 1

  2. Игорь:

    подскажите, что это за цикл

    не понятно, что за параметры в скобках

  3. Victoria:

    а почему нельзя просто использовать вот такой конструктор:

  4. Роман:

    Здравствуйте. Уместна ли такая реализация?

    1. Юрий:

      В целом да, только выполняется цепочка ненужных вызовов (reallocate -> erase), да и к тому же у вас не стоит проверка на равенство размеров массива и списка инициализации, зачем проводить лишние операции? Мое мнение — проще написать все ручками без лишних вызовов, чтобы весь код можно было посмотреть на месте, не такой уж это и большой объем.

  5. Alexandr:

    Объясните, пожалуйста, почему без const в параметре перегрузки оператора присваивания первые два элемента равны нулю в новом массиве? С const все норм.

    1. Пётр:

      Список представляет собой r-value, поэтому для его передачи по ссылке, должна использоваться константная ссылка. Либо передавать без ссылки, через копирование. Если же убрать const, то компилятор вместо нашего перегруженного оператора попытается сделать приведение списка к ArrayInt и вызвать неявный оператор присваивания, в общем получим то, что описано в уроке выше.

  6. kmish:

    Мой вариант:

    1. Юрий:

      Если я все правильно понял, то нет смысла в проверке "list.size() == 0", ибо список инициализации не может быть пустым. Также имеет смысл пересоздавать массив только в случае, если его размер не равен размеру list, т.е. желательна проверка, чтобы исключить лишние действия

  7. kmish:

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

    Результат:

    7 6 5 4 3 2 1

    Работает!

  8. Nick:

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

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

    1. Павел:

      Насколько я понял, при передаче initializer_list по значению, в конечном счете он все равно передается по ссылке / через указатель, поэтому передача его по ссылке сама по себе достаточно бессмысленна. В стандартной библиотеке все передачи initializer_list выполнены по значению.

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

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