STL

std::span: безопасная работа с массивами без копирования в C++20

Введение

Передача «массива и его размера» в функцию — одна из самых старых и самых проблемных идиом C++: void process(int* data, size_t size). Она не несёт информации о том, кто владеет памятью, не защищает от выхода за границы, заставляет дублировать код для std::vector, std::array, C-массивов и сырых указателей. C++20 вводит std::span<T> — невладеющий, легковесный «вид» на непрерывный массив элементов любого происхождения.

Концепция

std::span<T> хранит указатель на начало данных и их размер (либо размер фиксируется на этапе компиляции, если использовать std::span<T, N>). Он не владеет памятью — не выделяет и не освобождает её, а просто предоставляет единый интерфейс (operator[], begin()/end(), size(), subspan()) для любого непрерывного диапазона: C-массива, std::vector, std::array, части другого span.

Главное преимущество — функция, принимающая std::span<T>, может работать с любым из перечисленных источников без перегрузок и без копирования данных, при этом сохраняя информацию о границах для дополнительных проверок (в debug-сборках многие реализации STL добавляют проверку выхода за границы).

Пример кода

#include <span>
#include <vector>
#include <array>
#include <iostream>
#include <numeric>

// Единая функция работает с любым непрерывным контейнером
double average(std::span<const int> values) {
    if (values.empty()) return 0.0;
    long long sum = std::accumulate(values.begin(), values.end(), 0LL);
    return static_cast<double>(sum) / values.size();
}

void printFirstN(std::span<const int> values, size_t n) {
    auto sub = values.subspan(0, std::min(n, values.size()));
    for (int v : sub) std::cout << v << " ";
    std::cout << "n";
}

void scaleInPlace(std::span<int> values, int factor) {
    for (auto& v : values) v *= factor;
}

int main() {
    int cArray[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {10, 20, 30, 40};
    std::array<int, 3> arr = {7, 8, 9};

    std::cout << "Average (C array): " << average(cArray) << "n";
    std::cout << "Average (vector):  " << average(vec) << "n";
    std::cout << "Average (array):   " << average(arr) << "n";

    printFirstN(vec, 2);

    // Изменение "на месте" без копирования: span<int> неконстантный
    scaleInPlace(vec, 2);
    for (int v : vec) std::cout << v << " ";
    std::cout << "n";

    // span на часть массива
    std::span<int> wholeVec(vec);
    std::span<int> middle = wholeVec.subspan(1, 2); // элементы [1..2]
    for (int v : middle) std::cout << v << " ";
    std::cout << "n";

    return 0;
}

Пояснения к коду

average принимает std::span<const int> — это означает «только для чтения вид на непрерывный массив int«. Благодаря неявным конструкторам span из C-массива, std::vector и std::array, одна и та же функция вызывается с тремя совершенно разными типами без единой перегрузки и без копирования данных в новый буфер. Если бы функция принимала const std::vector<int>&, вызвать её с C-массивом или std::array напрямую было бы невозможно без создания временного вектора (то есть с копированием).

scaleInPlace принимает неконстантный std::span<int> и модифицирует исходные данные на месте — span лишь «смотрит» на чужую память, поэтому изменения через него видны в оригинальном контейнере.

subspan(offset, count) создаёт новый span, который указывает на подмножество исходных данных — без копирования, просто пересчитывая указатель и размер.

Подводные камни и советы

1. span не владеет памятью — если исходный контейнер (например, локальный std::vector) выйдет из области видимости или будет изменён так, что инвалидирует указатели (push_back, вызывающий реаллокацию), span, созданный из него, станет «висячим». Никогда не возвращайте span на локальный временный контейнер. 2. Не путайте span<T> и span<const T> — первый разрешает модификацию данных, второй — только чтение; выбирайте константность осознанно в сигнатурах функций. 3. span — это C++20, для более старых стандартов есть аналоги: gsl::span из Guideline Support Library или собственная мини-реализация. 4. Используйте span в сигнатурах функций как более общую и безопасную замену пар (T* data, size_t size) — это уменьшает число перегрузок и явно документирует, что функция не владеет переданными данными. 5. span сам по себе не проверяет границы при operator[] в release-сборке (поведение как у обычного индексирования массива) — для безопасного доступа используйте at()-подобные обёртки вручную, если такой метод доступен в вашей реализации стандартной библиотеки, либо явно проверяйте index < span.size().

================================================================================