Avatar ·

Что такое std::span в C++ и когда его следует использовать?

Привет, коллеги! Я пишу на C++ уже около года, но постоянно сталкиваюсь с проблемой передачи массивов и буферов в функции. Обычно я передаю указатель на первый элемент и размер отдельно, но это выглядит громоздко и чревато ошибками. Например: ```cpp void process_data(int* arr, size_t size) { for (size_t i = 0; i < size; ++i) { // что-то делаем с arr[i] } } int main() { int data[] = {1, 2, 3, 4, 5}; process_data(data, 5); // работает, но легко ошибиться с размером process_data(data, 100); // undefined behavior, ошибка! } ``` Недавно я наткнулся на `std::span` в C++20. Почитал документацию, но до конца не понял, чем он лучше обычного указателя + размера. Вроде бы это просто "view" на массив. Зачем он нужен? Когда его стоит использовать вместо передачи вектора по ссылке или пары (указатель, размер)? Есть ли случаи, где `span` не подходит? Буду благодарен за примеры из реальной жизни. <<>>

Что такое std::span?

std::span<T> — это невладеющий "вид" (view) на непрерывную последовательность объектов типа T. Он появился в C++20 как часть стандартной библиотеки. По сути, это легковесная обёртка над парой (указатель, размер), которая предоставляет безопасный и удобный интерфейс для работы с подмассивами, буферами и любыми непрерывными диапазонами данных.

Почему не указатель + размер?

Основные проблемы подхода с указателем и размером:

  • Ошибки с размером: легко передать неверный размер или забыть его передать.
  • Небезопасность: нет проверки границ (если не использовать at() в span).
  • Неудобство: нужно передавать два параметра, код становится менее читаемым.
  • Отсутствие STL-совместимости: нельзя использовать range-based for, алгоритмы STL.

std::span решает все эти проблемы. Он знает свой размер, поддерживает итераторы, безопасный доступ через at() и легко конвертируется из массивов, std::vector, std::array и даже сырых указателей с размером.

Примеры использования

1. Функция, работающая с любым непрерывным диапазоном:

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

void print_and_sum(std::span<const int> data) {
    int sum = 0;
    for (int x : data) { // range-based for работает!
        std::cout << x << " ";
        sum += x;
    }
    std::cout << "\nSum: " << sum << "\n";
}

int main() {
    int raw_array[] = {1, 2, 3};
    std::vector vec = {4, 5, 6, 7};
    std::array arr = {8, 9, 10};

    print_and_sum(raw_array); // из C-массива
    print_and_sum(vec);       // из std::vector
    print_and_sum(arr);       // из std::array
}

2. Безопасное создание поддиапазона (subspan):

void process_subset(std::span<int> data) {
    // Берём первые 3 элемента, если их достаточно
    if (data.size() >= 3) {
        auto first_three = data.first(3); // subspan из 3 элементов
        for (int& x : first_three) {
            x *= 2;
        }
    }
}

3. Работа с сырыми данными (например, из сети или файла):

void handle_buffer(std::span<std::byte> buffer) {
    // Безопасное преобразование в другой тип
    auto int_view = std::as_writable_bytes(std::span<int>{
        reinterpret_cast<int*>(buffer.data()),
        buffer.size() / sizeof(int)
    });
}

Когда использовать std::span?

  • Функции, которые принимают массив/буфер для чтения или записи — вместо const std::vector<T>& (если не нужен владеющий контейнер) или пары (указатель, размер).
  • Интерфейсы, работающие с подмассивами — first(), last(), subspan() очень удобны.
  • Код, который должен быть гибким — span принимает всё: C-массивы, std::vector, std::array, инициализационные списки.
  • Оптимизация — span не копирует данные, это просто ссылка. Передача по значению (copy) дешёвая (обычно 16 байт на 64-битной системе).

Когда НЕ стоит использовать std::span?

  • Когда нужно владение данными — используйте std::vector или std::array.
  • Когда данные могут быть изменены извне — span не защищает от инвалидации (если исходный контейнер удалит или перераспределит память).
  • Для хранения в классе — span не владеет данными, поэтому если объект переживёт данные — будет висячая ссылка.

std::span — это мощный и безопасный инструмент, который делает код чище и менее подверженным ошибкам. Рекомендую использовать его везде, где нужен невладеющий вид на непрерывные данные.

<<>>

std::span vs. gsl::span и другие альтернативы

До C++20 многие использовали gsl::span из библиотеки Guidelines Support Library (GSL). std::span является стандартизированной версией этой идеи и полностью её заменяет. Основные отличия минимальны, но std::span теперь часть стандарта, что гарантирует его поддержку и переносимость.

Когда span может быть опасен

Важно помнить: std::span — это невладеющая ссылка. Если вы передаёте span на данные, которые могут быть удалены или перемещены, вы рискуете получить висячий указатель. Например:

std::span<int> get_span() {
    std::vector<int> vec = {1, 2, 3};
    return vec; // ВНИМАНИЕ: после выхода из функции vec уничтожится, span повиснет!
}

Такой код приведёт к неопределённому поведению. Всегда следите за временем жизни данных, на которые ссылается span.

Производительность

std::span обычно состоит из двух членов: указатель и размер. Передача по значению дешёвая (как передача двух size_t). Однако компилятор может оптимизировать и передавать его через регистры. Использование span не должно быть медленнее, чем ручная передача пары (указатель, размер), но код становится значительно безопаснее.

Если вам нужен только доступ на чтение, используйте std::span<const T> — это явно указывает, что функция не будет менять данные.