Standard Library[lib]
std::span in C++20: pass a view, not a pointer and a size
- Section
- Standard Library [lib]
- Standard
- C++20
- Compilers
- MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
- Published
- Length
- 10 min read
In this answer9 clauses
- 1Take a std::span parameter
- 2What converts to std::span, and what does not
- 3Read-only is
std::span<const T>, notconst std::span<T> - 4Slice it: first, last and subspan
- 5Does std::span check bounds?
s[i]andat() - 6How a std::span dangles, and which compilers warn
- 7Static extent:
std::span<T, N> - 8What a span costs, and why you pass it by value
- 9std::span or something else
Abstract
std::span<T> (C++20, header <span>) is a pointer and a size over contiguous elements that another object owns. Take a std::span<const T> by value where you took a const std::vector<T>& or a pointer and a length: a vector, a std::array, a C array or a slice of one converts to it, and no element is copied. Out of range, s[i] is undefined behavior in C++20 and C++23 (a hardened precondition in C++26), and a span that outlives its elements dangles.

std::span<T> (C++20, header <span>) is a pointer and a size: a view of contiguous elements that something else owns. Take a std::span<const T>, by value, in place of a const std:: parameter or a pointer and a length: callers pass a vector, a std::array, a C array or part of one, and no element is copied. A span keeps nothing alive and is not required to check an index: s[i] out of range is undefined behavior in C++20 and C++23 (a hardened precondition in C++26), and a span that outlives its elements dangles.
This answer is written for C++20, the standard that added std::span. What came after:
- C++23 requires every span to be trivially copyable (P2251R1), adds
cbegin()andcend()for constant iterators (P2278R4; libc++ 23 still rejects them) and addsstd::mdspan, the multidimensional counterpart, in<mdspan>(Since When, C++23). - C++26 adds
at(), which throws, and makes the preconditions ofoperator[],front,back,first,lastandsubspanhardened. A constructor fromstd::was added to C++26 and withdrawn before the standard was finished.initializer_list
Four compilers on three standard libraries built every sample, in C++20 mode unless the text says otherwise: MSVC 19.44 and Clang 19.1 on Microsoft’s STL (Windows x64), GCC 16.2 with libstdc++ and Clang 23.1 with libc++ (Compiler Explorer, Linux x86-64). An output shown is the same on all four unless a compiler is named. Since When’s C++20 table gives GCC 10.1, Clang 7 and Visual Studio 2019 16.6 as the first releases with std::span, and cppreference’s std::span page lists every member.
What changed: a function that reads a sequence takes a pointer and a length, or a a const std::std::span<const T>.
since C++20
Take a std::span parameter
The running example averages a sensor’s readings. One function, five kinds of caller:
double average(std::span<const double> values) {
if (values.empty()) return 0.0;
double sum = 0.0;
for (double v : values) sum += v;
return sum / static_cast<double>(values.size());
}
// ...
std::vector<double> readings{1.5, 2.0, 2.5, 3.0, 3.5, 4.0};
std::array<double, 4> calibration{0.5, 1.0, 1.5, 2.0};
double driver_buffer[3] = {2.0, 2.5, 3.0}; // from a C API
const double* samples = driver_buffer;
std::size_t count = 2;
std::printf("vector: %.2f\n", average(readings));
std::printf("std::array: %.2f\n", average(calibration));
std::printf("C array: %.2f\n", average(driver_buffer));
std::printf("pointer+count: %.2f\n",
average(std::span(samples, count)));
std::printf("first half: %.2f\n",
average(std::span(readings).first(3)));— end example]
It printed:
vector: 2.75
std::array: 1.25
C array: 2.50
pointer+count: 2.25
first half: 2.00— end example]
- The vector, the
std::arrayand the C array convert implicitly. What crosses the call is a pointer and a size. - A pointer and a count go in as
std::orspan(samples, count) {samples, count}, and the span trustscount. - In C++17 mode all four reject the function; GCC adds
note: 'std::.span' is only available from C++20 onwards
To call an older API that still takes a pointer and a length, take the span apart:
| The API takes | Pass |
|---|---|
| a pointer and an element count | s.data(), s.size() |
| a pointer and a byte count | s.data(), s.size_bytes(); or std::as_bytes(s) |
For a span of four std::uint32_t, size() printed 4, and size_bytes() and the size() of std::as_bytes both printed 16.
What converts to std::span, and what does not
Passed to average(std:: and to scale(std::, on all four:
| You pass | To std::span<const T> |
To std::span<T> |
|---|---|---|
| a vector | yes | yes |
a const vector |
yes | no: const elements |
| a temporary vector | yes, for the call | no |
a std::array or a C array |
yes | yes |
a pointer and a size, {p, n} |
yes | yes |
a std::span<T> |
yes | yes |
a std::list |
no: not contiguous | no |
a braced list, {1.0, 2.0, 3.0} |
no | no |
a braced array, {{1.0, 2.0, 3.0}} |
yes, as an argument | no |
Every refusal is an ordinary conversion error, such as Clang’s no known conversion from 'const std::. The messages’ first lines do not say why a temporary vector is refused: the range constructor takes an rvalue only if it is a borrowed range or the elements are const ([views.span]).
The braced list has a history. C++26 accepted {1.0, 2.0, 3.0} for a std::span<const T> for a while (P2447R6), and libc++ shipped that from LLVM 18. P4144R1 withdrew it before C++26 was finished, because std:: then chose the list over the pointer and the size, and libc++ 23 removed it again.
So code that compiled under -std=c++26 with those releases, or with GCC 15, whose libstdc++ shipped the constructor too, stops compiling. All four compilers here reject average({1.0, 2.0, 3.0}) and std:: in C++20, C++23 and C++26 mode.
For a std::span<const T>, the form that works in every mode has a second pair of braces: average({{1.0, 2.0, 3.0}}) makes a C array, and printed 2.00 on all four in all three modes. The array lives until the end of the call, so the form is safe as an argument; stored in a span variable it dangles at the semicolon.
Read-only is std::span<const T>, not const std::span<T>
const on the span works like const on a pointer: the span cannot be re-pointed, but its elements can still be written. Both spans below look at readings:
const std::span<double> fixed(readings);
fixed[0] = 9.0; // compiles: a const span, mutable elements
std::span<const double> view(readings);— end example]
All four compiled the write, and readings[0] printed 9.0. Writing view[0] = 9.0; fails on all four (Clang: cannot assign to return value because function 'operator[]' returns a const value), and re-pointing fixed with fixed = spare; fails on all four too. So a const std:: parameter still lets the function write the caller’s elements; std:: does not.
Slice it: first, last and subspan
Each returns another span over the same elements, so nothing is copied, and subspan(offset) without a count runs to the end. print writes a label, the elements and the size:
std::vector<double> readings{1.5, 2.0, 2.5, 3.0, 3.5, 4.0};
std::span<double> s(readings);
print("s.first(2)", s.first(2)); // 1.5 2.0
print("s.subspan(2, 3)", s.subspan(2, 3)); // 2.5 3.0 3.5
print("s.last(1)", s.last(1)); // 4.0
print("s.subspan(2)", s.subspan(2)); // to the end
std::span<double> middle = s.subspan(2, 3);
std::printf("same storage: %s\n",
middle.data() == readings.data() + 2 ? "yes" : "no");
middle[0] = 2.75; // writes into readings
std::printf("readings[2] = %.2f\n", readings[2]);— end example]
It printed:
s.first(2) 1.5 2.0 (size 2)
s.subspan(2, 3) 2.5 3.0 3.5 (size 3)
s.last(1) 4.0 (size 1)
s.subspan(2) 2.5 3.0 3.5 4.0 (size 4)
same storage: yes
readings[2] = 2.75— end example]

- These runtime forms return a span of dynamic extent. The template forms put the size in the type (static extent, below): on
s,first<2>(),last<1>()andsubspan<2, 3>()gave extents 2, 1 and 3 over the same elements, on all four. - Asking for more elements than the span has is undefined behavior in C++20 and C++23, as an index out of range is.
Does std::span check bounds? s[i] and at()
No standard requires every implementation to check s[i]. In C++20 and C++23, s[i] with i >= s.size() is undefined behavior. C++26 makes idx < size() a hardened precondition of operator[], and does the same for front, back, first, last and subspan ([views.span], from P3471R4): a hardened implementation ends the program on a violation, a non-hardened one leaves it undefined.
Each library already has a switch that checks it in C++20 mode. The program below reads one past the end of a four-element window, and refuses to build unless its library’s check is on:
std::vector<double> readings{1.5, 2.0, 2.5, 3.0, 3.5, 4.0};
std::span<const double> window = std::span(readings).first(4);
// ...
double r = window[window.size()]; // checked: stops here— end example]
What window[4] did with each switch on, at -O0 and -O2 alike:
| Library | Without a flag | Switch | With the switch |
|---|---|---|---|
| Microsoft’s STL | not checked (release build) | _MSVC_STL_HARDENING=1 |
no message; exit code 0xC0000409 |
| libstdc++ | checked at -O0 since GCC 15, not at -Og and up |
_GLIBCXX_ASSERTIONS |
Assertion '__idx < size()' failed. on stderr; SIGABRT |
| libc++ | not checked | _LIBCPP_HARDENING_MODE= |
no message; SIGILL |
libstdc++ checks unasked when nothing is optimized: since GCC 15 it defines _GLIBCXX_ASSERTIONS by itself (GCC 15 release notes), so the -O0 build with no flag aborted on that assertion, and at -Og and above it does not (-D_GLIBCXX_NO_ASSERTIONS opts out). Microsoft’s debug builds check too, per its header (_ITERATOR_DEBUG_LEVEL not 0); no debug build was run here.
The other cells are observations on the releases named: libc++’s default is set by whoever built it. The same switches stopped window.first(5), and they are the ones that check a read of an empty std::optional.
C++26 adds a checked accessor: s.at(i) throws std::out_of_range when i >= size() (P2821R5). With the same window:
try {
double r = window.at(window.size()); // C++26: checked, throws
std::printf("window.at(4) = %.1f\n", r);
} catch (const std::out_of_range& e) {
std::printf("std::out_of_range: %s\n", e.what());
}— end example]
Under -std=c++26, GCC 16.2 printed std:: and Clang 23.1 with libc++ printed std::. Microsoft’s STL has no at, even under /std:c++latest (MSVC: 'at': is not a member of 'std::), and in C++23 mode none of the three has it. libc++’s status page dates at to LLVM 18.
How a std::span dangles, and which compilers warn
A span keeps nothing alive. Three everyday patterns can leave one pointing at elements that no longer exist; the samples below were compiled at -O0 and -O2 and never run:
// 1. A span over a local container, returned.
std::span<const double> latest_readings() {
std::vector<double> buffer{1.5, 2.0, 2.5};
return buffer; // buffer is destroyed here
}
// ...
// 2. A span of a temporary.
std::span<const double> s = make_readings(); // gone at the ;
// 3. A span into a vector that then grows.
std::vector<double> readings{1.5, 2.0, 2.5};
std::span<const double> view(readings);
readings.push_back(3.0); // may move every element— end example]
What each compiler said at /W4 for MSVC and -Wall -Wextra for the rest, with the braced array of the conversion table added as a fourth pattern:
| Pattern | Clang 19.1, Clang 23.1 | MSVC, GCC 16.2 |
|---|---|---|
| span over a local, returned | -Wreturn-stack-address |
nothing |
| span of a temporary vector | -Wdangling-gsl |
nothing |
| span into a vector that then grows | nothing | nothing |
{{1.0, 2.0, 3.0}} in a span variable |
Clang 23.1 only: -Wdangling |
GCC at -O2 only: -Wdangling-pointer= |
Clang’s warning for the temporary:
warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]— end example]
None of the four warns about the grown vector at those flags. Anything that invalidates pointers into the elements a span covers invalidates the span too ([views.span]), so take the span again after anything that can reallocate the vector. Toolchain lists the analyzers and sanitizers to run on top of the compiler, and Clang vs GCC compares what else the two warn about.
Static extent: std::span<T, N>
Put the size in the type and the compiler checks what it can. apply_calibration takes exactly four gains:
void apply_calibration(std::span<double> values,
std::span<const double, 4> gains) {
for (std::size_t i = 0; i < values.size(); ++i)
values[i] *= gains[i % gains.size()];
}
// ...
std::array<double, 4> calibration{0.5, 1.0, 1.5, 2.0};
std::span<double, 4> block = calibration; // size from the type
std::span deduced(calibration); // span<double, 4>
auto head = block.first<2>(); // span<double, 2>
// ...
std::vector<double> latest{1.0, 1.0, 1.0, 1.0};
std::vector<double> readings{1.5, 2.0, 2.5, 3.0, 3.5, 4.0};
// ...
apply_calibration(readings, std::span<const double, 4>(latest));
apply_calibration(readings, calibration); // array: implicit— end example]
- From a
std::or aarray<double, 4> double[4]the conversion is implicit, and class template argument deduction keeps the size:deducedis astd::span<double, 4>. - From a vector it is explicit.
apply_calibration(readings,fails on all four, and GCC alone says why:latest) note: explicit conversion function was not considered. A wrong size in the explicit conversion is undefined behavior until C++26, a hardened precondition from then. block.first<5>()is a compile-time error on all four (MSVC:static_assert failed: 'Count out of range in span::), wherefirst<Count>()' first(5)on a dynamic span is a runtime precondition.std::span<T>isstd::, and the standard definesspan<T, std:: dynamic_extent> dynamic_extentasstd::([span.syn]): thenumeric_limits<std:: size_t>:: max() 18446744073709551615in MSVC’s messages.
What a span costs, and why you pass it by value
On all three libraries (x64), sizeof printed:
std::span<double> 16
std::span<double, 4> 8
double* + std::size_t 16
std::vector<double> 24— end example]
A dynamic span is the pointer and size it replaces; a static one stores only the pointer. Both are trivially copyable on all four in C++20 mode (a static_assert on std:: compiled), which C++23 turns into a requirement. So pass a span by value, as you would the pointer: the Core Guidelines’ F.24 calls passing one “exactly as efficient as passing a pair of pointer arguments”. The general rule is in pass by value or const reference.
std::span or something else
| You have | Take | Since |
|---|---|---|
| a run of contiguous elements to read | std::span<const T> |
C++20 |
| elements to modify in place | std::span<T> |
C++20 |
| text | std::string_view |
C++17 |
| any range, contiguous or not | a template constrained by std:: |
C++20 |
| elements to add or remove | the container, by reference | C++98 |
| data to keep after the call | the container, by value | C++98 |
The library’s other vocabulary types are answered under Standard library.
Questions
Does std::span own or copy its elements?
No. A std::span is a pointer and a size over elements that another object owns, so copying a span copies those two and no element. On Microsoft’s STL, libstdc++ and libc++ (x64), a std::span of double was 16 bytes, the size of a pointer plus a std::size_t. The elements must outlive every span over them.
How do I compare two std::span objects?
With std::ranges::equal(a, b), which compares the elements. std::span has no == or other comparison operators: the committee removed them (P1085R2), and a == b failed to compile on all four compilers tested. Comparing a.data() with b.data() only tells you whether two spans start at the same element.
How do I convert a std::span to a std::vector?
Construct the vector from the span's iterators: std::vector<double> kept(s.begin(), s.end()); copies the elements into storage the vector owns. In C++23, auto kept = std::; does the same; under C++20 all four compilers tested rejected it, because std::ranges::to does not exist there.
What is the difference between gsl::span and std::span?
Bounds checking. The C++ Core Guidelines say gsl::span should be the same as std::span and that “the only difference should be that GSL span is fully bounds-safe by default”; GSL’s documentation adds that its iterators are checked too and that a violation terminates the program. A std::span’s s[i] checks only when its standard library’s checks are on (libstdc++ does so by itself in unoptimized builds since GCC 15) or in a hardened C++26 implementation; C++26’s s.at(i) always checks and throws std::out_of_range.
What is std::mdspan?
The multidimensional counterpart of std::span, added in C++23 in the header <mdspan>: a view that owns nothing and indexes another object's elements along several dimensions. Since When's C++23 table gives GCC 16.1, Clang 18 and Visual Studio 2022 17.9 as the first releases with it.
Sources
- std::spancppreference.com
- [views.span] Class template spanC++ standard working draft
- P3471R4: Standard library hardeningWG21
- P2821R5: span.at()WG21
- P4144R1: Remove span’s initializer_list constructor for C++26WG21
- F.24: Use a span<T> or a span_p<T> to designate a half-open sequenceC++ Core Guidelines
- Hardening Modeslibc++ documentation
- STL HardeningMicrosoft STL wiki