
Сегодня мы поговорим про std::span и как не порезаться на острых углах C++.
Согласно определению на cppreference, шаблон класса span описывает объект, который может ссылаться на непрерывную последовательность объектов, где первый элемент последовательности находится на позиции ноль.
Вообще, с учётом того, что, начиная с С++17 мы уже знакомы с понятием string_view, можно представить, что std::span — это нечто подобное, только действующее для непрерывных участков памяти, которые ещё можно и модифицировать. Но не будем забегать вперед, обо всех свойствах по-порядку.
Итак, std::span — это, по сути, это обёртка вокруг указателя и размера, которая предоставляет удобный интерфейс для работы с массивами, векторами и другими непрерывными контейнерами. Нужно подчеркнуть, что std::span не владеет данными, а лишь предоставляет «вид» на них, что делает его безопасным и эффективным инструментом.
Какова же была в принципе мотивация для появления такого рода шаблона? Вспомним, как мы привыкли работать с самыми что ни на есть классическими массивами. Нам обязательно нужно было передать размер массива (потому как без него поди разбери: это указатель или массив, который мы можем итерировать и сколько мы можем его итерировать):
void print_array(int* arr, std::size_t size) {
for(std::size_t i = 0; i < size; ++i) {
std::cout << arr[i];
}
}
А вот если переписать эту же функцию, но при помощи std::span, получится следующее:
void print_array(std::span<int> arr) {
for(std::size_t i = 0; i < arr.size(); ++i) {
std::cout << arr[i];
}
}
То есть в std::span есть size() — размер массива. Это уже интересно, но это ещё не всё, можно ещё вот так:
void print_array_iter(std::span<int> arr) {
for(auto& elem : arr) {
std::cout << elem;
}
}
Ого, то есть у нас появились и Итераторы. То есть необходимость передавать размер массива отдельно отпала вовсе.
А теперь давайте испытаем наши вышеописанные функции:
int main() {
int data[] = { 1, 2, 3, 4, 5 };
auto data_ptr = new int[] { 6,7,8,9,10 };
std::vector<int> data_vect = { 11,12,13,14,15 };
print_array(data);
print_array(std::span{ data_ptr,5 });
print_array(data_vect);
print_array_iter(data);
print_array_iter(std::span{ data_ptr,5 });
print_array_iter(data_vect);
}
Получилось, действительно, универсально. Единственный момент — при использовании динамического массива (int[]) всё равно придется создать span с указанием размера последовательности. Волшебства не произошло, да и, собственно, не могло. Для таких массивов даже программисты не всегда знают, какой у них размер.
Но, с другой стороны, зона ответственности уже снаружи функций, а сами функции получились действительно универсальны. Также напомню, что span — это своего рода view, который не владеет данными.
А сейчас давайте посмотрим сколько занимает std::span в памяти. По определению существует 2 вида std::span — «статический» и «динамический», — в зависимости от того, каким образом и для какой последовательности он создан.
int main() {
int data[] = { 1, 2, 3, 4, 5 };
auto data_ptr = new int[] { 6,7,8,9,10 };
std::vector<int> data_vect = { 11,12,13,14,15 };
std::span<int> data_span { data };
std::span<int, 5> span_data_ptr { data_ptr, 5 };
//std::span<int> span_data_ptr { data_ptr, 5 };
std::span<int> data_vect_span { data_vect };
std::cout << std::format("sizeof data_span: {}\n", sizeof(data_span));
std::cout << std::format("sizeof span_data_ptr: {}\n", sizeof(span_data_ptr));
std::cout << std::format("sizeof data_vect_span: {}\n", sizeof(data_vect_span));
}
В случае, если span «статический» (применительно к std::span — задаём при создании), то размер самого span будет занимать 8 байт*. Только указатель на внутренний массив, т.к не нужно хранить размер внутреннего массива. Результат работы кода для определения размера самого std::span:

Но это всё стандартно, а давайте попробуем вот так:
std::span<int, 3> span_data_ptr { data_ptr, 5 };
Или вот так:
std::span<int, 5> span_data_ptr { data_ptr, 3 };
И в обоих случаях получим вот такое сообщение:

Ошибка говорит о том, что нельзя создавать std::span с фиксированным размером, если он не совпадает с размером передаваемой последовательности.
А вот сейчас ещё парочку экспериментов, которые явно покажут, что можно делать с std::span
auto data_ptr = new int[] { 6, 7, 8, 9, 10 };
print_array(std::span<int, 3>{data_ptr + 1, 3});
std::vector<int> data_vect = { 11,12,13,14,15 };
print_array(data_vect);
data_vect.push_back(55);
print_array(data_vect);
И соответствующий вывод:

Давайте посмотрим подробнее, что получилось. В первой части мы задали явно диапазон и размерность span, то есть мы можем указывать любой участок для работы с нашими изначальными данными.
А во второй части примера создали span на основе вектора и дальше, увеличивая вектор, получили автоматически проход по всему вектору, а не только по той части, которая была на момент создания. Динамика в действии.
До этого мы рассматривали только возможность считывания данных из span, а что, если мы хотим их изменить?
template<typename T>
void change(std::span<T> arr) {
std::transform(arr.begin(), arr.end(), arr.begin(), std::negate());
}
int main() {
int data[] = { 1, 2, 3, 4, 5 };
change<int>(data);
print_array(data);
}
То есть мы можем обращаться с исходными данными как с обычным массивом.
Но ведь бывают случаи, когда это не нужно, как поступать в таком случае? Достаточно добавить const в нашу функцию, и код не скомпилируется :)
template<typename T>
void change(std::span<const T> arr) {
std::transform(arr.begin(), arr.end(), arr.begin(), std::negate());
}
Идём дальше. С учётом того, что через span мы всегда работаем с непрерывной областью памяти — почему бы не предоставить нам доступ к указателю на начало последовательности. И действительно — есть и такой метод. Хотя это уже попахивает тем старым добрым С++, от которого мы тут так хотим уйти.
void print_array(const int* arr, std::size_t size) {
for (std::size_t i = 0; i < size; ++i) {
std::cout << arr[i];
}
}
void print(std::span<const int> arr)
{
print_array(arr.data(), arr.size());
}
А сейчас вернёмся к нашему примеру с изменением изначального массива, но задействуем возможность использовать subspan (подпоследовательности):
template<typename T>
void change(std::span<T> arr) {
std::transform(arr.begin(), arr.end(), arr.begin(), std::negate());
}
int main() {
int data[] = { 1, 2, 3, 4, 5 };
change<int>(std::span{ data }.subspan(1,3));
print_array(data);
}
Результат выполнения:

Выше получился отличный пример частичной модификации.
Подводя итоги по span, нужно отметить что он отлично подходит не только для входных параметров, он также может быть полезен как тип возвращаемого значения. Это может быть компромиссом между ссылками и копиями с дополнительной гибкостью для обработки отсутствующих значений. Но тут главное помнить, что span не владеет памятью, а лишь ссылается на нее.
Что нас ждёт в будущем:
C++26: span::at()
В C++26 std::span получит метод at(), который обеспечивает доступ к элементам с проверкой границ аналогично std::vector::at(). Это гарантирует безопасное индексирование, выбрасывая исключение std::out_of_range, если доступ осуществляется по недопустимому индексу.
C++26: span над списком инициализации
В C++26 std::span можно будет создавать из std::initializer_list, если все элементы лежат в одном блоке памяти, что раньше было запрещено.
Давайте посмотрим на ряд основных преимуществ использования std::span:
Производительность: хорошая альтернатива ссылкам с дополнительной универсализацией — избегаем ненужных копий.
Безопасность: возможность работы с размером контейнера с помощью size()
Совместимость: работает с разными типами контейнеров, такими как std::array, std::vector или сырые массивы.
Упрощение обработки отсутствующих данных: в отличие от возврата const std::vector&, где вы должны возвращать допустимую ссылку, std::span может просто вернуть пустой span ({}).
Конечно, это не серебряная пуля — и бежать рефакторить весь код сейчас уж точно не нужно. Но для текущей разработки иметь этот инструмент в голове и в руках явно полезно.
Полезные ссылки:
https://en.cppreference.com/w/cpp/container/span
https://www.studyplan.dev/pro-cpp/span
https://dev.to/pgradot/let-s-try-c-20-std-span-1682
https://www.cppstories.com/2023/span-cpp20/
Статья подготовлена для будущих студентов курса «Разработчик С++20». Узнать больше про новые стандарты языка, его возможности и программу обучения — по ссылке.