Last Update:
How to use std::span from C++20

Table of Contents
In this article, we’ll look at std::span
, which has been available since C++20. This “view” type is more generic than string_view
and can help work with arbitrary contiguous collections.
Updated in Feb 2025: added section about returning spans and C++26 improvements (.at()
and creatoion from initializer list).
A Motivating Example
Here’s an example that illustrates the primary use case for std::span
:
In traditional C (or low-level C++), you’d pass an array to a function using a pointer and a size like this:
void process_array(int* arr, std::size_t size) {
for(std::size_t i = 0; i < size; ++i) {
// do something with arr[i]
}
}
std::span
simplifies the above code:
void process_array(std::span<int> arr_span) {
for(auto& elem : arr_span) {
// do something with elem
}
}
The need to pass a separate size variable is eliminated, making your code less error-prone and more expressive.
And in essence: std::span<T>
is:
- a lightweight abstraction of a contiguous sequence of values of type T,
- more or less implemented as
struct { T * ptr; std::size_t length; }
, - a non-owning type (i.e. a “reference type” rather than a “value type”).
Construction of std::span
std::span
lives in its own new header <span>
. It’s defined as follows:
template<class T, std::size_t Extent = std::dynamic_extent>
class span;
To create this object, you have two basic options: static extent and dynamic:
Static Extent
When you know the size at compile-time:
int arr[] = {1, 2, 3, 4, 5};
std::span<int, 5> arr_span {arr};
//std::span<int, 2> arr_span2 {arr}; // error size doesn't match
Here, the 5
is an integral part of the type. You’ll get a compiler error if you try to initialize this span with an array of different sizes.
Dynamic Extent
When you only know the size at runtime, like when working with vectors:
int arr[] = {1, 2, 3, 4, 5};
std::vector v { 1, 2, 3, 4, 5};
std::span<int> arr_span {arr}; // dynamic extent
std::span<int> vec_span {v}; // also!
Notice the absence of size in the type? That’s the dynamic extent in action.
Sizeof span
The interesting part about span
is that when the size is static, then the type is smaller as there’s no need to store the size of the sequence:
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int, 5> arr_span {arr};
std::span<int> other_span {arr};
std::span<int> vec_span{vec};
std::cout << std::format("sizeof arr_span: {}\n", sizeof(arr_span));
std::cout << std::format("sizeof other_span: {}\n", sizeof(other_span));
std::cout << std::format("sizeof vec_span: {}\n", sizeof(vec_span));
The output on GCC is:
sizeof arr_span: 8
sizeof other_span: 16
sizeof vec_span: 16
More construction options
For completeness, let’s now revise other construction options following the list of available constructors:
This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.
Default Constructor
This constructs an empty span.
std::span<int> empty_span;
.data()
returns nullptr
and the size()
returns 0
in this case.
From Iterators and Size
Creates a span from a starting iterator and a size.
std::vector<int> vec = {1, 2, 3};
std::span<int> from_iterator_and_size(vec.begin(), /*count*/2); // 1, 2
std::span<int> from_iterator_and_size2(vec.begin(), /*count*/3); // 1, 2, 3
And also in CTAD version:
std::vector vec = {1, 2, 3};
std::span from_iterator_and_size(vec.begin(), /*count*/2); // 1, 2
std::span from_iterator_and_size2(vec.begin(), /*count*/3); // 1, 2, 3
From Two Iterators
Constructs a span from a range specified by two iterators.
std::span<int> from_two_iterators(vec.begin(), vec.end());
And using CTAD:
std::span from_iterator_and_end(vec.begin(), vec.end());
From C-style Array
For C-style arrays.
int arr[] = {1, 2, 3};
std::span<int> from_array(arr);
std::span from_array2(arr);
.data()
returns std::data(arr)
From std::array
Can construct both from non-const and const std::array
.
std::array<int, 3> std_arr = {1, 2, 3};
std::span<int, 3> from_std_array(std_arr);
// CTAD:
std::span from_std_array2(std_arr);
const std::array<int, 3> const_std_arr = {1, 2, 3};
std::span<const int> from_const_std_array(const_std_arr);
From Contiguous Range
Using this constructor, you can pass in any contiguous range like std::vector
.
std::vector vec {1, 2, 3, 4, 5};
std::span<int> from_range(vec); // covers entire vec
// CTAD:
std::span from_range2(vec);ec
Conversion from Another Span
Can be used for type conversions if the types are compatible.
std::span<int> int_span(vec);
std::span<const int> const_span = int_span; // conversion
Passing spans
Spans are lightweight objects intended to pass by value. But we have two options to preserve the constness of its elements:
void print(span<const char> outbuf);
void transform(span<char> inbuf);
In other words, you can pass span<const T>
to indicate constant elements, and “read only” access, or pass span<T>
to allow read/write access.
For example:
void transform(std::span<char> outbuf) {
for (auto& elem : outbuf) {
elem += 1;
}
}
void output(std::span<const char> outbuf) {
std::cout << "contents: ";
for (auto& elem : outbuf) {
std::cout << elem << ", ";
// elem = 0; // error!
}
std::cout << '\n';
}
int main() {
std::string str = "Hello World";
std::span<char> buf_span(str);
output(str);
transform(buf_span);
output(buf_span);
}
The output:
contents: H, e, l, l, o, , W, o, r, l, d,
contents: I, f, m, m, p, !, X, p, s, m, e,
The example above shows that str
nicely converts into a span and is passed to the output
function. And later, the buf_span
is also converted (char
to const char
) when passing to output
.
Subspans
You can easily create subviews/subspans of existing spans:
void printSpan(std::span<const int> sp) {
for (auto&& elem : sp)
std::cout << elem << ' ';
std::cout << '\n';
}
int main() {
int arr[] = {1, 2, 3, 4, 5, 6};
std::span<int> sp(arr, 6);
// subspan(start, count):
std::span<int> sub = sp.subspan(2, /*count*/2);
printSpan(sub);
// fist(count):
std::span<int> subFirst3 = sp.first(3);
printSpan(subFirst3);
// last(count):
std::span<int> subLast4 = sp.last(4);
printSpan(subLast4);
}
Play @Compiler Explorer
The output:
3 4
1 2 3
3 4 5 6
Showing basic properties
Here’s another example that prints basic information about spans:
int main() {
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> vec = {1, 2, 3, 4, 5};
std::array sarr {1, 2, 3, 4, 5};
std::span<int, 5> arr_span {arr};
std::span<int> sarr_span {arr};
std::span<int> vec_span{vec};
auto span_info = [](std::string_view str, auto sp) {
std::cout <<
std::format("{}\n sizeof {}\n extent {}\n size in bytes: {}\n",
str, sizeof(sp), sp.extent == std::dynamic_extent ? "dynamic" : "static",
sp.size_bytes());
};
span_info("arr_span", arr_span);
span_info("sarr_span", sarr_span);
span_info("vec_span", vec_span);
}
The output from GCC:
arr_span
sizeof 8
extent static
size in bytes: 20
sarr_span
sizeof 16
extent dynamic
size in bytes: 20
vec_span
sizeof 16
extent dynamic
size in bytes: 20
C++26 span::at()
With C++26, std::span
gains the at()
method, which provides bounds-checked access to elements, similar to std::vector::at()
. This ensures safer indexing by throwing std::out_of_range
if an invalid index is accessed.
Have a look:
#include <span>
#include <vector>
#include <iostream>
#include <stdexcept>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
std::span<int> span = data;
try {
std::cout << "Element at index 2: " << span.at(2) << "\n";
std::cout << "Trying to access index 10...\n";
std::cout << span.at(10) << "\n";
} catch (const std::out_of_range& e) {
std::cerr << "Exception caught: " << e.what() << "\n";
}
}
Key benefits of this new feature:
- Safety: Prevents undefined behavior by ensuring out-of-bounds access throws an exception.
- Consistency: Aligns
std::span
with other standard containers (std::vector
,std::array
, etc.) that already haveat()
. - Debugging Aid: Makes debugging easier by immediately identifying invalid accesses.
C++26 span over an initializer list
With C++26, std::span
can now be constructed from an std::initializer_list
, closing a gap that previously existed between std::vector
and std::span
as function parameters. The change comes from the following proposal: P2447
While std::string_view
can be constructed directly from a string literal, std::span<const T>
couldn’t be constructed from a braced initializer list {1, 2, 3}
. This led to inconsistencies when using spans in function parameters.
Consider this example before C++26:
void process(std::span<const int> sp);
process({1, 2, 3}); // Error: No viable conversion
process(std::vector<int>{1, 2, 3}); // OK but unnecessary allocation
process(std::span<const int>(std::initializer_list<int>{1, 2, 3})); // Works but verbose
The C++26 Solution
With C++26, std::span<const T>
now has a constructor accepting std::initializer_list<T>
, allowing direct usage of braced initializer lists:
void process(std::span<const int> sp);
process({1, 2, 3}); // Now works in C++26!
process({}); // Also works, creating an empty span
This makes std::span<const T>
a better drop-in replacement for const std::vector<T>&
in function parameters, improving usability and reducing unnecessary copies.
See the full example:
#include <span>
#include <vector>
#include <print>
void process(std::span<const int> sp) {
std::println("sp size is {}", sp.size());
}
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
process(data);
process({1, 2, 3}); // Now works in C++26!
process({}); // Also works, creating an empty span
}
Check at Compiler Explorer
Important Notes on Dangling
Just like std::string_view
, std::span
does not extend the lifetime of temporary data. If used incorrectly, it can lead to dangling references:
std::span<const int> sp = {1, 2, 3}; // Dangles! The initializer list is a temporary.
This means std::span
over an initializer list is best used for function parameters, where the span is only needed within the function’s scope.
Returning std::span
While spans are great as input parameters, they can also show their strength as a return type. This can be a middle ground between references and copies, with additional flexibility for handling missing values.
Consider the following example, where there’s a config manager, and we return a Config
using getConfig
member function:
#include <span>
#include <vector>
#include <iostream>
#include <map>
class ConfigManager {
private:
std::map<std::string, std::vector<uint8_t>> configs;
public:
ConfigManager() {
configs["database"] = {
'h','o','s','t','=','l','o','c','a','l','h','o','s','t',';',
'p','o','r','t','=','5','4','3','2'
};
}
std::span<const uint8_t> getConfig(const std::string& name) const {
auto it = configs.find(name);
if (it != configs.end())
return it->second;
return {};
}
};
int main() {
ConfigManager manager;
auto dbConfig = manager.getConfig("database");
for (auto byte : dbConfig)
std::cout << static_cast<char>(byte);
auto errCfg = manager.getConfig("error");
for (auto byte : errCfg)
std::cout << static_cast<char>(byte);
}
By using span
we can clearly indicate a “view” type. Additionally, spans support many containers, so even if we change the internal representation of the config entry, we can still return a span object (assuming it’s still contiguous…).
Without spans, I’d had to return a reference to a vector, which might be tricky when there’s no entry found. See my other article about this issue: Improving Code Safety in C++26: Managers and Dangling References - C++ Stories.
Advantages of Using std::span
- Performance: Like references, spans avoid unnecessary copies.
- Safety: Being non-owning, spans make it clear that the caller doesn’t own the data.
- Interoperability: Works seamlessly with other container types like
std::array
or raw arrays. - Simpler handling of missing data: Unlike returning a
const std::vector&
, where you must return a valid reference,std::span
can simply return an empty span ({}
).
Comparing with std::string_view
- Generality:
std::span
can be used with any type, not just character types. - Mutability: Unlike
std::string_view
, astd::span
can modify the data it views (unless you define it as a span of const). - Extent:
std::span
can have either static or dynamic extent,string_view
is always “dynamic”.
Guidelines
Some of the guidelines related to spans:
- I.13: Do not pass an array as a single pointer
- F.24: Use a span or a span_p to designate a half-open sequence
- R.14: Avoid
[]
parameters, prefer span - ES.42: Keep use of pointers simple and straightforward
Summary
In this article we looked at std::span
introduced in C++20. This type offers a handy way to work with contiguous sequences like arrays or containers. We can say that it’s a more generic approach than std::string_view
as it allows read/write access (if needed).
Back to you
- Have you tried
std::span
? - Do you use other “Reference”/view types from the Standard Library?
Share your comments below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: