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.
Fixed-extent vs dynamic-extent spans
The Extent template parameter is the key design choice when using std::span. A dynamic-extent span (std::dynamic_extent) stores a pointer and a size_t, while a fixed-extent span of size N stores only a pointer β the size is baked into the type.
#include <span>
#include <array>
// Dynamic extent β size tracked at runtime, sizeof == 2 * sizeof(void*)
std::span<int> dyn; // C++20
static_assert(sizeof(dyn) == 2 * sizeof(void*));
// Static extent β size is part of the type, sizeof == sizeof(void*)
std::span<int, 4> fixed; // C++20
static_assert(sizeof(fixed) == sizeof(void*));
// Deduced static extent from std::array
std::array<int, 4> arr = {1, 2, 3, 4};
std::span<int, 4> s = arr; // extent=4 deduced or explicit
// Converting staticβdynamic is always implicit; dynamicβstatic is not
std::span<int> dyn2 = s; // OK β widening conversion
// std::span<int, 4> bad = dyn2; // Error β runtime size unknownStatic-extent spans are especially valuable in embedded or SIMD code where wrong-size arguments should be caught at compile time rather than silently proceeding with a runtime check.
// Enforces at compile time that callers pass exactly 4 floats // C++20
void processQuad(std::span<float, 4> quad) {
quad[0] *= 2.0f;
quad[1] *= 2.0f;
quad[2] *= 2.0f;
quad[3] *= 2.0f;
}
std::array<float, 4> v4 = {1, 2, 3, 4};
processQuad(v4); // OK
// processQuad(someVector); // compile error β size unknown at compile timeSubspans: first, last, subspan
Every subspan operation has both a runtime form and a template form that preserves the static extent. Prefer the template form whenever the sub-size is a compile-time constant; it propagates the size into the type and enables the same zero-overhead properties as a static-extent span.
#include <span>
#include <cstdint>
void demonstrate(std::span<const uint8_t> buf) { // C++20
// Runtime forms β result has dynamic extent
auto head3 = buf.first(3); // span<const uint8_t>
auto tail2 = buf.last(2); // span<const uint8_t>
auto mid = buf.subspan(3, 4); // span<const uint8_t>, offset=3 count=4
// Template forms β result carries size in its type
auto head3s = buf.first<3>(); // span<const uint8_t, 3>
auto tail2s = buf.last<2>(); // span<const uint8_t, 2>
auto mids = buf.subspan<3, 4>(); // span<const uint8_t, 4>
// Precondition: count/offset must be in-range β violation is UB
// buf.first(buf.size() + 1); // UB at runtime
// buf.first<100>(); // UB if buf.size() < 100
}A common pattern is peeling a fixed-size header off a raw byte buffer and leaving a variable-length payload:
#include <span>
#include <cstdint>
#include <cstring>
struct MsgHeader { uint32_t id; uint16_t len; }; // C++20
std::span<const std::byte> parseMessage(std::span<const std::byte> raw) {
constexpr auto kHdrSz = sizeof(MsgHeader);
if (raw.size() < kHdrSz) return {};
auto hdrBytes = raw.first<kHdrSz>(); // span<const byte, 6>
MsgHeader hdr{};
std::memcpy(&hdr, hdrBytes.data(), kHdrSz);
if (raw.size() < kHdrSz + hdr.len) return {};
return raw.subspan(kHdrSz, hdr.len); // span<const byte>
}span over 2D data
std::span is inherently one-dimensional, but a flat span over 2D data is idiomatic C++ for row-major matrices. Pass the full buffer plus dimensions separately, or encode them in the type via a helper wrapper.
#include <span>
#include <vector>
#include <cstddef>
// Row-major matrix view: element [r][c] = data[r * cols + c] // C++20
void addScalar(std::span<float> data, std::size_t rows,
std::size_t cols, float k) {
for (std::size_t r = 0; r < rows; ++r)
for (std::size_t c = 0; c < cols; ++c)
data[r * cols + c] += k;
}
// Extract a single row as a span β zero-copy sub-view
std::span<float> row(std::span<float> data, std::size_t cols,
std::size_t r) {
return data.subspan(r * cols, cols);
}
void demo() {
std::vector<float> matrix(3 * 4, 0.0f); // 3Γ4 row-major
addScalar(matrix, 3, 4, 1.0f);
auto r1 = row(matrix, 4, 1); // view of row 1
r1[0] = 99.0f; // writes back into matrix
}C++23's std::mdspan (from <mdspan>) is the proper multidimensional view type; use a flat std::span only when you need single-dimension access or when targeting C++20 without mdspan.
// C++23: mdspan as the multidimensional counterpart
#include <mdspan>
std::vector<float> buf(3 * 4);
std::mdspan<float, std::extents<std::size_t, 3, 4>> mat(buf.data()); // C++23
mat[1, 2] = 42.0f; // row 1, col 2span and contiguous_range concept
std::span<T> satisfies std::ranges::contiguous_range, std::ranges::sized_range, and std::ranges::borrowed_range, making it a first-class citizen of the C++20 ranges machinery. Any range algorithm that requires contiguous, sized, or borrowed ranges can accept a span directly.
#include <span>
#include <algorithm>
#include <ranges>
#include <concepts>
static_assert(std::ranges::contiguous_range<std::span<int>>); // C++20
static_assert(std::ranges::sized_range<std::span<int>>);
static_assert(std::ranges::borrowed_range<std::span<int>>);
// Because span is a borrowed_range, algorithms can safely return iterators
// into it without dangling-reference problems
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::span<int> s = v;
auto it = std::ranges::find(s, 4); // C++20
if (it != s.end())
*it = 0; // writes back into vThe borrowed_range property is critical: it tells the ranges library that an iterator into the span remains valid after the span object itself is destroyed (the span is just a view β the underlying storage persists). This makes it safe to pass a span as a temporary to algorithms that return iterators.
#include <span>
#include <algorithm>
#include <ranges>
#include <vector>
// span as a temporary β safe because it is a borrowed_range // C++20
std::vector<int> data = {5, 3, 1, 4, 2};
auto it = std::ranges::min_element(std::span{data});
// it is valid β refers into data, not into the (now-destroyed) temporary spanspan vs array_view and gsl::span
std::span standardises the concept pioneered by gsl::span (Guidelines Support Library) and array_view from early C++ Core Guidelines work. If you are migrating a codebase that used either predecessor, the interfaces are nearly identical with a few naming differences.
| Feature | gsl::span (pre-C++20) | std::span (C++20) |
|---|---|---|
| Header | <gsl/span> | <span> |
| Dynamic sentinel | gsl::dynamic_extent | std::dynamic_extent |
| Bounds checking | operator[] throws (configurable) | operator[] β UB; at() throws (C++23) |
as_bytes | gsl::as_bytes | std::as_bytes |
narrow_cast helpers | yes | no |
zstring_span | yes | use std::string_view |
// Migration: replace gsl::span with std::span // C++20
// Before (pre-C++20):
#include <gsl/span>
void process(gsl::span<const int> data);
// After (C++20):
#include <span>
void process(std::span<const int> data); // typically a drop-in replacementThe main behavioral difference is bounds checking: gsl::span::operator[] could be configured to throw or assert on out-of-bounds access, while std::span::operator[] has undefined behavior. Migrate any call sites relying on GSL's checked operator to use at() (C++23) or an explicit bounds check.
Byte-level access with as_bytes
std::as_bytes and std::as_writable_bytes reinterpret any contiguous span as a view of raw std::byte, enabling type-safe I/O, hashing, and serialisation without reinterpret_cast. The two free functions are the sanctioned way to obtain a byte-level alias of any trivially copyable data.
#include <span>
#include <cstddef>
#include <cstdint>
#include <array>
struct Sensor { float temperature; uint32_t timestamp; }; // C++20
Sensor reading{36.6f, 1700000000u};
// as_bytes β read-only byte view; always available
std::span<const std::byte> rb = std::as_bytes(std::span{&reading, 1});
static_assert(sizeof(Sensor) == 8);
// rb.size() == 8
// as_writable_bytes β mutable byte view; requires non-const T
std::span<std::byte> wb = std::as_writable_bytes(std::span{&reading, 1});
wb[0] = std::byte{0xFF}; // overwrites first byte of 'reading'Use as_bytes over reinterpret_cast for two reasons: it is guaranteed well-defined for any span element type (the standard explicitly blesses byte-aliasing), and the resulting span carries the correct size so callers cannot accidentally read past the end.
#include <span>
#include <cstddef>
#include <vector>
#include <fstream>
// Write any contiguous container to a binary stream // C++20
template<class T>
requires std::is_trivially_copyable_v<T>
void writeBinary(std::ofstream& out, std::span<const T> data) {
auto bytes = std::as_bytes(data);
out.write(reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
}
// Read back
template<class T>
requires std::is_trivially_copyable_v<T>
void readBinary(std::ifstream& in, std::span<T> data) {
auto bytes = std::as_writable_bytes(data);
in.read(reinterpret_cast<char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
}Note that as_writable_bytes requires the span's element type to be non-const β std::as_writable_bytes(std::span<const int>{...}) is a compile error by design.
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