Что такое std::span в C++ и когда его следует использовать?
Что такое 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> — это явно указывает, что функция не будет менять данные.
Ответов пока нет.
Войдите чтобы оставить ответ