Перейти к содержанию

Последнее обновление:2026-10-12

Дизайн и подводные камни std::span в C++20

0.Исходная мотивация

В традиционном коде на C/C++ мы обычно обращаемся к массиву через указатель + размер, например:

void print(int* arr, int sz) {
    for (int i = 0; i < sz; i++) {
        // todo
    }
}

span позволяет упростить эту операцию до:

void print(std::span<int> arr) {
    for (auto x : arr) {
        // todo
    }
}

Так что же такое span, каковы сценарии его использования и какие у него подводные камни? В этом разделе подробно это обсудим.

1. Что такое span?

В C++20 span — это лёгкий контейнер диапазонного типа, предоставляющий доступ к непрерывному участку памяти. span как представление (view) не владеет данными, а является простой ссылкой на них, и часто используется вместо традиционных C-массивов или указателей. Он может вмещать массивы и стандартные контейнеры произвольных типов, такие как std::vector, и предлагает более безопасный и современный способ работы с этими данными.

Несколько ключевых моментов:

  • Непрерывная память
  • Не владеет данными
  • Ссылка

Исходный код span находится в gcc в libstdc++-v3/include/std/span.

template <typename _Type, size_t _Extent = dynamic_extent>
class span {};

Тип span включает два важных параметра:

  • _Type: тип элементов.
  • Extent: размер массива. Если равен dynamic_extent (значение по умолчанию), это означает, что размер представления может меняться динамически.

2. Динамический и статический span

У span есть два режима: статический и динамический.

2.1 Статический span

Статический span определяет свой размер на этапе компиляции, его Extent — константное значение. При использовании статического span нам нужно знать размер данных во время компиляции.

Например:

int arr[5] = {1, 2, 3, 4, 5};
std::span<int, 5> s(arr);  // 静态 span,大小为 5

Размер статического span фиксирован на этапе компиляции, поэтому компилятор может выполнять некоторые оптимизации — например, отказаться от проверки границ. Но поскольку Extent — константа, статический span не может изменять размер во время выполнения.

2.2 Динамический span

Размер динамического span определяется во время выполнения, Extent по умолчанию равен dynamic_extent. Такой span не требует знать размер на этапе компиляции и подстраивается под массивы или контейнеры любого размера.

Например:

int arr[5] = {1, 2, 3, 4, 5};
std::span<int> s(arr);  // 动态 span,大小在运行时确定

Динамический span подходит для случаев, когда размер невозможно определить во время компиляции; он даёт большую гибкость.

В исходниках gcc реализация довольно проста: по умолчанию это очень большое целое число (беззнаковое -1), а фактическое вычисление происходит при конструировании.

inline constexpr size_t dynamic_extent = static_cast<size_t>(-1);

Например: здесь для установки внутреннего размера используется ranges::size, см. код ниже.

std::vector<int> data{1, 2, 3};
std::span<int> s(data);

Установка динамического размера.

span(ranges::data(__range), ranges::size(__range))

3. subspan

span предоставляет очень полезную функцию-член subspan, которая позволяет извлечь подпредставление из существующего span без копирования данных. Это очень удобно при работе с большими наборами данных, позволяя избежать лишних накладных расходов памяти.

У метода subspan() две перегрузки:

  1. Одна принимает __offset и __count в качестве параметров, где __offset — начальная позиция, а __count — размер поддиапазона (по умолчанию dynamic_extent, то есть до конца исходного span).
  2. Другая — без параметров, реализована на основе шаблонов.
constexpr auto
subspan() const noexcept
-> span<element_type, _S_subspan_extent<_Offset, _Count>()> {}

template<size_t _Offset, size_t _Count = dynamic_extent>
[[nodiscard]]
constexpr auto
subspan() const noexcept
-> span<element_type, _S_subspan_extent<_Offset, _Count>()> {}

Пример 1:

std::span<int> s(arr);  
std::span<int> sub = s.subspan(1, 3);

Пример 2:

std::span<int> s(arr);  
auto sub = s.subspan<2, 2>();

4. Подводные камни использования

Хотя span — очень полезный инструмент, при его использовании следует учитывать ряд типичных ловушек:

4.1 Не позволяйте span ссылаться на уже освобождённую память

Сам span не владеет данными, он лишь ссылается на них. Поэтому, если память, на которую ссылается span, освобождена (например, исходный массив уничтожен или вышел из области видимости), обращение к такому span приведёт к неопределённому поведению.

Например:

std::span<int> create_span() {
    int arr[5] = {1, 2, 3, 4, 5};
    return std::span<int>(arr, 5);
}

// 错误:返回的 span 引用了栈上临时变量,函数返回后 arr 被销毁
auto s = create_span();

Чтобы решить эту проблему, следует убедиться, что данные, на которые ссылается span, действительны в течение всего его времени жизни. Можно хранить данные в динамически выделенной памяти либо обеспечить, чтобы время жизни данных совпадало со временем жизни span.

4.2 Проблемы при изменении размера нижележащего массива

Ссылка span на нижележащие данные прямая, а не копия. Когда нижележащие данные меняются, сам span об этих изменениях не узнаёт. Поэтому при изменении размера нижележащего массива или контейнера (например, при перераспределении памяти) прежний span может стать недействительным.

std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> s(vec);  // s 引用 vec 的数据
// 改变底层容器的大小
vec.push_back(6);  // vec 大小变为 6

// 这里 s 仍然引用 vec 的数据,但 vec 可能已经重新分配了内存
// 这可能导致 s 引用失效或未定义行为

Похожие темы

Комментарии