Eventually, I have decided to try C++20 π
Let's start with std::span
!
What is a Span?
Here is the definition given by cppreference:
The class template
span
describes an object that can refer to a contiguous sequence of objects with the first element of the sequence at position zero.
Spans are sometimes called "views" because they don't own the sequence of objects.
If you are familiar std::string_view
from C++17, then you can easily understand the concept of spans. std::string_view
s are somehow spans on sequences of characters. The main difference is that spans can modify the objects while string views can't modify the caracters.
In the following, I will try to use std::span
s in various situations to understand how they work and how they can be useful for future code.
With Plain Arrays
I personally use a lot of plain arrays because I work with on embedded systems where those C-style arrays are widely used. Why? Because dynamic allocation is forbidden (so std::vector
s are not an option) and because there is a lot of C code.
When you pass a plain array to a function, it is decayed to a pointer to its first element and its size is lost.
std::span
s sound like a great solution to safely pass around plan arrays to functions.
First, let's write a template function that prints any span:
#include <iostream>
#include <span>
template<typename T, std::size_t length>
void print(std::span<T, length> span) {
for(auto i : span) {
std::cout << i << ' ';
}
std::cout << '\n';
}
Then, let's create a span from a plain array:
int main() {
int data[] = {1, 2, 3, 4, 5};
print(std::span{data});
}
This code outputs:
1 2 3 4 5
The size of the array is deduced from its type and stored in the std::span
object.
With a Pointer and a Size
If you are already in a function where the original plain array has been "transformed" to pointer + size, you can create a span from them. Of course, you cannot be sure it maps the original plain array:
void function(int* pointer, unsigned int size) {
printf("> ");
print(std::span{pointer, size});
}
int main() {
int data[] = {1, 2, 3, 4, 5};
function(data, 0);
function(data, 2);
function(data, 5);
function(data, 10);
}
Output:
>
> 1 2
> 1 2 3 4 5
> 1 2 3 4 5 32766 0 0 4199488 0
The last span is reading outside of the array, which is clearly an undefined behavior. You are responsible for creating the span with a valid sequence.
The form (2) of std::span
's constructors is used here:
template< class It >
explicit(extent != std::dynamic_extent)
constexpr span( It first, size_type count );
With std::array
s or std::vector
s
You can also easily create spans from std::array
s or std::vector
s:
int main() {
std::array a = {1, 2, 3, 4, 5};
std::vector v = {6, 7, 8, 9, 10};
print(std::span{a});
print(std::span{v});
}
Output:
1 2 3 4 5
6 7 8 9 10
Obviously, constructor (5) is used for the array:
template< class U, std::size_t N >
constexpr span( std::array<U, N>& arr ) noexcept;
But which one is used for the vector?
It took me a while to understand that form (7) is used:
template< class R >
explicit(extent != std::dynamic_extent)
constexpr span( R&& r );
This is because a vector is a range :
#include <iostream>
#include <span>
#include <ranges>
#include <vector>
int main() {
std::vector v = {1, 2, 3, 4, 5};
auto r = std::ranges::range<decltype(v)>;
std::cout << std::boolalpha << r;
}
std::ranges::range
is a concept, a new feature of C++20. Concepts are one of the next features I will try.
With a part of an std::array
or std::vector
You can't directly create a span over a part of the array or the vector. For instance:
int main() {
std::array a = {1, 2, 3, 4, 5};
print(std::span{a, 2});
}
doesn't compile because of the following error:
error: no viable constructor or deduction guide for deduction of template arguments of 'span'
The same error occurs with an std::vector
.
The trick is to get an iterator/pointer from the container. For instance:
int main() {
std::array a = {1, 2, 3, 4, 5};
print(std::span{a.data(), 2});
print(std::span{a.cbegin() + 2, a.cbegin() + a.size()});
}}
Output:
1 2
3 4 5
Here, constructors (2) and (3) are used:
// (2)
template< class It >
explicit(extent != std::dynamic_extent)
constexpr span( It first, size_type count );
// (3)
template< class It, class End >
explicit(extent != std::dynamic_extent)
constexpr span( It first, End last );
Subspans
It is possible to create subspans, which are spans on a part of a span. There are 3 member functions to do that:
-
first()
: obtains a subspan consisting of the first N elements of the sequence -
last()
: obtains a subspan consisting of the last N elements of the sequence -
subspan()
: obtains a subspan
Example:
int main() {
std::array data = {1, 2, 3, 4, 5};
auto span = std::span{data};
print(span);
print(span.first(3));
print(span.last(3));
print(span.subspan(1, 3));
}
Output:
1 2 3 4 5
1 2 3
3 4 5
2 3 4
Modify Objects in the Span
It is possible to modify the objects the span refers to:
template<typename T, std::size_t length>
void change(std::span<T, length> span) {
std::transform(span.begin(), span.end(), span.begin(), std::negate());
}
int main() {
std::array data = {1,2,3,4,5};
print(std::span{data});
change(std::span{data});
print(std::span{data});
}
Output:
1 2 3 4 5
-1 -2 -3 -4 -5
Note than std::span
doesn't have cbegin()
and cend()
as iterator functions. It only has begin()
/ end()
/ rbegin()
/ rend()
.
Nevertheless, if we declare data
as const std::array data = {1,2,3,4,5};
, then the code won't compile. Here is an example where we try to modify a const
array using a span:
int main() {
const std::array data = {1,2,3,4,5};
auto s = std::span{data};
s[0] = 42;
}
This doesn't compile:
error: assignment of read-only location 's.std::span<const int, 5>::operator[](0)'
Static vs Dynamic Extent
The size of the sequence of objects can be static or dynamic, as stated in the overview of the class:
A span can either have a static extent, in which case the number of elements in the sequence is known at compile-time and encoded in the type, or a dynamic extent.
If a span has dynamic extent a typical implementation holds two members: a pointer to T and a size. A span with static extent may have only one member: a pointer to T.
Let's create a function to print the extent of spans:
template<typename T, std::size_t length>
void print_extent(std::span<T, length> span) {
std::cout << std::hex << "0x" << span.extent << '\n';
}
int main() {
std::array array = {1,2,3,4,5};
auto span = std::span{array};
print_extent(span);
print_extent(span.subspan(1, 3));
std::vector<int> vector;
print_extent(std::span{vector});
int c_array[] = {42};
print_extent(std::span{c_array});
}
Output:
0x5
0xffffffffffffffff
0xffffffffffffffff
0x1
Those 0xffffffffffffffff
do look like std::dynamic_extent
.
A dynamic extent really means that the size of the span changes along with the size of the sequence. Here is an example where we push items to a vector and look how the size of the span evolves (we know from the previous example it has dynamic extent):
template<typename T, std::size_t length>
void print(std::span<T, length> span) {
std::cout << "Size = " << span.size() << " --> ";
for(auto i : span) {
std::cout << i << ' ';
}
std::cout << '\n';
}
int main() {
std::vector<int> vector;
print(std::span{vector});
vector.push_back(42);
print(std::span{vector});
vector.push_back(66);
print(std::span{vector});
}
Output:
Size = 0 -->
Size = 1 --> 42
Size = 2 --> 42 66
Non-Template Code with Dynamic Extent
A span with a static extent can be converted implicitly to span with a dynamic extent:
#include <iostream>
#include <span>
int main() {
char buffer[] = "hello, world";
std::span<char, 13> static_span{buffer};
std::span<char> dynamic_span{static_span};
std::cout << std::boolalpha;
std::cout << (static_span.extent == std::dynamic_extent) << '\n';
std::cout << (dynamic_span.extent == std::dynamic_extent) << '\n';
}
Output:
false
true
Why is this really interesting? Because we can rely on this conversion to write non-template functions that take spans of any size as their parameters.
This is something we cannot do with std::array
s.
Compare these 2 functions that prints arrays:
#include <array>
#include <iostream>
#include <span>
void print(std::span<char> span) {
for(auto c : span) std::cout << c;
std::cout << '\n';
}
template<std::size_t size>
void print(std::array<char, size> array) {
for(auto c : array) std::cout << c;
std::cout << '\n';
}
int main() {
std::array data{'h', 'e', 'l', 'l', 'o'};
print(data);
print(data);
}
Both of course output "hello". Because the first one is non-template, it can be simply declared in a *.h and defined in a *.c.
This is not possible of course for the second one.
But I think you already know that "issue" with templates, right? π
sizeof()
And finally, because I am an embedded software developer and I care about footprint, let's take a look at the size of a span:
int main() {
std::vector<int> vector;
auto sv = std::span{vector};
std::cout << sizeof(sv) << '\n';
std::array<int, 10> array;
auto sa = std::span{array};
std::cout << sizeof(sa) << '\n';
int c_array[10];
auto sca = std::span{c_array};
std::cout << sizeof(sca) << '\n';
}
Output:
16
8
8
I didn't test the ROM footprint because I don't have a C++20 compatible embedded compiler on my computer yet.
Conclusion
OK, spans look amazing π
I believe they will help write me better code, mainly when I deal with plain arrays I get from C code. Spans will provide a good solution (safe, standard, portable) to carry the size of the data along with a pointer to them. They will probably be a nice bridge between the C and the C++ words.
Top comments (0)