16.7 – Список инициализаторов std::initializer_list

Добавлено 29 июля 2021 в 02:18

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

int array[5];

Если мы хотим инициализировать этот массив значениями, мы можем сделать это напрямую с помощью синтаксиса списка инициализаторов:

#include <iostream>
 
int main()
{
    int array[] { 5, 4, 3, 2, 1 }; // список инициализаторов
    for (auto i : array)
        std::cout << i << ' ';
 
    return 0;
}

Этот код печатает:

5 4 3 2 1

Это также работает и для динамически размещаемых массивов:

#include <iostream>
 
int main()
{
    auto *array{ new int[5]{ 5, 4, 3, 2, 1 } }; // список инициализаторов
    for (int count{ 0 }; count < 5; ++count)
        std::cout << array[count] << ' ';
    delete[] array;
 
    return 0;
}

В предыдущем уроке мы представили концепцию контейнерных классов и показали пример класса IntArray, который содержит массив чисел int:

#include <cassert> // для assert()
#include <iostream>
 
class IntArray
{
private:
    int m_length{};
    int *m_data{};
 
public:
    IntArray() = default;
 
    IntArray(int length):
        m_length{ length },
        m_data{ new int[length]{} }
    {
    }
 
    ~IntArray()
    {
        delete[] m_data;
        // здесь нам не нужно устанавливать m_data в значение null
        // или m_length в 0, так как объект в любом случае будет
        // уничтожен сразу после этой функции
    }
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() const { return m_length; }
};
 
int main()
{
    // Что произойдет, если мы попытаемся использовать список
    // инициализаторов с этим контейнерным классом?
    IntArray array { 5, 4, 3, 2, 1 }; // эта строка не компилируется
    for (int count{ 0 }; count < 5; ++count)
        std::cout << array[count] << ' ';
 
    return 0;
}

Этот код не компилируется, потому что у класса IntArray нет конструктора, который знает, что делать со списком инициализаторов. В результате нам остается инициализировать элементы массива по отдельности:

int main()
{
    IntArray array(5);
    array[0] = 5;
    array[1] = 4;
    array[2] = 3;
    array[3] = 2;
    array[4] = 1;
 
    for (int count{ 0 }; count < 5; ++count)
        std::cout << array[count] << ' ';
 
    return 0;
}

Это не так уж и хорошо.

Инициализация класса с помощью std::initializer_list

Когда компилятор видит список инициализаторов, он автоматически преобразует его в объект типа 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. Вместо этого вы увидите что-то вроде std::initializer_list<int> или std::initializer_list<std::string>.

Во-вторых, std::initializer_list имеет (не совсем правильную) функцию size(), которая возвращает количество элементов в списке. Она полезна, когда нам нужно знать длину переданного списка.

Давайте посмотрим, как обновить наш класс IntArray с помощью конструктора, который принимает std::initializer_list.

#include <cassert>          // для assert()
#include <initializer_list> // для std::initializer_list
#include <iostream>
 
class IntArray
{
private:
    int m_length{};
    int *m_data{};
 
public:
    IntArray() = default;
 
    IntArray(int length) :
        m_length{ length },
        m_data{ new int[length]{} }
    {
 
    }
 
    // позволяет инициализировать IntArray с помощью списка инициализаторов
    IntArray(std::initializer_list<int> list) :
        // используем делегирующий конструктор для начальной установки массива
        IntArray(static_cast<int>(list.size())) 
    {
        // Теперь инициализируем наш массив из списка
        int count{ 0 };
        for (auto element : list)
        {
            m_data[count] = element;
            ++count;
        }
    }
 
    ~IntArray()
    {
        delete[] m_data;
        // здесь нам не нужно устанавливать m_data в значение null
        // или m_length в 0, так как объект в любом случае будет
        // уничтожен сразу после этой функции
    }
 
    // чтобы избежать поверхностного копирования
    IntArray(const IntArray&) = delete; 
    // чтобы избежать поверхностного копирования
    IntArray& operator=(const IntArray& list) = delete; 
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() const { return m_length; }
};
 
int main()
{
    IntArray array{ 5, 4, 3, 2, 1 }; // список инициализаторов
    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';
 
    return 0;
}

Этот код дает ожидаемый результат:

5 4 3 2 1

Всё работает! Теперь давайте рассмотрим этот код более подробно.

Вот наш конструктор IntArray, который принимает std::initializer_list<int>.

// позволяет инициализировать IntArray с помощью списка инициализаторов
IntArray(std::initializer_list<int> list) :
    // используем делегирующий конструктор для начальной установки массива
    IntArray(static_cast<int>(list.size())) 
{
    // Теперь инициализируем наш массив из списка
    int count{ 0 };
    for (auto element : list)
    {
        m_data[count] = element;
        ++count;
    }
}

Строка 2: Как отмечалось выше, мы должны использовать угловые скобки, чтобы обозначить, какой тип элементов мы ожидаем внутри списка. В этом случае, поскольку это IntArray, мы ожидаем, что список будет заполнен значениями int. Обратите внимание, что мы не передаем список по константной ссылке. Как и std::string_view, std::initializer_list очень легкий, и его копии обычно дешевле, чем косвенное обращение.

Строка 4: Мы делегируем выделение памяти для IntArray другому конструктору через делегирующий конструктор (для уменьшения избыточного кода). Этот другой конструктор должен знать длину массива, поэтому мы передаем ему значение list.size(), которое содержит количество элементов в списке. Обратите внимание, что list.size() возвращает size_t (без знака), поэтому здесь нам нужно привести это значение к signed int. Мы используем прямую инициализацию, а не инициализацию с фигурными скобками, потому что для инициализации с фигурными скобками предпочтительны конструкторы со списком инициализаторов. Хотя конструктор будет разрешен правильно, для инициализации классов с конструкторами со списком инициализаторов безопаснее использовать прямую инициализацию, если мы не хотим использовать конструктор со списком инициализаторов.

Тело конструктора зарезервировано для копирования элементов из списка в наш класс IntArray. По какой-то необъяснимой причине std::initializer_list не предоставляет доступ к элементам списка через индексирование (operator[]). Об этом упущении много раз сообщали комитету по стандартам.

Однако есть простые способы обойти отсутствие индексов. Самый простой способ – использовать здесь цикл for-each. Цикл for-each проходит по всем элементам списка инициализаторов, и мы можем вручную скопировать эти элементы в наш внутренний массив.

Одно предостережение: списки инициализаторов всегда будут отдавать предпочтение соответствующему конструктору с initializer_list по сравнению с другими потенциально подходящими конструкторами. Таким образом, это определение переменной:

IntArray array { 5 };

будет соответствовать IntArray(std::initializer_list<int>), а не IntArray(int). Если после определения конструктора со списком вы хотите сопоставить этот код с IntArray(int), вам нужно будет использовать копирующую или прямую инициализацию. То же самое происходит с std::vector и другими контейнерными классами, которые имеют конструктор со списком и конструктор с параметром аналогичного типа.

// Вызывает std::vector::vector(std::vector::size_type),
// 5 элементов, инициализированных значениями: 0 0 0 0 0
std::vector<int> array(5);

// Вызывает std::vector::vector(std::initializer_list<int>),
// 1 элемент: 5
std::vector<int> array{ 5 };

Присваивание объектам класса с использованием std::initializer_list

Вы также можете использовать std::initializer_list для присваивания новых значений объектам класса, перегрузив оператор присваивания, чтобы он принимал параметр std::initializer_list. Это работает аналогично предыдущему случаю. Пример того, как это сделать, будет показан в приведенном ниже решении на вопрос теста.

Обратите внимание, что если вы реализуете конструктор, который принимает std::initializer_list, вы должны убедиться, что выполняете хотя бы одно из следующих действий:

  1. предоставить перегруженный оператор присваивания со списком инициализаторов;
  2. предоставить правильный оператор присваивания для глубокого копирования.

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

array = { 1, 3, 5, 7, 9, 11 }; // перезаписываем элементы массива элементами из списка

Во-первых, компилятор заметит, что функции присваивания, принимающей std::initializer_list, не существует. Затем он будет искать другие функции присваивания, которые он сможет использовать, и обнаружит неявно предоставленный оператор копирующего присваивания. Однако эту функцию можно использовать только в том случае, если она может преобразовать список инициализаторов в IntArray. Поскольку {1, 3, 5, 7, 9, 11} является списком std::initializer_list, компилятор будет использовать конструктор со списком инициализаторов для преобразования списка инициализаторов во временный массив IntArray. Затем он вызовет неявный оператор присваивания, который выполнит поверхностное копирование временного массива IntArray в наш объект массива.

На этом этапе и m_data временного IntArray, и array->m_data указывают на один и тот же адрес (из-за поверхностного копирования). Вы уже можете догадаться, к чему это идет.

В конце инструкции присваивания временный массив IntArray уничтожается. Это вызывает деструктор, который удаляет m_data временного массива IntArray. Это оставляет нашу переменную массива с висячим указателем m_data. Когда вы попытаетесь использовать array->m_data для чего-либо (в том числе, когда массив выходит за пределы области видимости, а деструктор переходит к удалению m_data), вы получите неопределенное поведение (и, возможно, сбой).

Правило

Если вы обеспечиваете создание объекта с использованием списка инициализаторов, неплохо также предоставить присваивание с использованием списка инициализаторов.

Резюме

Реализация конструктора, который принимает параметр std::initializer_list, позволяет нам использовать инициализацию списком с нашими пользовательскими классами. Мы также можем использовать std::initializer_list для реализации других функций, которые должны использовать список инициализаторов, например, оператора присваивания.

Небольшой тест

Вопрос 1

Используя приведенный выше класс IntArray, реализуйте перегруженный оператор присваивания, который принимает список инициализаторов.

Должен запуститься следующий код:

int main()
{
    IntArray array { 5, 4, 3, 2, 1 }; // список инициализаторов
    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';
 
    std::cout << '\n';
 
    array = { 1, 3, 5, 7, 9, 11 };
 
    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Эта программа должна напечатать:

5 4 3 2 1 
1 3 5 7 9 11

#include <cassert>          // для assert()
#include <initializer_list> // для std::initializer_list
#include <iostream>
 
class IntArray
{
private:
    int m_length {};
    int *m_data {};
 
public:
    IntArray() = default;
 
    IntArray(int length) :
        m_length{ length },
        m_data{ new int[length]{} }
    {
 
    }

    // позволяет инициализировать IntArray с помощью инициализацию списком 
    IntArray(std::initializer_list<int> list) :
        // используем делегирующий конструктор для начальной установки массива 
        IntArray(static_cast<int>(list.size())) 
    {
        // Теперь инициализируем наш массив из списка
        int count{ 0 };
        for (auto element : list)
        {
            m_data[count] = element;
            ++count;
        }
    }
 
    ~IntArray()
    {
        delete[] m_data;
        // здесь нам не нужно устанавливать m_data в значение null
        // или m_length в 0, так как объект в любом случае будет
        // уничтожен сразу после этой функции
    }
 
    // чтобы избежать поверхностного копирования
    IntArray(const IntArray&) = delete;
    // чтобы избежать поверхностного копирования
    IntArray& operator=(const IntArray& list) = delete; 
 
    IntArray& operator=(std::initializer_list<int> list)
    {
        // Если новый список другого размера, размещаем его заново
        int length{ static_cast<int>(list.size()) };
        if (length != m_length)
        {
            delete[] m_data;
            m_length = length;
            m_data = new int[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() const { return m_length; }
};
 
int main()
{
    IntArray array { 5, 4, 3, 2, 1 }; // список инициализаторов
    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';
 
    std::cout << '\n';
 
    array = { 1, 3, 5, 7, 9, 11 };
 
    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Теги

C++ / CppLearnCppstd::initializer_listДля начинающихКонструктор / Constructor / ctor (программирование)ОбучениеОбъектно-ориентированное программирование (ООП)Оператор присваиванияПерегрузка операторовПрограммированиеСписок инициализаторов

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.