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
  1. 1Take a std::span parameter
  2. 2What converts to std::span, and what does not
  3. 3Read-only is std::span<const T>, not const std::span<T>
  4. 4Slice it: first, last and subspan
  5. 5Does std::span check bounds? s[i] and at()
  6. 6How a std::span dangles, and which compilers warn
  7. 7Static extent: std::span<T, N>
  8. 8What a span costs, and why you pass it by value
  9. 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.

Eight hatched cells, the elements, with a purple frame around cells three to six. Below, a pointer cell and a size cell; a purple arrow runs from the pointer up to the frame’s left edge.
Figure 1 — A span is a pointer and a size laid over elements that something else owns.

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::vector<T>& 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() and cend() for constant iterators (P2278R4; libc++ 23 still rejects them) and adds std::mdspan, the multidimensional counterpart, in <mdspan> (Since When, C++23).
  • C++26 adds at(), which throws, and makes the preconditions of operator[], front, back, first, last and subspan hardened. A constructor from std::initializer_list was added to C++26 and withdrawn before the standard was finished.

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 const std::vector<T>& a 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:

[Example 1:
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:

[Example 2:
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::array and the C array convert implicitly. What crosses the call is a pointer and a size.
  • A pointer and a count go in as std::span(samples, count) or {samples, count}, and the span trusts count.
  • 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::span<const double>) and to scale(std::span<double>, double), 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::vector<double>' to 'std::span<double>'. 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::span<const bool>{ptr, size} 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::span<const double> s = {1.0, 2.0}; 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:

[Example 3:
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::span<double> parameter still lets the function write the caller’s elements; std::span<const double> 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:

[Example 4:
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:

[Example 5:
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]

The vector readings as six cells holding 1.5, 2.0, 2.5, 3.0, 3.5 and 4.0. Purple brackets beneath mark s.first(2) under the first two, s.subspan(2, 3) under the next three, s.last(1) under the last. Note: no element is copied.
Figure 2 — first, last and subspan return views of the same elements.
  • 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>() and subspan<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:

[Example 6:
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=_LIBCPP_HARDENING_MODE_FAST 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:

[Example 7:
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::out_of_range: span::at(4) out-of-range for span of size 4 and Clang 23.1 with libc++ printed std::out_of_range: span. Microsoft’s STL has no at, even under /std:c++latest (MSVC: 'at': is not a member of 'std::span<const double,18446744073709551615>'), 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:

[Example 8:
// 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:

[Example 9:
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:

[Example 10:
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::array<double, 4> or a double[4] the conversion is implicit, and class template argument deduction keeps the size: deduced is a std::span<double, 4>.
  • From a vector it is explicit. apply_calibration(readings, latest) fails on all four, and GCC alone says why: 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::first<Count>()'), where first(5) on a dynamic span is a runtime precondition.
  • std::span<T> is std::span<T, std::dynamic_extent>, and the standard defines dynamic_extent as std::numeric_limits<std::size_t>::max() ([span.syn]): the 18446744073709551615 in MSVC’s messages.

What a span costs, and why you pass it by value

On all three libraries (x64), sizeof printed:

[Example 11:
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::is_trivially_copyable_v 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::ranges::input_range 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::ranges::to<std::vector>(s); 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.

Read next

  1. C++ pass by value or const reference: the rule since C++17Performance · Miriam Castellanos · written for C++20
  2. How to write comparators for std::sort and std::mapStandard Library · Miriam Castellanos · written for C++20
  3. std::println in C++23: print without std::cout or printfStandard Library · Miriam Castellanos · written for C++23