Last Update:
How to use std::span from C++20
Table of Contents
In this article, we’ll look at std::span
which is more generic than string_view
and can help work with arbitrary contiguous collections.
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”).
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.
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:
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 bnasic 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
Comparing with std::string_view
- Generality:
std::span
can be used with any type, not just char 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: