Skip to content

Last updated:2026-10-12

C++20 std::span Design and Pitfalls

0. Motivation

In traditional C/C++ code, we usually access an array with a pointer plus a size, for example:

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

span simplifies this operation, turning it into:

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

So what exactly is a span, what are its use cases, and what pitfalls does it have? This section explores these questions in detail.

1. What Is a span?

In C++20, a span is a lightweight, range-like container that provides access to a stretch of contiguous memory. As a view, a span does not own its data; it is simply a reference to the data, and it is commonly used to replace traditional C-style arrays or pointers. It can wrap arrays of any type as well as standard containers such as std::vector, providing a safer, more modern way to handle such data.

Several key points:

  • Contiguous memory
  • Does not own the data
  • A reference

The source code of span in gcc lives at libstdc++-v3/include/std/span.

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

The span type takes two important parameters:

  • _Type: the element type.
  • Extent: the array size. If it is dynamic_extent (the default), the size of the view can change dynamically.

2. Dynamic and Static span

span has two modes: static and dynamic.

2.1 Static span

A static span determines its size at compile time; its Extent is a constant value. When using a static span, we need to know the size of the data at compile time.

For example:

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

The size of a static span is fixed at compile time, so the compiler can apply certain optimizations, such as omitting bounds checks. But because Extent is a constant, a static span cannot have its size changed at runtime.

2.2 Dynamic span

The size of a dynamic span is determined at runtime; Extent defaults to dynamic_extent. This kind of span does not require the size to be known at compile time and can adapt to arrays or containers of any size.

For example:

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

A dynamic span suits the cases where the size cannot be determined at compile time; it offers greater flexibility.

The implementation in the gcc source is fairly simple: it defaults to a very large integer (the unsigned value of -1), and the real value is computed at construction time.

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

For example, ranges::size is used here to set the internal size; see the code below.

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

Setting the dynamic size.

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

3. subspan

span provides a very useful member function, subspan, which lets us extract a sub-view from an existing span without copying data. This is very handy when processing large data sets and avoiding unnecessary memory overhead.

The subspan() method has two overloads:

  1. One takes __offset and __count as parameters, where __offset is the starting position and __count is the size of the sub-range (defaulting to dynamic_extent, i.e. up to the end of the original span).
  2. The other takes no parameters and is implemented on top of templates.
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>()> {}

Example 1:

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

Example 2:

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

4. Usage Pitfalls

Although span is a very useful tool, there are some common pitfalls to keep in mind when using it:

4.1 Do Not Let a span Refer to Memory That Has Been Freed

A span does not own the data; it is only a reference to it. So if the memory a span refers to is released (for example, the original array is destroyed or goes out of scope), accessing that span leads to undefined behavior.

For example:

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();

To avoid this problem, make sure the data a span refers to stays valid for its lifetime. Consider holding the data in dynamically allocated memory, or making sure the data's lifetime matches the span's.

4.2 Problems When the Underlying Array Changes Size

A span's reference to the underlying data is direct, not a copy. When the underlying data changes, the span itself does not notice these changes. Therefore, when the size of the underlying array or container changes (for example, through memory reallocation), the original span may become invalid.

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 引用失效或未定义行为

Comments