std::span
Non-owning view over a contiguous sequence of objects. C++20's type-safe replacement for (pointer, length) pairs, working with arrays, vectors, std::array, and raw buffers.
std::spansince C++20A non-owning, lightweight view over a contiguous sequence of T objects with an optional compile-time extent, providing safe array abstraction without ownership or allocation.
Overview
std::span<T, Extent> solves a decades-old problem in C++ APIs: functions that need to accept "an array of T" have historically been forced to choose between unsafe (T*, size_t) pairs, artificial coupling to std::vector<T>, or an explosion of overloads. A span accepts all contiguous storage β C-style arrays, std::array, std::vector, std::string, and raw pointer-length pairs β without copying, allocating, or coupling to any specific container type.
The second template parameter Extent defaults to std::dynamic_extent, a sentinel value indicating the size is tracked at runtime. When set to an explicit compile-time constant N, the span stores only a pointer β sizeof(std::span<T, N>) == sizeof(T*) β and the size becomes a compile-time constant. This zero-overhead variant is useful in embedded, numeric, and protocol code where array dimensions are fixed by design.
Spans are value types. Copying a std::span copies two machine words (pointer + size for dynamic; one pointer for static). Pass them by value in function parameters β never by const reference.
std::span was introduced in C++20 (<span>). C++23 added at() for bounds-checked element access and construction of std::span<const T> from std::initializer_list<T>.
Syntax
// Primary template
template<class T, std::size_t Extent = std::dynamic_extent>
class std::span; // C++20
// Construction β all produce non-owning views
std::span<T> s1(ptr, count); // pointer + element count
std::span<T> s2(first, last); // iterator pair
std::span<T> s3(arr); // C-style array β extent deduced
std::span<T, N> s4(std_array); // std::array<T,N> β static extent N
std::span<T> s5(vec); // std::vector<T> β dynamic extent
std::span<const T> s6({1, 2, 3}); // initializer_list (C++23, const T only)
// Subspan operations β compile-time vs runtime extent
auto a = s.first(n); // span<T> β dynamic
auto b = s.first<N>(); // span<T, N> β static
auto c = s.last(n); // span<T> β dynamic
auto d = s.last<N>(); // span<T, N> β static
auto e = s.subspan(offset, count); // span<T> β dynamic
auto f = s.subspan<Offset, Count>(); // span<T, Count> β static
// Byte-level views (free functions)
std::span<const std::byte> rb = std::as_bytes(s); // always available
std::span<std::byte> wb = std::as_writable_bytes(s); // T must be non-constConst correctness: two very different types
std::vector<int> v = {1, 2, 3};
std::span<int> s1 = v; // mutable view β can write through s1
std::span<const int> s2 = v; // read-only view β implicit conversion from span<int>
const std::span<int> s3 = v; // span can't be reseated, but elements ARE mutable
// Analogy:
// span<const T> β const T* β data is const, span itself can be rebound
// const span<T> β T* const β span can't be rebound, data is mutablestd::span<T> implicitly converts to std::span<const T>; the reverse is not allowed. Always prefer span<const T> for read-only parameters.
Examples
Unified function parameter replacing multiple overloads
#include <span>
#include <numeric>
#include <vector>
#include <array>
// One function works for every contiguous container β no copies, no templates
double mean(std::span<const double> data) {
if (data.empty()) return 0.0;
return std::reduce(data.begin(), data.end()) / static_cast<double>(data.size());
}
void demo() {
double arr[] = {1.0, 2.0, 3.0};
std::array<double, 4> a = {4.0, 5.0, 6.0, 7.0};
std::vector<double> v = {8.0, 9.0, 10.0};
mean(arr); // OK β C-array, no decay
mean(a); // OK β std::array
mean(v); // OK β std::vector
mean({v.data(), 2}); // OK β first two elements, no copy
}Binary protocol framing with static-extent subspans
#include <span>
#include <cstdint>
#include <cstring>
#include <stdexcept>
struct PacketHeader {
uint16_t magic;
uint16_t length;
};
void parsePacket(std::span<const std::byte> buf) {
constexpr std::size_t kHeaderSize = sizeof(PacketHeader);
if (buf.size() < kHeaderSize)
throw std::runtime_error("truncated header");
// Static-extent subspan β size checked at compile time
auto header_bytes = buf.first<kHeaderSize>(); // span<const byte, 4>
PacketHeader hdr{};
std::memcpy(&hdr, header_bytes.data(), kHeaderSize);
if (hdr.magic != 0xABCD)
throw std::runtime_error("bad magic");
auto payload = buf.subspan(kHeaderSize, hdr.length);
// process payload...
}In-place numeric algorithm
#include <span>
#include <algorithm>
#include <ranges> // C++20
void clampInPlace(std::span<float> values, float lo, float hi) {
// std::span satisfies std::ranges::contiguous_range (C++20)
std::ranges::clamp(values, lo, hi); // hypothetical β use loop for clarity:
for (auto& v : values)
v = std::clamp(v, lo, hi);
}
void normalizeInPlace(std::span<float> values) {
if (values.size() < 2) return;
auto [mn, mx] = std::ranges::minmax(values); // C++20
float range = mx - mn;
if (range == 0.0f) return;
for (auto& v : values) v = (v - mn) / range;
}Bounds-checked access (C++23)
#include <span>
#include <stdexcept>
void safeRead(std::span<const int> data, std::size_t idx) {
// operator[] β no bounds check, UB if idx >= size()
int unsafe = data[idx];
// at() β throws std::out_of_range if idx >= size() (C++23)
try {
int safe = data.at(idx);
} catch (const std::out_of_range& e) {
// handle
}
}Raw memory serialisation with as_bytes
#include <span>
#include <cstddef>
#include <cstring>
// Write any trivial type into a byte buffer
template<class T>
requires std::is_trivially_copyable_v<T> // C++20 concept constraint
std::size_t serialise(const T& value, std::span<std::byte> out) {
if (out.size() < sizeof(T)) return 0;
std::memcpy(out.data(), &value, sizeof(T));
return sizeof(T);
}
// Read the object back
template<class T>
requires std::is_trivially_copyable_v<T>
T deserialise(std::span<const std::byte> src) {
if (src.size() < sizeof(T))
throw std::runtime_error("buffer too small");
T value{};
std::memcpy(&value, src.data(), sizeof(T));
return value;
}Best Practices
Pass by value, not by reference. A std::span is at most two machine words. void f(std::span<const int> s) is cheaper than void f(const std::span<const int>& s).
Prefer span<const T> for read-only parameters. It communicates intent, prevents accidental writes, and allows callers to pass const-qualified containers.
Use static extent when size is compile-time known. std::span<float, 4> produces a single-pointer type with no runtime size bookkeeping. Compilers can additionally verify argument sizes at compile time in constrained contexts.
Don't store spans as class members without careful lifetime analysis. A span does not extend the lifetime of its referent. If your class needs to own data, store the container β not the view.
Use std::as_bytes for type-agnostic I/O. Instead of reinterpret-casting buffers, convert to span<const std::byte> via std::as_bytes. It's well-defined and signals intent clearly.
Validate size before constructing a fixed-extent span from a dynamic source. The constructor std::span<T, N>(ptr, count) has a precondition that count == N; violating it is undefined behaviour.
Common Pitfalls
Dangling after container reallocation
std::vector<int> v = {1, 2, 3};
std::span<int> s = v;
v.push_back(4); // may reallocate β s.data() now points to freed memory
s[0] = 99; // UBA span captures the address of the underlying storage at construction time. Any operation that invalidates a container's iterators also invalidates any span over that container.
Returning a span to local storage
std::span<int> bad() {
int arr[] = {1, 2, 3};
return std::span(arr); // UB β arr destroyed on return
}
std::span<int> also_bad() {
std::vector<int> v = {1, 2, 3};
return std::span(v); // UB β v destroyed on return
}const span<T> does not mean read-only
const std::span<int> s = v;
s[0] = 42; // compiles and works β the SPAN is const, not the intsIf your intent is a read-only view, the type must be std::span<const int>.
Not null-terminated β unsafe with C APIs
std::span<const char> s = getSomeChars();
printf("%s", s.data()); // dangerous β no guarantee of null terminatorUse std::string_view for character sequences that must interact with null-terminated C APIs.
See Also
std::string_viewβ the character-specific analogue; prefer it for textstd::vectorβ owning contiguous container; source of many spansstd::arrayβ fixed-size owning array; maps to a static-extent span