1. Основы std::span: память, view и const-контракт
Если вы пишете учебные программы, сначала всё выглядит просто: вот у меня есть std::vector<int>, я передаю его в функцию, и всё хорошо. Но затем в коде внезапно появляется обычный C-массив, потом std::array, а потом вы хотите обработать не «всё», а, например, только первые 10 элементов. И начинается классика: либо вы пишете три версии одной и той же функции, либо переходите на «указатель + размер» и начинаете жить в режиме “пожалуйста, не перепутай параметры местами”.
std::span — это аккуратная стандартная упаковка идеи «указатель на начало + длина», только в виде нормального типа с методами и понятным контрактом. Он существует именно как вокабулярный тип стандартной библиотеки: то есть тип, который вы используете в интерфейсах, чтобы другие люди (и вы через неделю) сразу понимали смысл. Сам факт, что span подробно обсуждается и правится в стандартных документах, хорошо показывает его «официальный» статус в языке.
Что такое непрерывные данные: простая модель памяти
Перед тем как брать std::span, нужно почувствовать, на что он вообще может смотреть. «Непрерывные данные» означают, что элементы лежат в памяти подряд, без дырок: один за другим, как места в кинотеатре (только в кинотеатре места иногда ломаются, а в памяти — если вы сами не устроите UB).
Такими контейнерами являются C-массив T arr[N], std::array<T, N>, std::vector<T> (его элементы лежат подряд, пока вектор не «переехал» при росте), и строка std::string (символы тоже лежат подряд, и у них есть data() и size()). А вот, например, std::list — не непрерывный: там каждый элемент живёт отдельно, как коты, которые не согласны лежать рядом.
Чтобы закрепить ощущение, вот условная схема того, что значит «подряд»:
flowchart LR
A["arr[0]"] --> B["arr[1]"] --> C["arr[2]"] --> D["arr[3]"]
subgraph Memory["Память: подряд, без дырок"]
A
B
C
D
end
И именно на такой «плотный ряд» std::span и рассчитан.
std::span<T>: «указатель + размер», только по-человечески
Теперь можно знакомиться с героем лекции. std::span<T> — это невладеющее представление (view) на последовательность элементов T, которые лежат непрерывно. Внутри это действительно очень близко к паре (T*, size), но с нормальным интерфейсом: size(), empty(), operator[], range-for и так далее.
Важно не перепутать философию. std::vector<int> — это коробка с числами и своим складом (памятью). std::span<int> — это стикер на коробке: «смотри на элементы вот отсюда и вот столько». Если коробка исчезла, стикер не начинает внезапно хранить числа в кармане.
Минимальный пример «создали спан и пробежались»:
#include <iostream>
#include <span>
int main() {
int a[] = {10, 20, 30};
std::span<int> s(a); // view на весь массив
std::cout << s.size() << '\n'; // 3
std::cout << s[1] << '\n'; // 20
}
Обратите внимание на стиль: span часто передают по значению, потому что он маленький (как два числа: адрес и длина). Передавать const std::span<int>& обычно не нужно: вы не экономите копирование «чего-то большого», там нечему быть большим.
std::span<const T> vs std::span<T>: «читаю» и «меняю элементы»
Когда новичок впервые видит span, он часто делает одну и ту же вещь: везде пишет std::span<int>, даже если функция только читает. Это примерно как брать бензопилу, чтобы порезать хлеб: иногда сработает, но окружающим тревожно.
Если функция не должна менять элементы, то правильный выбор — std::span<const T>. Это сразу «зашивает» в тип параметра обещание: «я читаю, но не изменяю».
Сравните две функции (и обратите внимание, что код почти одинаковый — разница именно в контракте):
#include <span>
int sum(std::span<const int> xs) {
int total = 0;
for (int x : xs) total += x;
return total;
}
#include <span>
void negate_all(std::span<int> xs) {
for (int& x : xs) x = -x;
}
Смысл прост: std::span<int> позволяет менять сами элементы, но по-прежнему не позволяет менять размер (нельзя “добавить элемент в span”, потому что добавлять можно только владельцу, например vector).
3. Откуда брать std::span
Сейчас будет приятная часть: span действительно делает код «универсальнее», но без магии и без шаблонов (пока что). Вы просто берёте непрерывные данные — и делаете на них view.
Из C-массива T arr[N]
После заголовка важно понять: C-массив — это честный непрерывный блок. std::span умеет из него строиться очень удобно:
#include <iostream>
#include <span>
void print_first(std::span<const int> xs) {
if (xs.empty()) return;
std::cout << xs[0] << '\n';
}
int main() {
int a[] = {5, 6, 7};
print_first(a); // span создаётся неявно
}
Здесь происходит полезная вещь: print_first не знает, что такое «массив» как отдельный тип хранения. Она знает только: «мне дали последовательность int».
Из std::array<T, N>
std::array — это «культурный» массив: размер хранится в типе, есть методы, он хорошо дружит с алгоритмами. Для span он тоже отличный источник:
#include <array>
#include <iostream>
#include <span>
int main() {
std::array<int, 4> a{1, 2, 3, 4};
std::span<const int> s(a);
std::cout << s.size() << '\n'; // 4
}
Из std::vector<T>
Вектор тоже подходит идеально, но с оговоркой: память у вектора может перевыделяться при росте. Сегодня мы это просто фиксируем как факт; подробно «когда ломается view» будем разбирать в следующей лекции дня. Пока используем span как короткоживущий объект «рядом с вызовом функции».
#include <iostream>
#include <span>
#include <vector>
int sum(std::span<const int> xs) {
int total = 0;
for (int x : xs) total += x;
return total;
}
int main() {
std::vector<int> v{10, 20, 30};
std::cout << sum(v) << '\n'; // 60
}
С точки зрения читаемости это один из самых приятных моментов: функция принимает не «вектор», а «диапазон элементов». И внезапно вы можете скормить ей и массив, и std::array, и std::vector.
std::span для std::string: да, строка непрерывная, но это не string_view
Переходим к тонкому месту: “span на строку”. Символы std::string лежат подряд, поэтому можно сделать std::span<const char> и обработать строку как массив символов. Это иногда полезно, когда вы хотите именно «байтовое» или «символьное» представление без особенностей строковых методов.
Но важно не путать роли. std::string_view — это view именно строки (и там удобно find, substr, remove_prefix). std::span<const char> — это view на символы как элементы массива. Он не «понимает» подстроки как строковые операции, но зато универсален как диапазон char.
Пример: посчитаем, сколько в тексте цифр (не очень умно, зато показательно):
#include <iostream>
#include <span>
#include <string>
int count_digits(std::span<const char> chars) {
int c = 0;
for (char ch : chars) {
if (ch >= '0' && ch <= '9') ++c;
}
return c;
}
int main() {
std::string s = "Room 101";
std::cout << count_digits(std::span<const char>(s.data(), s.size())) << '\n'; // 3
}
Почему нельзя просто count_digits(s)? Потому что std::string не обязан неявно превращаться в span. Мы создаём span явно через data() и size(), и это даже хорошо: так вы меньше шансов «случайно» получить view туда, куда не хотели.
4. Ежедневные операции со span
Сейчас полезно приземлиться: в 90% случаев от span вам нужны очень простые вещи. Вы проверяете empty(), узнаёте size(), пробегаете range-for, иногда берёте элемент по индексу. По сути это тот же набор, что вы уже делали для std::vector и std::string, только теперь он работает для «чужих данных».
Небольшой пример «распечатать диапазон красиво»:
#include <iostream>
#include <span>
void print(std::span<const int> xs) {
std::cout << '[';
for (std::size_t i = 0; i < xs.size(); ++i) {
std::cout << xs[i] << (i + 1 == xs.size() ? "" : ", ");
}
std::cout << "]\n";
}
int main() {
int a[] = {3, 1, 4};
print(a); // [3, 1, 4]
}
Обратите внимание: индексация у span такая же «честная», как у массива или vector через operator[]: границы он не проверяет. Поэтому если индекс приходит извне, проверка — ваша ответственность.
subspan(): «окошко» на часть данных без копирования
Теперь самое вкусное: std::span умеет делать «под-диапазоны» без копирования элементов. Это похоже по духу на std::string_view::substr(): вы не создаёте новый массив, вы создаёте новый взгляд на кусочек старого массива.
Это полезно в прикладных задачах постоянно. Например, вы хотите обработать «все элементы, кроме первого», или «первые N», или «середину». И вместо копирования в новый std::vector вы делаете subspan().
Пример: берём средние два элемента:
#include <iostream>
#include <span>
int main() {
int a[] = {10, 20, 30, 40};
std::span<int> all(a);
std::span<int> mid = all.subspan(1, 2); // {20, 30}
std::cout << mid[0] << ' ' << mid[1] << '\n'; // 20 30
}
Важно: subspan() не меняет владельца и не двигает данные. Он просто говорит: «начинай смотреть с позиции offset, и смотри count элементов». Если указали неверные границы — получите ошибку логики (а иногда и UB), как и с любой индексацией.
5. Практический пример: добавляем span в “MiniStats”
Чтобы std::span не остался «умной теорией», давайте встроим его в маленький консольный кусочек приложения, которое мы постепенно развиваем по курсу. Пусть у нас будет модуль “MiniStats”: он умеет считать сумму и среднее по последовательности чисел, причём источник данных не важен: массив, std::array или std::vector.
Считаем сумму и среднее по span<const int>
Сделаем две функции, максимально простые и «читаемые глазами». Мы пока не лезем в сложные модели данных: просто работаем с числами и выводом.
#include <iostream>
#include <span>
int sum(std::span<const int> xs) {
int total = 0;
for (int x : xs) total += x;
return total;
}
double average(std::span<const int> xs) {
if (xs.empty()) return 0.0;
return static_cast<double>(sum(xs)) / xs.size();
}
Здесь приятно то, что average умеет принимать любые непрерывные числа: ему не нужен vector. И он честно обрабатывает пустой диапазон.
Вызываем эти функции и для массива, и для vector
Теперь покажем «вот зачем это всё было»:
#include <iostream>
#include <span>
#include <vector>
int sum(std::span<const int> xs);
double average(std::span<const int> xs);
int main() {
int a[] = {1, 2, 3};
std::vector<int> v{10, 20, 30, 40};
std::cout << sum(a) << '\n'; // 6
std::cout << average(v) << '\n'; // 25
}
Фокус в том, что у нас одни и те же функции работают с разными источниками данных. И их сигнатуры не «врут»: sum и average не владеют числами и не меняют их — они просто читают.
Обрабатываем только кусочек данных через subspan()
Очень типичный сценарий в реальных программах: «посчитай статистику не по всем числам, а по хвосту/префиксу». Покажем это:
#include <iostream>
#include <span>
#include <vector>
int sum(std::span<const int> xs);
int main() {
std::vector<int> v{5, 100, 100, 100};
std::span<const int> all(v);
std::span<const int> tail = all.subspan(1); // всё, кроме первого
std::cout << sum(tail) << '\n'; // 300
}
Обратите внимание: мы не копировали {100, 100, 100} в новый контейнер. Мы просто сделали «окошко» на нужный диапазон.
6. Типичные ошибки при работе с std::span
Ошибка №1: воспринимать span как контейнер и пытаться “добавлять элементы”.
std::span — не владелец, у него нет push_back и не может быть resize. Если вы ловите себя на мысли “мне бы сюда ещё один элемент дописать”, это знак, что вам нужен владеющий контейнер (std::vector) или вы должны менять владельца, а не view.
Ошибка №2: принимать std::span<T>, хотя функция ничего не меняет.
Такой код компилируется, но интерфейс становится опаснее: вызывающий код теперь думает, что функция может менять данные. Правильная привычка: по умолчанию принимать std::span<const T>, а std::span<T> использовать только там, где изменение элементов — часть контракта.
Ошибка №3: строить span и хранить его “где-то подольше”, не думая о владельце.
span не продлевает жизнь данных. Если владелец исчез (например, это был локальный std::vector внутри функции), span становится бесполезной бумажкой с адресом в никуда. Точно так же неприятно, когда владелец “переезжает” (особенно это актуально для std::vector при росте). Сегодня достаточно запомнить правило: span должен жить недолго и рядом с местом использования.
Ошибка №4: забыть, что operator[] не проверяет границы.
У span индексация такая же «без страховки», как у массивов и у vector через []. Если индекс вычисляется или приходит из ввода, сначала проверяйте i < xs.size(). Иначе вы получите либо мусор, либо падение, либо (самое “весёлое”) «иногда работает».
Ошибка №5: делать span на std::string и ожидать “строковых” операций.
std::span<const char> — это диапазон char, не строка. Он не знает про find, starts_with и “подстроки” как текст. Если задача про текст — чаще удобнее std::string_view. Если задача про «массив байт/символов» — тогда span уместен.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ