библиотека — невладеющие представленияlibrary — non-owning views
Невладеющее представление непрерывного диапазона элементов — указатель плюс длина, для произвольного типа элемента. То же самое, что string_view из C++17, только не только для char.A non-owning view of a contiguous range of elements — a pointer plus a length, for any element type. The same idea as C++17's string_view, just not limited to char.
std::span<T> — невладеющее представление непрерывного диапазона элементов типа T: внутри буквально указатель на первый элемент плюс длина, без владения памятью. Это прямое обобщение идеи std::string_view (см. cpp17/string-view.html) с частного случая "char-массив" на произвольный тип элемента — span<int>, span<Point>, span<std::byte>. span принимает C-массив, std::vector, std::array, любой contiguous-контейнер — без копирования данных.std::span<T> is a non-owning view of a contiguous range of elements of type T: under the hood it's literally a pointer to the first element plus a length, owning none of the memory. It's a direct generalization of the idea behind std::string_view (see cpp17/string-view.html) from the special case of "a char array" to any element type — span<int>, span<Point>, span<std::byte>. span accepts a C array, a std::vector, a std::array, any contiguous container — without copying the data.
// либо шаблон — раздувает код на каждый тип контейнера// either a template — bloats code per container type
template <class Container>
void process(Container& c) { for (auto& x : c) { /*...*//*...*/ } }
// либо указатель+размер раздельными параметрами —// or a pointer+size as two separate parameters —
// легко перепутать порядок или забыть один из них// easy to swap the order or forget one of them
void process(int* data, size_t size);
void process(std::span<int> s) {
for (int x : s) { /*...*//*...*/ } // range-for работает// range-for just works
}
int arr[5] = {1,2,3,4,5};
std::vector<int> v = {1,2,3};
std::array<int, 4> a = {1,2,3,4};
process(arr); // одна сигнатура принимает все три —// one signature accepts all three —
process(v); // без шаблона, без копирования// no template, no copying
process(a);
Функция, желающая принимать "любой непрерывный массив int-ов", не имела единого удобного способа сделать это до span:A function wanting to accept "any contiguous array of ints" had no single convenient way to do it before span:
Шаблон, параметризованный по типу контейнера, инстанциируется заново на каждую комбинацию (vector, array, C-массив) — рост объёма кода, увеличение времени компиляции, и всё равно нет способа передать "часть вектора" (через .data()+offset) без ещё одной перегрузки на сырой указатель.A template parameterized on the container type gets instantiated anew for every combination (vector, array, a C array) — code bloat, longer compile times, and there's still no way to pass "part of a vector" (via .data()+offset) without yet another overload taking a raw pointer.
void f(int* data, size_t size) — раздельная передача двух логически связанных значений, ничем не мешающая вызвать функцию с несогласованной парой (указатель от одного массива, размер от другого) или забыть один из параметров при рефакторинге сигнатуры.void f(int* data, size_t size) passes two logically linked values separately, with nothing stopping a call with an inconsistent pair (a pointer from one array, a size from another) or a forgotten parameter during a signature refactor.
Один нешаблонный тип параметра функции, конструируемый неявно из C-массива, vector, array или части любого из них — с operator[], .size(), .begin()/.end() для range-for, и .first(n)/.last(n)/.subspan(offset, count) для получения под-диапазонов без ручной арифметики указателей.A single non-template parameter type, implicitly constructible from a C array, a vector, an array, or a slice of any of them — with operator[], .size(), .begin()/.end() for range-for, and .first(n)/.last(n)/.subspan(offset, count) for getting sub-ranges without manual pointer arithmetic.
Как и string_view, span внутри — пара "указатель + размер", ничего не аллоцирующая и не освобождающая. Отличие — необязательный второй шаблонный параметр Extent: std::span<int> (динамический размер, по умолчанию std::dynamic_extent) хранит размер в рантайме как поле, а std::span<int, 10> (статический размер) знает размер на этапе компиляции — само число становится частью типа, и объект span в этом случае может обойтись без поля размера вовсе.Like string_view, span is internally a "pointer + size" pair, allocating and freeing nothing. The difference is an optional second template parameter, Extent: std::span<int> (dynamic extent, defaulting to std::dynamic_extent) stores the size at runtime as a field, while std::span<int, 10> (static extent) knows the size at compile time — the number itself becomes part of the type, and in that case the span object can skip the size field entirely.
#include <span>
void sum_all(std::span<const int> s) { // динамический размер — обычный выбор// dynamic extent — the usual choice
int total = 0;
for (int x : s) total += x;
}
int arr[10];
std::span<int, 10> fixed = arr; // статический размер — известен как часть типа// static extent — known as part of the type
std::span<int> dyn = fixed; // static -> dynamic: implicit, разрешено// static -> dynamic: implicit, allowed
// std::span<int, 10> back = dyn; // dynamic -> static: НЕ implicit// std::span<int, 10> back = dyn; // dynamic -> static: NOT implicit
std::vector<int> v = {1,2,3,4,5};
std::span<int> view(v);
auto firstTwo = view.first(2); // {1, 2}, без копирования// {1, 2}, no copying
auto middle = view.subspan(1, 3); // {2, 3, 4}, без копирования// {2, 3, 4}, no copying
Три метода — .first(n), .last(n), .subspan(offset, count) — возвращают новый span, указывающий внутрь того же буфера с другими границами, без единой аллокации или копии элемента. Это ровно то же отношение к данным, что у string_view::substr, только для произвольного типа элемента, а не только char:Three methods — .first(n), .last(n), .subspan(offset, count) — return a new span pointing into the same buffer with different bounds, with no allocation or element copy whatsoever. This is exactly the same relationship to the data as string_view::substr, just for any element type, not only char:
std::vector<int> data = {10,20,30,40,50,60};
std::span<int> s(data);
auto head = s.first(2); // {10, 20} — первые 2 элемента// {10, 20} — the first 2 elements
auto tail = s.last(2); // {50, 60} — последние 2 элемента// {50, 60} — the last 2 elements
auto middlePart = s.subspan(2, 3); // {30, 40, 50} — 3 элемента, начиная с индекса 2// {30, 40, 50} — 3 elements starting at index 2
auto fromMiddle = s.subspan(3); // {40, 50, 60} — count опущен: "до конца"// {40, 50, 60} — count omitted: "to the end"
// все четыре — span, смотрящие в тот же буфер data, ни одной копии// all four are spans pointing into the same data buffer, zero copies
Функция обработки числовых данных, которая раньше существовала как шаблон (чтобы принимать и C-массив, и vector) либо как пара перегрузок — с span достаточно одной нешаблонной сигнатуры.A numeric-data processing function that used to exist either as a template (to accept both a C array and a vector) or as a pair of overloads — with span, a single non-template signature is enough.
double average(std::span<const double> values) {
if (values.empty()) return 0.0;
double total = 0.0;
for (double v : values) total += v;
return total / values.size();
}
double raw[] = {1.0, 2.0, 3.0};
std::vector<double> vec = {4.0, 5.0};
average(raw); // оба вызова — одна и та же нешаблонная функция// both calls hit the same non-template function
average(vec);
Функция, работающая всегда ровно с 4 числами (например, компонентами RGBA-цвета) — статический размер документирует это прямо в типе и не даёт передать span другой длины.A function that always works with exactly 4 numbers (say, the components of an RGBA color) — a static extent documents this right in the type and rejects a span of any other length.
void apply_rgba(std::span<const float, 4> color) {
// color.size() == 4 гарантированно, проверено на этапе компиляции// color.size() == 4 is guaranteed, checked at compile time
set_pixel(color[0], color[1], color[2], color[3]);
}
std::array<float, 4> white = {1.f, 1.f, 1.f, 1.f};
apply_rgba(white); // ok — размер совпадает// ok — size matches
// std::array<float, 3> rgb = {1.f, 1.f, 1.f};// std::array<float, 3> rgb = {1.f, 1.f, 1.f};
// apply_rgba(rgb); // ошибка компиляции — 3 != 4// apply_rgba(rgb); // compile error — 3 != 4
Обработка одного "чанка" внутри большого буфера сетевых данных или файла, загруженного целиком в память — subspan передаёт нужный диапазон в функцию, не выделяя под него отдельную память.Processing one "chunk" inside a large buffer of network data, or a file loaded entirely into memory — subspan hands the needed range to a function without allocating separate memory for it.
void process_chunk(std::span<const std::byte> chunk);
void process_packet(std::span<const std::byte> packet) {
constexpr std::size_t header_size = 16;
auto header = packet.first(header_size);
auto payload = packet.subspan(header_size); // "до конца" без явного count// "to the end" with no explicit count
process_chunk(payload); // без копии payload из packet// no copy of payload out of packet
}
Для кода, который просто перебирает элементы, span визуально неотличим от vector или array — переход на span в существующей функции часто не требует менять тело функции вовсе, только сигнатуру параметра.For code that simply iterates over elements, span is visually indistinguishable from a vector or an array — switching an existing function to span often requires no change to the function body at all, only the parameter's type.
void print_all(std::span<const int> values) {
for (int v : values) { // тот же синтаксис, что для vector<int> или int[]// the same syntax as for vector<int> or int[]
std::cout << v << ' ';
}
std::cout << '\n';
}
Ровно как string_view (см. гочу "Dangling" в cpp17/string-view.html), span становится висячим, если исходный контейнер уничтожается или реаллоцируется раньше, чем сам span — например, span на std::vector, который затем растёт через push_back и переезжает в новый буфер, или span на временный вектор, вернувшийся по значению из функции и уничтоженный в конце выражения. Компилятор не предупреждает — это тот же класс багов use-after-free.Exactly like string_view (see the "Dangling" gotcha in cpp17/string-view.html), a span goes dangling if the source container is destroyed or reallocated before the span itself — for instance, a span over a std::vector that then grows via push_back and moves to a new buffer, or a span over a temporary vector returned by value from a function and destroyed at the end of the expression. The compiler gives no warning — it's the same class of use-after-free bugs.
span::operator[] не проверяет границы — выход за пределы диапазона это UB, как и у raw array или vector::operator[]. В отличие от std::vector, у span в базовом C++20 нет safe-access метода вроде .at() с проверкой и исключением при выходе за границы (появилось позже в некоторых предложениях/реализациях, но не в самом C++20) — дисциплина проверки границ целиком на вызывающем коде.span::operator[] performs no bounds checking — going out of range is UB, just as with a raw array or vector::operator[]. Unlike std::vector, plain C++20 span has no safe-access method like .at() that checks bounds and throws (this arrived later in some proposals/implementations, but not in C++20 itself) — bounds-checking discipline is entirely on the calling code.
std::span<T, N> и std::span<T> (то есть std::span<T, std::dynamic_extent>) — не просто разные значения одного шаблона, а формально разные типы. Функция, параметризованная над span<T> (динамический), не принимает span<T, N> (статический) без явного или неявного преобразования — но преобразование в эту сторону как раз implicit (static → dynamic всегда безопасно, размер известен и просто "забывается" в рантайм-поле). Обратное преобразование (dynamic → static) implicit не является — компилятор не может на этапе компиляции доказать, что рантайм-размер совпадает с N.std::span<T, N> and std::span<T> (i.e. std::span<T, std::dynamic_extent>) aren't just different values of one template — they're formally distinct types. A function parameterized over span<T> (dynamic) does not accept a span<T, N> (static) without a conversion — but that particular direction is implicit (static → dynamic is always safe, the size is known and simply "forgotten" into a runtime field). The reverse (dynamic → static) is not implicit — the compiler can't prove at compile time that the runtime size matches N.