9.22 – Знакомство с std::array

Добавлено 10 июня 2021 в 18:39
Глава 9 – Массивы, строки, указатели и ссылки  (содержание)

В предыдущих уроках мы подробно говорили о фиксированных и динамических массивах. Хотя оба они встроены прямо в язык C++, у них обоих есть недостатки: фиксированные массивы распадаются на указатели, при этом теряется информация о длине массива, а у динамических массивов возникают проблемы с освобождением памяти, и их сложно изменить без допущения ошибок.

Для решения этих проблем стандартная библиотека C++ включает в себя функционал, упрощающий управление массивами, std::array и std::vector. В этом уроке мы рассмотрим std::array, а в следующем – std::vector.

Знакомство с std::array

std::array обеспечивает функциональность фиксированного массива, которая не пропадает при передаче в функцию. std::array определяется в заголовке <array> внутри пространства имен std.

Объявить переменную std::array очень просто:

#include <array>
 
std::array<int, 3> myArray; // объявляем массив значений int длиной 3

Как и в случае с нативной реализацией фиксированных массивов, длина std::array должна быть известна во время компиляции.

std::array может быть инициализирован с помощью списка инициализаторов или инициализации списком:

std::array<int, 5> myArray = { 9, 7, 5, 3, 1 }; // список инициализаторов
std::array<int, 5> myArray2 { 9, 7, 5, 3, 1 };  // инициализация списком

В отличие от встроенных фиксированных массивов, с std::array вы не можете опустить длину массива при предоставлении инициализатора:

std::array<int, > myArray { 9, 7, 5, 3, 1 };// недопустимо, должна быть указана длина массива
std::array<int> myArray { 9, 7, 5, 3, 1 };  // недопустимо, должна быть указана длина массива

Однако, начиная с C++17, можно не указывать тип и размер. Их можно опустить только обоих сразу, а не что-то одно, и только если массив инициализирован явно.

std::array myArray { 9, 7, 5, 3, 1 };// Тип выводится как std::array<int, 5>
std::array myArray { 9.7, 7.31 };    // Тип выводится как std::array<double, 2>

Мы предпочитаем использовать этот синтаксис, и не указывать в объявлении тип и размер. Если ваш компилятор не поддерживает C++17, вам нужно вместо этого использовать явный синтаксис.

// std::array myArray { 9, 7, 5, 3, 1 };      // начиная с C++17
std::array<int, 5> myArray { 9, 7, 5, 3, 1 }; // до C++17
 
// std::array myArray { 9.7, 7.31 };         // начиная с C++17
std::array<double, 2> myArray { 9.7, 7.31 }; // до C++17

Начиная с C++20, можно указать тип элемента, но опустить длину массива. Это делает создание std::array немного более похожим на создание массивов в стиле C. Чтобы создать массив с определенным типом и предполагаемым размером, мы используем функцию std::to_array:

auto myArray1 { std::to_array<int, 5>({ 9, 7, 5, 3, 1 }) };// Указываем тип и размер
auto myArray2 { std::to_array<int>({ 9, 7, 5, 3, 1 }) };   // Указываем только тип, вывести размер
auto myArray3 { std::to_array({ 9, 7, 5, 3, 1 }) };        // Вывести тип и размер

К сожалению, std::to_array дороже, чем создание std::array напрямую, потому что она фактически копирует все элементы из массива в стиле C в std::array. По этой причине следует избегать использования std::to_array, когда массив создается много раз (например, в цикле).

Вы также можете присвоить значения массиву, используя список инициализаторов

std::array<int, 5> myArray;
myArray = { 0, 1, 2, 3, 4 };    // ok
myArray = { 9, 8, 7 };          // ok, элементы 3 и 4 установлены в ноль!
myArray = { 0, 1, 2, 3, 4, 5 }; // недопустимо, слишком много элементов
                                // в списке инициализаторов!

Доступ к значениям std::array с помощью оператора индекса работает так же, как и следовало ожидать:

std::cout << myArray[1] << '\n';
myArray[2] = 6;

Как и у встроенных фиксированных массивов, оператор индекса не проверяет границы. Если указан неверный индекс, вероятно, произойдет что-то плохое.

std::array поддерживает вторую форму доступа к элементам массива (функция at()), которая выполняет проверку границ:

std::array myArray { 9, 7, 5, 3, 1 };
myArray.at(1) = 6;  // элемент массива 1 допустим, устанавливает элемент массива 1 в значение 6
myArray.at(9) = 10; // элемент массива 9 недопустим, выдаст ошибку

В приведенном выше примере вызов myArray.at(1) проверяет, является ли индекс 1 допустимым, и, поскольку это так, он возвращает ссылку на элемент массива 1. Затем мы присваиваем ему значение 6. Однако вызов myArray.at(9) завершается неудачно, поскольку элемент массива 9 находится за пределами массива. Вместо возврата ссылки функция at() выдает ошибку, которая завершает программу (примечание: на самом деле она генерирует исключение типа std::out_of_range – мы рассмотрим исключения в главе 14). Поскольку at() выполняет проверку границ, она медленнее (но безопаснее), чем operator[].

std::array будет очищаться, когда выходит за пределы области видимости, поэтому нет необходимости выполнять какую-либо ручную очистку.

Размер и сортировка

Для получения длины массива std::array можно использовать функцию size():

std::array myArray { 9.0, 7.2, 5.4, 3.6, 1.8 };
std::cout << "length: " << myArray.size() << '\n';

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

length: 5

Поскольку std::array не раскладывается на указатель при передаче в функцию, функция size() будет работать, даже если вы вызовете ее из функции:

#include <array>
#include <iostream>
 
void printLength(const std::array<double, 5> &myArray)
{
    std::cout << "length: " << myArray.size() << '\n';
}
 
int main()
{
    std::array myArray { 9.0, 7.2, 5.4, 3.6, 1.8 };
 
    printLength(myArray);
 
    return 0;
}

Это также напечатает:

length: 5

Обратите внимание, что в стандартной библиотеке термин «size» (размер) используется для обозначения длины массива – не путайте его с результатами sizeof() для встроенного фиксированного массива, который возвращает фактический размер массива в памяти (произведение размера элемента на длину массива). Да, эта номенклатура противоречива.

Также обратите внимание, что мы передали std::array по (константной) ссылке. Это сделано для того, чтобы компилятор не создал копию std::array, когда тот был передан в функцию (по соображениям производительности).

Лучшая практика


Всегда передавайте std::array по ссылке или по константной ссылке

Поскольку длина всегда известна, с std::array работают циклы for на основе диапазона (for-each):

std::array myArray{ 9, 7, 5, 3, 1 };
 
for (int element : myArray)
    std::cout << element << ' ';

Вы можете отсортировать std::array, используя функцию std::sort, которая находится в заголовке <algorithm>:

#include <algorithm> // для std::sort
#include <array>
#include <iostream>
 
int main()
{
    std::array myArray { 7, 3, 1, 9, 5 };
    std::sort(myArray.begin(), myArray.end());   // сортируем массив по возрастанию
//  std::sort(myArray.rbegin(), myArray.rend()); // сортируем массив по убыванию
 
    for (int element : myArray)
        std::cout << element << ' ';
 
    std::cout << '\n';
 
    return 0;
}

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

1 3 5 7 9

Функция сортировки использует итераторы, концепцию, которую мы еще не рассмотрели. Поэтому пока вы можете рассматривать параметры std::sort() как небольшую магию. Мы объясним их позже.

Передача массивов std::array разной длины в функции

В std::array тип элемента и длина массива являются частью информации о типе. Следовательно, когда мы используем std::array в качестве параметра функции, мы должны указать тип элемента и длину массива:

#include <array>
#include <iostream>
 
void printArray(const std::array<int, 5>& myArray)
{
    for (auto element : myArray)
        std::cout << element << ' ';
    std::cout << '\n';
}
 
int main()
{
    std::array myArray5{ 9.0, 7.2, 5.4, 3.6, 1.8 };
    printArray(myArray5);
 
    return 0;
}

Обратной стороной здесь является то, что это ограничивает нашу функцию обработкой массивов только этого конкретного типа и длины. Но что, если мы хотим, чтобы наша функция обрабатывала массивы элементов разного типа или длины? Нам нужно будет создать копию функции для каждого типа элемента и/или длины массива, которые мы хотим использовать. Но тогда мы получим много дублирующего кода.

К счастью, мы можем заставить C++ сделать это за нас, используя средство языка, называемое шаблонами. Мы еще не рассмотрели шаблоны, но основная идея заключается в том, что мы можем создать шаблонную функцию, которая параметризует часть или всю информацию о типе, а затем C++ по мере необходимости будет использовать этот шаблон для создания «реальных» функций (с фактическими типами).

#include <array>
#include <cstdef>
#include <iostream>
 
// printArray - это шаблонная функция
template <class T, std::size_t size> // параметризуем тип элемента и размер
void printArray(const std::array<T, size>& myArray)
{
    for (auto element : myArray)
        std::cout << element << ' ';
    std::cout << '\n';
}
 
int main()
{
    std::array myArray5{ 9.0, 7.2, 5.4, 3.6, 1.8 };
    printArray(myArray5);
 
    std::array myArray7{ 9.0, 7.2, 5.4, 3.6, 1.8, 1.2, 0.7 };
    printArray(myArray7);
 
    return 0;
}

Связанный контент


Мы подробно рассмотрим шаблоны в главе 19.

Ручное индексирование std::array через size_type

Популярный вопрос тестов: что не так со следующим кодом?

#include <iostream>
#include <array>
 
int main()
{
    std::array myArray { 7, 3, 1, 9, 5 };
 
    // Обходим массив и выводим значения элементов
    for (int i{ 0 }; i < myArray.size(); ++i)
        std::cout << myArray[i] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Ответ заключается в том, что в этом коде есть вероятное несоответствие «со знаком / без знака»! Из-за любопытного решения функция size() и параметр индекса массива для operator[] используют тип с именем size_type, который определен стандартом C++ как целочисленный тип без знака. Наш счетчик/индекс цикла (переменная i) – это целочисленный тип со знаком, signed int. Поэтому и сравнение i < myArray.size(), и индекс массива myArray[i] имеют несоответствия типов.

Интересно, что size_type не является глобальным типом (как int или std::size_t). Он определен внутри определения std::array (C++ допускает вложенные типы). Это означает, что когда мы хотим использовать size_type, мы должны поставить перед ним префикс полного типа массива (подумайте о std::array, действующем в этом отношении как пространство имен). В нашем примере, приведенном выше, тип size_type с полным префиксом – это std::array<int, 5>::size_type!

Следовательно, правильный способ написать приведенный выше код выглядит следующим образом:

#include <array>
#include <iostream>
 
int main()
{
    std::array myArray { 7, 3, 1, 9, 5 };
 
    // std::array<int, 5>::size_type - это тип, возвращаемый функцией size()!
    for (std::array<int, 5>::size_type i{ 0 }; i < myArray.size(); ++i)
        std::cout << myArray[i] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Это не очень читабельно. К счастью, std::array::size_type – это просто псевдоним для std::size_t, поэтому мы можем использовать его.

#include <array>
#include <cstddef> // std::size_t
#include <iostream>
 
int main()
{
    std::array myArray { 7, 3, 1, 9, 5 };
 
    for (std::size_t i{ 0 }; i < myArray.size(); ++i)
        std::cout << myArray[i] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Лучшее решение – в первую очередь, избегать ручной индексации std::array. Вместо этого по возможности используйте циклы for на основе диапазона (или итераторы).

Имейте в виду, что беззнаковые целые числа пойдут по кругу, когда вы достигнете их пределов. Распространенной ошибкой является уменьшение индекса, который уже равен 0, что приводит к переходу к максимальному значению. Вы видели это в уроке о циклах for, но давайте повторимся.

#include <array>
#include <iostream>
 
int main()
{
    std::array myArray { 7, 3, 1, 9, 5 };
 
    // Распечатываем массив в обратном порядке.
    // Мы можем использовать auto, потому что мы не инициализируем i нулем.
    // Плохо:
    for (auto i{ myArray.size() - 1 }; i >= 0; --i)
        std::cout << myArray[i] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Это бесконечный цикл, приводящий к неопределенному поведению, поскольку i идет по кругу. Здесь есть две проблемы. Если myArray пуст, т.е. size() возвращает 0 (что возможно с std::array), myArray.size() - 1 переносится к другому концу диапазона. Другая проблема возникает независимо от количества элементов. Условие i >= 0 всегда истинно потому, что целые числа без знака не могут быть меньше 0.

Рабочий вариант обратного цикла for для целочисленных типов без знака принимает странную форму:

#include <array>
#include <iostream>
 
int main()
{
    std::array myArray { 7, 3, 1, 9, 5 };
 
    // Распечатываем массив в обратном порядке.
    for (auto i{ myArray.size() }; i-- > 0; )
        std::cout << myArray[i] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Внезапно теперь мы уменьшаем индекс в условии и используем постфиксный оператор --. Условие проверяется перед каждой итерацией, включая первую. В первой итерации i равно myArray.size() - 1, потому что i было уменьшено в условии. Когда i равно 0 и вот-вот перейдет к противоположному концу своего диапазона, условие больше не выполняется, и цикл останавливается. i на самом деле переносится, когда мы выполняем i-- в последний раз, но потом уже не используется.

Массив структур

Конечно std::array не ограничивается числами в качестве элементов. Каждый тип, который можно использовать в обычном массиве, можно использовать и в std::array.

#include <array>
#include <iostream>
 
struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};
 
int main()
{
    std::array<House, 3> houses{};
 
    houses[0] = { 13, 4, 30 };
    houses[1] = { 14, 3, 10 };
    houses[2] = { 15, 3, 40 };
 
    for (const auto& house : houses)
    {
        std::cout << "House number " << house.number
                  << " has " << (house.stories * house.roomsPerStory)
                  << " rooms\n";
    }
 
    return 0;
}

Вывод программы:

House number 13 has 120 rooms
House number 14 has 30 rooms
House number 15 has 120 rooms

Однако всё становится немного странно, когда мы пытаемся инициализировать массив.

// Не работает
std::array<House, 3> houses{
    { 13, 4, 30 },
    { 14, 3, 10 },
    { 15, 3, 40 }
};

Хотя мы можем так инициализировать std::array, если его элементы принадлежат простым типам, таким как int или std::string, но это не работает с типами, которым необходимо создать несколько значений. Давайте посмотрим, почему это так.

std::array – это агрегированный тип, как, например, и House. Специальной функции для создания std::array нет. Его внутренний массив инициализируется, как и любая другая переменная-член структуры. Чтобы упростить понимание, мы сами реализуем простой тип массива.

На данный момент мы не можем сделать это без доступа к члену value. Как это обойти, вы узнаете позже. Но это не влияет на проблему, которую мы наблюдаем.

struct Array
{
  int value[3]{};
};
 
int main()
{
    Array array{
        11,
        12,
        13
    };
 
    return 0;
}

Как и ожидалось, это работает. То же самое и с std::array, если мы используем его с элементами int. Создавая экземпляр структуры, мы можем инициализировать все ее члены. Если мы попытаемся создать массив Array из структур House, то получим ошибку.

struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};
 
struct Array
{
    // Теперь это массив структур House
    House value[3]{};
};
 
int main()
{
    // Если мы попытаемся инициализировать массив, то получим ошибку
    Array houses{
        { 13, 4, 30 },
        { 14, 3, 10 },
        { 15, 3, 40 }
    };
 
    return 0;
}

Когда мы используем фигурные скобки внутри инициализации, компилятор попытается инициализировать один член структуры для каждой пары фигурных скобок. Вместо того, чтобы инициализировать массив следующим образом:

// Это не верно
Array houses{
    { 13, 4, 30 }, // value[0]
    { 14, 3, 10 }, // value[1]
    { 15, 3, 40 }  // value[2]
};

Компилятор пытается инициализировать массив следующим образом:

Array houses{
    { 13, 4, 30 }, // value
    { 14, 3, 10 }, // ???
    { 15, 3, 40 }  // ???
};

Первая пара внутренних фигурных скобок инициализирует value, поскольку value является первым членом Array. Без двух других пар фигурных скобок был бы один дом (одна структуру House) с номером 13, 4 этажами и 30 комнатами на этаж.

Напоминание


Скобки во время агрегированной инициализации можно опустить:

struct S
{
  int arr[3]{};
  int i{};
};
 
// Эти две инструкции делают одно и то же
S s1{ { 1, 2, 3 }, 4 };
S s2{ 1, 2, 3, 4 };

Чтобы инициализировать все дома (структуры House), нам нужно сделать это в первой паре скобок.

Array houses{
    { 13, 4, 30, 14, 3, 10, 15, 3, 40 } // value
};

Это работает, но очень сбивает с толку. Так сбивает с толку, что ваш компилятор может даже предупредить вас об этом. Инициализацию будет намного легче читать, если мы добавим фигурные скобки вокруг каждого элемента массива.

#include <iostream>
 
struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};
 
struct Array
{
    House value[3]{};
};
 
int main()
{
    // С фигурными скобками это работает.
    Array houses{
        { { 13, 4, 30 }, { 14, 3, 10 }, { 15, 3, 40 } }
    };
 
    for (const auto& house : houses.value)
    {
        std::cout << "House number " << house.number
                  << " has " << (house.stories * house.roomsPerStory)
                  << " rooms\n";
    }
 
    return 0;
}

Вот почему вы встретите лишнюю пару фигурных скобок при инициализации std::array.

Резюме

std::array – отличная замена встроенным фиксированным массивам. Он эффективен, поскольку использует памяти не больше, чем встроенные фиксированные массивы. Единственный реальный недостаток std::array, по сравнению со встроенным фиксированным массивом, – это немного более неудобный синтаксис, в котором вам нужно явно указать длину массива (компилятор не будет рассчитывать ее из инициализатора вместо вас, если вы также не опустите тип, что не всегда возможно), и проблемы «со знаком / без знака» с размером и индексированием. Но это сравнительно незначительные придирки – мы рекомендуем использовать std::array вместо встроенных фиксированных массивов для любого нетривиального использования массива.

Теги

C++ / CppLearnCppstd::arraystd::to_arraySTL / Standard Template Library / Стандартная библиотека шаблоновДля начинающихМассивОбучениеПрограммирование

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

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