std::span
Примеры на понимание
Заголовок раздела «Примеры на понимание»1. Индексирование указателей
Заголовок раздела «1. Индексирование указателей»std::array<int, 3> arr{ 0, 1, 2 };int* arrStart{ arr.data() };*arrStart = 3;*(arrStart + 0) = 4;arrStart[0] = 5;*(arrStart + 1) = 6;arrStart[1] = 7;*(arrStart + 2) = 8;arrStart[2] = 9;
int item0{ *arrStart };item0 = arrStart[0];int item1{ *(arrStart + 1) };item1 = arrStart[1];Ответ:
arrStart получает адрес первого элемента в массиве, благодаря .data().
Далее *arrStart ссылается на первый элемент в массиве по адресу.
*arrStart эквивалентно *(arrStart + 0).
Синтаксис *(x + 1) эквивалентен синтаксису x[1].
Соответственно, *(arrStart + 0) можно индексированием записать как arrStart[0].
x[1]также эквивалентен1[x]
*(arrStart + 1) эквивалентно arrStart[1], что эквивалентно arr[1].
Здесь, мы перепрыгиваем через первый элемент (+ 1),
начиная с начала первого элемента (arrStart),
попадая на следующую ячейку в памяти.
Эти выражения можно использовать как для вписывания значений, так и для их считывания.
2. Передача массива как указатель
Заголовок раздела «2. Передача массива как указатель»#include <iostream>#include <array>
void func(int* arr){ std::cout << arr[0] << std::endl; std::cout << arr[1] << std::endl;}
int main(){ std::array<int, 2> arr{ 1, 2 }; func(arr.data());}Ответ:
Здесь функции передается адрес первого элемента из массива.
arr[0] эквивалентен *(arr + 0)
и считает первый элемент из массива arr из функции main.
Соответственно arr[1] эквивалентен *(arr + 1) и напечатает второй элемент.
Выведется 1 и 2.
3. Особый синтаксис передачи массива
Заголовок раздела «3. Особый синтаксис передачи массива»#include <iostream>#include <array>
void func(int arr[]){ std::cout << arr[0] << std::endl; std::cout << arr[1] << std::endl;}
int main(){ std::array<int, 2> arr{ 1, 2 }; func(arr.data());}Ответ:
int* arr эквивалентно int arr[] когда используется в качестве параметра.
Этот пример эквивалентен предыдущему.
4. Чтение за пределами массива
Заголовок раздела «4. Чтение за пределами массива»#include <iostream>#include <array>
void func(int arr[]){ std::cout << arr[0] << std::endl; std::cout << arr[1] << std::endl; std::cout << arr[2] << std::endl;}
int main(){ std::array<int, 2> arr{ 1, 2 }; func(arr.data());}Ответ:
arr[2] в инструкции std::cout << arr[2] << std::endl пытается считать по индексу 2 из массива.
В C, попытка считывания по несуществующему индексу считается UB (undefined behavior).
Этот код скомпилируется и запустится без проблем,
но считывание arr[2] технически способно привести к любому результату при выполнении.
Подобное считается логической ошибкой в коде.
Как предотвратить данную ошибку?
Нужно выполнять проверку того, находится ли индекс за пределами массива перед тем, как считываете по тому или иному индексу. Для того чтобы выполнить данную проверку, помимо указателя на начало массива нужно иметь также и длину массива. В данном примере, длина массива известна (2), но в общем случае ее тоже нужно будет передать. Об этом есть в дальнейших примерах.
5. Вызов той же функции для разных массивов
Заголовок раздела «5. Вызов той же функции для разных массивов»#include <iostream>#include <array>
void func(int arr[]){ std::cout << arr[0] << std::endl; std::cout << arr[1] << std::endl; std::cout << arr[2] << std::endl;}
int main(){ std::array<int, 3> arr1{ 0, 1, 2 }; std::array<int, 2> arr2{ 0, 1 }; func(arr1.data()); func(arr2.data());}Ответ:
Для первого массива не возникает UB, поскольку он позволяет просматривать все индексы от 0 до 2.
Однако для второго массива, повторяется ситуация в примере выше.
Здесь иллюстрируется тот факт, что одна и та же функция может использоваться для массивов разной длины, и не может предположить ее заранее.
6. Печать циклом
Заголовок раздела «6. Печать циклом»#include <iostream>#include <array>
void print(int* arr){ for (size_t i = 0; i < 2; i++) { std::cout << arr[i] << std::endl; }}
int main(){ std::array<int, 2> arr{ 0, 1 }; print(arr.data());}Ответ:
Здесь иллюстрируется то, как можно напечатать в консоль все элементы из массива, используя цикл.
2 в условии цикла ссылается на длину массива.
Как не дублировать длину для массива?
Здесь несколько вариантов:
- Передать статический массив или ссылку на него параметром;
- Передать длину отдельным параметром;
- Создать глобальную константу для длины, но тогда функция будет ограничена только этой длиной;
- Передать
span(указатель на первый элемент и длину одним параметром).
Все это описано в дальнейших примерах.
Что будет, если длина не совпадает?
Если в цикле упомянуть не ту длину, что длина массива:
for (size_t i = 0; i < 3; i++)произойдет чтение за элемента за пределами массива, что считается UB.
Если задать большую длину переменной:
std::array<int, 3> arr{ 0, 1, 2 };напечатается не весь массив.
Если меньшую:
std::array<int, 1> arr{ 0 };снова получим UB в цикле из print.
7. Константа длины массива
Заголовок раздела «7. Константа длины массива»В чем преимущество этого подхода по сравнению с предыдущим?
#include <iostream>#include <array>
constexpr size_t ARRAY_LENGTH = 2;
void print(int* arr){ for (size_t i = 0; i < ARRAY_LENGTH; i++) { std::cout << arr[i] << std::endl; }}
int main(){ std::array<int, ARRAY_LENGTH> arr{ 0, 1 }; print(arr.data());}Что такое constexpr
constexpr (от constant expression) это константа (неизменяемое значение с именем),
известная во время компиляции.
Она как бы заменится компилятором на ее значение везде где она упоминается.
Нам необходима константа на время компиляции,
потому что ARRAY_LENGTH в std::array<int, ARRAY_LENGTH> это часть типа,
а размеры типов в C++ всегда известны при копиляции.
Например, следующее не скомпилируется:
#include <array>
int main(){ size_t len = 2; std::array<int, len> arr{ 1, 2 };}А следующее — да:
#include <array>
int main(){ constexpr size_t len = 2; std::array<int, len> arr{ 1, 2 };}constexpr в данном примере можно заменить на static inline const
(в теме с линкером будет больше об этом), чтобы получить то же поведение
(const int-ы в C++ также являются константами времени компиляции).
Ответ
Преимущество в том, что теперь сложнее передать массив не той что надо длины, хотя это все еще возможно. Например:
std::array<int, 3> arr{};print(arr.data());Это происходит, поскольку нет проверки на то откуда появился указатель при вызове функции.
8. Как получить длину C-массива? А std::array?
Заголовок раздела «8. Как получить длину C-массива? А std::array?»C-массив? Ты о чем?
Вот это определение C-массива:
int arr[2]{};А вот тут определяется C++ массив (std::array):
std::array<int, 2> arr{};Если инициализируете элементы, int и 2 можно не писать, их компилятор сам поймет:
std::array arr{1, 2};Подсказка 1
У C-массива нет метода .size() как у std::array.
Подсказка 2
Приходится обходить систему через sizeof.
Подсказка 3
sizeof для всего массива дает его величину в байтах.
Подсказка 4
sizeof выражения одного из элементов или базового типа дает его величину.
Это можно совместить с предыдущей подсказкой.
Ответ
Количество байтов, выделенное под массив, делите на то, сколько байтов занимает один из элементов, получаете количество элементов.
Например, есть массив из 3 элементов
int arr[3]{}. Всего он занимает 12 байтов (3 раза по 4 байта для каждого из элементов). Каждый элемент занимает по 4 байта.Если цель получить длину массива (3), имея общую длину в байтах (12), нужно посчитать, сколько групп байтов размером в 1 элемент (по 4 байта) влезает в общую длину (12). Для этого применяется деление (12 / 4 = 3).
Если ранее был определен arr:
int arr[3]{};Этого можно достичь вот так:
constexpr size_t len{ sizeof(arr) / sizeof(int) };Или вот так, что более гибко:
constexpr size_t len{ sizeof(arr) / sizeof(arr[0]) };sizeof(arr[0]) дает константу, потому что sizeof смотрит лишь на тип выражения —
операция arr[0] и не вычислится априори — компилятор смотрит на то, какой тип бы вернулся.
9. Передача массива как параметры для указателя и для длины
Заголовок раздела «9. Передача массива как параметры для указателя и для длины»В чем преимущества и недостатки этого подхода?
#include <array>#include <iostream>
void print(int* arr, size_t length){ for (size_t i = 0; i < length; i++) { std::cout << arr[i] << std::endl; }}
int main(){ std::array<int, 2> arr{ 0, 1 }; print(arr.data(), arr.size());
int arr1[3]{}; print(arr1, sizeof(arr1) / sizeof(arr1[0]));}Ответ
Преимущество в том, что теперь функция будет работать для массивов разных длин.
Еще один плюс в том, что еще сложнее ошибится при передаче длины — просто нужно передать длину массива, который передаете.
Главный недостаток в том, что длину таки надо передать правильно, и в этом моменте можно допустить ошибку. Например:
std::array<int, 2> arr{};print(arr.data(), 3);10. (продвинутый уровень) Ссылка на C-массив
Заголовок раздела «10. (продвинутый уровень) Ссылка на C-массив»#include <iostream>
int main(){ int arr[3]{}; int (&arrRef)[3]{ arr }; arrRef[0] = 1; std::cout << arr[0] << std::endl;}11. (продвинутый уровень) Передача ссылки на C-массив
Заголовок раздела «11. (продвинутый уровень) Передача ссылки на C-массив»#include <iostream>
void print(int (&arr)[3]){ for (size_t i = 0; i < 3; i++) { std::cout << arr[i] << std::endl; }}
int main(){ int arr1[3]{}; print(arr1);}12. (продвинутый уровень) Темплейт длины массива
Заголовок раздела «12. (продвинутый уровень) Темплейт длины массива»#include <iostream>
template<size_t len>void print(int (&arr)[len]){ for (size_t i = 0; i < len; i++) { std::cout << arr[i] << std::endl; }}
int main(){ int arr1[3]{}; print(arr1);}template?
Идея в том, что компилятор определит каждое использование данной функции для разных длин массивов,
и создаст разные функции print, подставляя соответствующий len, исходя из этой длины.
Например, здесь, print заменится на print с подстановкой 3 как len, что иначе можно записать
вот так:
print<3>(arr1);Ответ
В шаблоны можно таким образом передавать ссылки на массивы.
13. Изменение элементов C++ массива, переданного параметром (1)
Заголовок раздела «13. Изменение элементов C++ массива, переданного параметром (1)»#include <array>#include <iostream>
void change(std::array<int, 3> arr){ arr[0] = 1;}
int main(){ std::array<int, 3> arr{}; change(arr); std::cout << arr[0] << std::endl;}Ответ
arr не изменится, поскольку функции была передана копия его значения.
14. Изменение элементов C++ массива, переданного параметром (2)
Заголовок раздела «14. Изменение элементов C++ массива, переданного параметром (2)»#include <array>#include <iostream>
void change(std::array<int, 3>& arr){ arr[0] = 1;}
int main(){ std::array<int, 3> arr{}; change(arr); std::cout << arr[0] << std::endl;}Ответ
В arr[0] запишется 1, поскольку функции была передана ссылка на объект масссива в main.
15. Передача C++ массива ссылкой
Заголовок раздела «15. Передача C++ массива ссылкой»#include <array>#include <iostream>
void print(std::array<int, 3>& arr){ for (size_t i = 0; i < arr.size(); i++) { std::cout << arr[i] << std::endl; }}
int main(){ std::array<int, 3> arr{}; print(arr);}Ответ
Здесь, в функцию был передан массив ссылкой. Теперь невозможно передать массив неправильной длины! Например, следующее не скомпилируется:
std::array<int, 2> arr{};print(arr);Недостаток этого подхода в том, что функция поддерживает только массивы длиной 3.
16. (продвинутый уровень) Темплейт длины C++ массива
Заголовок раздела «16. (продвинутый уровень) Темплейт длины C++ массива»#include <array>#include <iostream>
template<size_t len>void print(std::array<int, len>& arr){ for (size_t i = 0; i < arr.size(); i++) { std::cout << arr[i] << std::endl; }}
int main(){ std::array<int, 3> arr{}; print(arr);
std::array<int, 2> arr1{}; print(arr1);}Ответ
Данный подход позволяет создавать по функции для каждой длины массива. Теперь функция поддерживает любую длину массива!
Недостатка сейчас 2:
- Функция поддерживает только C++ массивы со статически известной длиной (длина часть типа и известна при компиляции);
- Будет создано по одной новой функции для каждой длины, что будет тратить лишнюю память в виде инструкций в скомпилированной программе (об этом больше в своей теме).
Эти недостатки можно решить, передавая длину параметром как значение, а не как часть типа.
17. std::span
Заголовок раздела «17. std::span»#include <span>#include <iostream>#include <array>
int main(){ std::array<int, 6> arr{0, 1, 2, 3, 4, 5}; std::span<int> span{ arr };
std::cout << "Item 1: " << span[1] << std::endl; std::cout << "Item 4: " << span[4] << std::endl; std::cout << "arr size in bytes: " << sizeof(arr) << std::endl; std::cout << "span size in bytes: " << sizeof(span) << std::endl; std::cout << "span length: " << span.size() << std::endl;}span
span — это указатель на первый элемент и длина в одном объекте.
Ответ (индексирование):
Индексирование span-а применяется к основной памяти массива.
Напечатается 1 и 4 из arr.
Ответ (sizeof):
arr состоит из 6 int, каждый из которых занимает по 4 байта.
Итого, 6 × 4 = 24 байта.
span хоть и ссылается на arr, делает он это благодаря указателю.
span это один указатель int* на начало массива и один size_t с длиной массива.
Итого, sizeof(int*) + sizeof(size_t) = 8 + 8 = 16.
Ответ (span.size()):
span.size() получает доступ к сохраненной в span длине и возвращает 6.
18. std::span из C++ массива
Заголовок раздела «18. std::span из C++ массива»#include <span>#include <iostream>#include <array>
int main(){ std::array<int, 2> arr{1, 2}; std::span<int> span{ arr }; arr[0] = 3; arr[1] = 4;
std::cout << span[0] << std::endl; std::cout << span[1] << std::endl;}Ответ:
Поскольку span хранит адрес начала массива, а не его копию,
всегда будут выводится текущие значения из массива.
Выведется 3, 4.
19. Метод at
Заголовок раздела «19. Метод at»#include <iostream>#include <array>
int main(){ std::array<int, 2> arr{1, 2}; std::cout << arr[0] << std::endl; std::cout << arr.at(0) << std::endl; std::cout << arr[2] << std::endl; std::cout << arr.at(2) << std::endl;}Что делает arr.at
at дает доступ к элементу, точно так же как индексирование (arr[...]),
но помимо этого делает проверку длины.
Если заданный индекс за пределами массива, программа крашнется.
На самом деле выбросится исключение, которое можно поймать, но это мы не проходим.
Ответ
arr[0] как обычно считает значение первого элемента из массива.
arr.at(0) сделает то же самое, после удачной проверки на длину.
Проверка на длину успешна, потому что в массиве есть индекс 0,
так как длина больше 0.
arr[2] это UB (обсуждалось ранее).
arr.at(2) крашнет программу при проверке длины.
Проверка на длину не успешна, поскольку в массиве нет индекса 2,
так как длина не есть больше 2.
Эта проверка на длину грамотно называется “bounds-check”.
atсуществует дляstd::spanначиная со стандартаC++ 26.
20. Передача std::span параметром
Заголовок раздела «20. Передача std::span параметром»#include <iostream>#include <array>#include <span>
void print(std::span<int> s){ for (size_t i = 0; i < s.size(); i++) { std::cout << s[i] << std::endl; }}
int main(){ std::array<int, 3> arr{ 1, 2, 3 }; std::span<int> span{ arr }; print(span);
print({ arr }); print(arr);}Ответ
Все 3 синтаксиса print(span), print({ arr }) и print(arr) сработают и сделают то же самое.
print(span) передаст копию уже созданного span.
print({ arr }) и print(arr) инициализируют временный
std::span<int> и передадут его функции print.
В каждом случае, print пройдется циклом по элементам arr из main и напечатает их.
21. Печать с помощью range-based цикла for
Заголовок раздела «21. Печать с помощью range-based цикла for»#include <iostream>#include <array>
int main(){ std::array<int, 3> arr{ 1, 2, 3 }; for (int value : arr) { std::cout << value << std::endl; }}Ответ
Напечатаются 1, 2 и 3.
for (int value : arr) перебирает элементы по очереди без индекса:
на каждом проходе value — это копия очередного элемента.
Со std::span это работает так же, поскольку у span тоже есть начало и конец.
22. Изменение элементов с помощью range-based цикла for
Заголовок раздела «22. Изменение элементов с помощью range-based цикла for»#include <iostream>#include <array>
int main(){ std::array<int, 3> arr{ 1, 2, 3 }; for (int& value : arr) { value *= 2; } for (int value : arr) { std::cout << value << std::endl; }}Ответ
Напечатаются 2, 4 и 6.
for (int& value : arr) берет каждый элемент по ссылке, а не копирует его,
поэтому value *= 2 перезаписывает элемент прямо в массиве.
С обычным int value цикл удвоил бы лишь свою копию,
и массив остался бы 1, 2, 3.
23. Подспан вручную
Заголовок раздела «23. Подспан вручную»#include <iostream>#include <array>#include <span>
void print(std::span<int> s){ for (size_t i = 0; i < s.size(); i++) { std::cout << s[i] << std::endl; }}
int main(){ std::array<int, 4> arr{1, 2, 3, 4}; std::span<int> wholeSpan{ arr }; std::span<int> span123{ arr.data(), 3 }; std::span<int> span234{ arr.data() + 1, 3 };
print(wholeSpan);
std::cout << std::endl; print(span123);
std::cout << std::endl; print(span234);}Ответ
std::span можно создать как из всего массива, так и из отдельно указателя и длины.
Например, std::span<int> span123{ arr.data(), 3 }; передает:
- Адрес начала массива (
arr.data()) как указатель, 3как длину.
std::span<int> span234{ arr.data() + 1, 3 }; передает:
- Адрес второго элемента массива (
arr.data() + 1=&arr[1]) как указатель, 3как длину.
При печати span123 напечатаются первые 3 элемента.
При печати span234 напечатаются 3 элемента, начиная со второго (2, 3 и 4).
24. Понимание метода data
Заголовок раздела «24. Понимание метода data»Что будет, если сменить
std::span<int> span123{ arr.data(), 3 };std::span<int> span234{ arr.data() + 1, 3 };на
std::span<int> span123{ wholeSpan.data(), 3 };std::span<int> span234{ span123.data() + 1, 3 };в предыдущем примере?
Ответ:
data для std::span возвращает сохраненный в нем указатель.
Результат будет тот же, поскольку и в wholeSpan и в span123 были
сохранены указатели на первый элемент из arr.
25. Подспан методом subspan
Заголовок раздела «25. Подспан методом subspan»#include <iostream>#include <array>#include <span>
void print(std::span<int> s){ for (size_t i = 0; i < s.size(); i++) { std::cout << s[i] << std::endl; }}
int main(){ std::array<int, 4> arr{1, 2, 3, 4}; std::span<int> wholeSpan{ arr }; std::span<int> span123{ wholeSpan.subspan(0, 3) }; std::span<int> span234{ wholeSpan.subspan(1, 3) };
print(wholeSpan);
std::cout << std::endl; print(span123);
std::cout << std::endl; print(span234);}Ответ
subspan принимает 2 аргумента:
- С какого индекса начать;
- Какая будет длина.
wholeSpan.subspan(0, 3) говорит начать с индекса 0, с длиной 3.
Это эквивалентно wholeSpan.data() + 0, 3.
wholeSpan.subspan(1, 3) говорит начать с индекса 1, с длиной 3.
Это эквивалентно wholeSpan.data() + 1, 3.
Если параметр длины опустить, он определится сам так,
чтобы span шел до конца массива.
Например, вместо wholeSpan.subspan(1, 3) можно сделать wholeSpan.subspan(1).
26. Тонкости subspan
Заголовок раздела «26. Тонкости subspan»Что если в предыдущем примере выйти за рамки массива, например вот так:
std::span<int> s{ wholeSpan.subspan(2, 3) };Ответ
Тут не делается bounds-checking, это просто UB при считывании из позиции 2 в s
Создание некорректного
std::spanне считается UB, по аналогии с указателями, а вот считывание за пределами массива считается.
27. Цикл в обратную сторону (1)
Заголовок раздела «27. Цикл в обратную сторону (1)»Что выведется здесь?
#include <iostream>#include <array>#include <span>#include <cassert>
void print(std::span<int> s){ int lastIndex = static_cast<int>(s.size()) - 1; for (int i { lastIndex }; i >= 0; i--) { size_t si = static_cast<size_t>(i); assert(si < s.size());
std::cout << s[si] << std::endl; }}
int main(){ std::array<int, 4> arr{1, 2, 3, 4}; print({ arr });}Ответ
В примере элементы массива печатаются в обратном порядке.
static_cast используются, чтобы обойти предупреждения
о возможной потере информации (narrowing conversion из size_t в int)
и неявной смене знака (из int в size_t).
28. Цикл в обратную сторону (2)
Заголовок раздела «28. Цикл в обратную сторону (2)»Что выведется здесь?
#include <iostream>#include <array>#include <span>#include <cassert>
void print(std::span<int> s){ for (size_t i { s.size() - 1 }; i >= 0; i--) { assert(i < s.size());
std::cout << s[i] << std::endl; }}
int main(){ std::array<int, 4> arr{1, 2, 3, 4}; print({ arr });}Ответ
Напечатаются все элементы, затем программа крашнется.
Так как i — это тип без знака,
0 - 1 как size_t будет не -1, а случится underflow и число станет максимальным.
Тогда assert(i < s.size()) крашнет программу, поскольку i станет больше размера массива.
Это можно обойти, если делать проверку вручную:
void print(std::span<int> s){ if (s.size() == 0) { return; } size_t i { s.size() - 1 }; while (true) { assert(i < s.size()); std::cout << s[i] << std::endl; if (i == 0) { return; } i--; }}Или считать максимальное значение недопустимым индексом и делать проверку на -1 как size_t:
void print(std::span<int> s){ for (size_t i { s.size() - 1 }; i != static_cast<size_t>(-1); i--) { assert(i < s.size()); std::cout << s[i] << std::endl; }}29. Итерация указателями (1)
Заголовок раздела «29. Итерация указателями (1)»#include <iostream>#include <array>#include <cassert>
void print(int* start, int* end){ for (int* current{start}; current <= end; current++) { std::cout << *current << std::endl; }}
int main(){ std::array<int, 4> arr{1, 2, 3, 4}; print(arr.data(), &arr[arr.size() - 1]);}Ответ
Здесь иллюстрируется другой подход к пределам итерации. Цикл работает не исходя из количества элементов в массиве, а исходя из первого и последнего адреса.
30. Итерация указателями (2)
Заголовок раздела «30. Итерация указателями (2)»#include <iostream>#include <array>#include <cassert>
void print(int* start, int* pastEnd){ for (int* current{start}; current != pastEnd; current++) { std::cout << *current << std::endl; }}
int main(){ std::array<int, 4> arr{1, 2, 3, 4}; print(arr.data(), arr.data() + arr.size()); print(arr.begin(), arr.end());}Ответ
Здесь применяется подход итераторов: вместо адреса последнего элемента передается первый адрес после последнего.
arr.begin() возвращает итератор (есть в другой теме) первого элемента,
а arr.end() возвращает итератор конца.
Здесь оба вызова компилируются, потому что итераторы std::array реализованы просто как int* под капотом.