Definition
Available in C++20, std::span
is a view of a contiguous sequence of objects. It is a struct like:
struct span<T>{
T* pointer; // start of sequence
size_t size; // size of sequence
}
with many useful functions to access and modify data.
Compiler
To compile examples here, I use GCC 10.2 with flag -std=c++20
.
Basics
span
interface can attach to a C-array:
#include<span> // remember header
int main(){
int a[2]={4,3};
std::span<int> s = a;
std::cout<< s[0] <<s[1];// 4 3
return 0;}
to a dynamic pointer array:
int* a = new int[2]{8, 8};
std::span<int> s{a, 2}; // size=2
to an array:
std::array<int,2> a = {5,6};
std::span<int> s = a;
to a vector:
std::vector<int> a = {1,1};
std::span<int> s = a;
and to a part of array or vector:
std::vector<int> v = {1,2,3,4};
std::span<int> s{v.data()+2,2};
std::cout<< s[0]<<s[1]<<'\n'; // 3 4
The compiler can automatically deduce the type of a span at initialization, so this is correct:
std::vector<int> a = {1,1};
std::span s = a; // span<int>
Member functions
Similiar to standard containers, span
has many useful functions that facilitate working with a sequence.
For a span like:
std::vector<int> v = {1,2,3,4};
std::span s = v;
We can traverse items with range-base loop:
for (auto& item:s)
std::cout<<item;
Read or write elements:
auto a = s.front(); // first element of sequence
auto b = s.back(); // last element of sequence
auto c = s[2]; // access via []
auto d = s.data(); // pointer to first element
Check the size:
auto e = s.size();
bool f = s.empty();
Get iterators:
auto g = s.begin(); // iterator to begining
auto h = s.end();// iterator to end
Subspan
A subspan is a span made of another span. The subspan is shorter or the same size as the primary span. For example, for a span
std::vector<int> v = {1,2,3,4,5,6};
std::span s = v;
we can get a span for the first 3 items:
auto s1 = s.first(3); // {1, 2, 3}
last 2 items:
auto s2 = s.last(2); // {5,6}
or an arbitrary span:
// subspan(offset, count)
auto s3 = s.subspan(2, 3); // {3,4,5}
If the count is not given, subspan will be from offset to the end:
// subspan(offset)
auto s4 = s.subspan(2); // {3,4,5,6}
Don’ts
Do not change the memory of a sequence, while working with a span pointing to it.
In the example below, a vector is assigned to a span, the vector’s memory location changed, then the span reused. This causes undefined behavior:
#include <iostream>
#include <vector>
#include <span>
int main(){
std::vector<int> v = {1,2,3,4};
std::cout<< v.capacity() <<'\n'; // 4
// span defined
std::span s = v;
// target memory changed
v.push_back(5);
// undefined behaviour
std::cout<< s[0] <<'\n';
return 0;}
Note that when we push_back(5)
, the vector memory is deleted and a new memory in a different location is allocated to fit in the new item, 5. But span, s
, is still pointing to the old memory of the vector.
For the same reason, don’t do this either:
#include <iostream>
#include <span>
int main(){
int* a = new int[4] {1,2,3,4};
// span defined
std::span s {a, 4};
// memory gone
delete a;
// undefined behaviour
std::cout<< s[0]<<'\n';
return 0;
}
Function parameter
It’s very common to pass a vector or array to a function, and the function only wants to read/write elements but doesn’t add/delete elements or deallocate the memory; that’s when span comes in handy.
See this example:
int sum(std::span<int> items){
int s = 0;
for (auto& item:items)
s+=item;
return s;
}
sum
reads the items but doesn’t delete any item i.e. it doesn’t own the memory. The outer scope is responsible to pass valid memory to sum
.
Now we can pass various contiguous sequences to sum
:
int main(){
std::vector<int> v = {1,1,1};
std::array<int,4> a = {1,1,1,1};
int b[5]{1,1,1,1,1};
std::cout<< sum(v)<<'\n'; // 3
std::cout<< sum(a)<<'\n'; // 4
std::cout<< sum(b)<<'\n'; // 5
return 0;
}
Note that if a C-array decayed to a pointer, the size information is striped and our sum
function won’t work:
int* c = new int[6]{1,1,1,1,1,1};
std::cout<< sum(c);
// Error: don't know the size of c,
// so cannot convert int* to span<int>
In this case, we can construct a temporary span and pass it to the function:
int* c = new int[6]{1,1,1,1,1,1};
std::cout<< sum(std::span{c,6}); // 6
Class member
A span can be a class member. It is an observer of a sequence of objects. The class is not responsible for managing the memory of the sequence. It can read/write the objects, but cannot delete them. The user of the class is responsible for providing a valid contiguous memory.
Static span
We can specify the size of a span to be fixed at runtime:
std::span<Type, Size>
We can directly assign a std::array
but not a std::vector
to a fixed span, see the example below:
#include <iostream>
#include <vector>
#include <array>
#include <span>
int main(){
int a[4] {1,2,3,4};
std::vector<int> v = {1,2,3,4};
std::array<int,4> arr = {1,2,3,4};
// static span
std::span<int,4> s = a;
// reassign
s = arr; // OK
// Error: assign span<int,4> to vector<int>
s = v;
for (auto& item:s)
std::cout<< item<<'\n';
return 0;
}
As it is shown before, if we don’t specify the size of a span, it will be dynamic during runtime.
Latest Posts
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?
- A C++ MPI code for 2D arbitrary-thickness halo exchange with sparse blocks