std::span
std::span
, and why we would want toIn the previous lesson, we introduced classic, C-style arrays. We also outlined some of their main problems and recommended they not be used because of those issues.
But sometimes, we can’t avoid them. In complex projects, we’ll often integrate with third-party libraries, or APIs provided by the platforms or operating systems we’re building for. Those libraries may provide us with C-style arrays, or expect us to provide them as arguments for their functions.
C++20 introduced the std::span
class to help us work with these arrays, while mitigating most of their problems.
In large projects prior to C++20, a version of span is likely to be available through a custom class, or third-party library.
The Guidelines Support Library (GSL) or boost are popular choices, each with an implementation of a span that works very similarly to std::span
from the C++20 standard library
The std::span
template class is available by including <span>
and can be constructed in the same way as any other object. We can provide a template parameter to specify what type of objects will be in the span:
#include <span>
std::span<int> Span{Values};
Below, we create a std::span
that is connected to a C-style array. Using class template argument deduction, we don’t need to explicitly state the elements are integers in this case:
#include <span>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span Span{Values};
}
By connected, we mean that the span has not created a copy of the elements. It simply provides a lightweight interface with which to access and work with the underlying array. We’ll discuss these properties in more detail a little later.
The most common place we’ll use a span is as a function parameter type, to receive a C-style array argument:
#include <span>
#include <iostream>
void HandleValues(std::span<int> Values){
std::cout << "Span Size: " << Values.size();
}
int main(){
int Values[]{1, 2, 3, 4, 5};
HandleValues(Values);
}
Span Size: 5
This example shows spans overcoming the first big problem with C-style arrays - the fact they decay to a pointer, and lose track of their size. With spans, this doesn’t happen - we’ve received a regular type, and we can access its size using the size()
 method.
Similar to containers like std::vector
and std::array
, we can access elements using the []
operator, using an index or an expression that results in an index:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
std::cout
<< "First: " << Span[0]
<< "\nSecond: " << Span[1]
<< "\nLast: " << Span[Span.size() - 1];
}
First: 1
Second: 2
Last: 5
The front()
and back()
methods give us direct access to the first and last elements of the span:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
std::cout
<< "First: " << Span.front()
<< "\nLast: " << Span.back();
}
First: 1
Last: 5
Spans are an example of a view - they do not own the underlying elements they’re providing access to. Those are owned by a different container - for example, the C-style array we passed to the std::span
 constructor.
This has two effects - Firstly, when it comes to performance, creating a span is inexpensive. This is because they don’t need to copy any of the underlying elements.
Secondly, when we change an element of the underlying array, that change is reflected in the span:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
Values[0] = 42;
std::cout << "First: " << Span.front();
}
First: 42
The opposite is also true - changing an element through the span also affects the original container from which the span was constructed:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
Span[0] = 42;
std::cout << "First: " << Values[0];
}
First: 42
const
Keyword with SpansIn most scenarios, we want the span to be a read-only view of the container from which it was created.
We can be explicit about this using the const
keyword. We can specify that the individual elements will not be changed, by marking their type as const
:
#include <span>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<const int> Span{Values};
Span[0] = 42; // Error - Span[n] is const
}
We can also make the span itself const
, which will prevent it from being reassigned:
#include <span>
int main(){
int Values[]{1, 2, 3, 4, 5};
const std::span<int> Span{Values};
Span = Values; // Error - Span is const
}
Finally, we can combine both techniques using the const
specifier twice, to ensure neither the span, nor the elements it is viewing are modified:
#include <span>
int main(){
int Values[]{1, 2, 3, 4, 5};
const std::span<const int> Span{Values};
Span[0] = 42; // Error - Span[n] is const
Span = Values; // Error - Span is const
}
We can iterate over the elements of a span in the usual ways. Because spans implement iterators, we will most typically use a range-based for loop:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
for (int x : Span) {
std::cout << x << ", ";
}
}
We cover iterators and range-based for loops in more detail a little later in the course.
Throughout this lesson, we’ve been creating spans from C-style arrays, but that is not their only use. Spans can be created from any container that holds its elements in a contiguous area of memory, such as a std::vector
or std::array
, or any custom type for which we provide appropriate methods.
This allows us to create functions that can potentially provide a simple, consistent interface for working with various types of arrays:
#include <span>
#include <iostream>
#include <vector>
#include <array>
void LogFirst(std::span<int> Values){
std::cout << Values.front() << ", ";
}
int main(){
int ValuesA[]{1, 2, 3, 4};
std::vector ValuesB{5, 6, 7};
std::array ValuesC{8, 9};
LogFirst(ValuesA);
LogFirst(ValuesB);
LogFirst(ValuesC);
}
1, 5, 9,
Remember, we also have access to all of the template-based techniques we covered in the previous lesson, allowing us to create even more versatile code:
#include <span>
#include <iostream>
#include <vector>
#include <array>
template <typename T>
void LogFirst(std::span<T> Values){
std::cout << Values.front() << ", ";
}
int main(){
int ValuesA[]{1, 2, 3, 4};
std::vector ValuesB{5.f, 6.f, 7.f};
std::array ValuesC{true, false};
LogFirst<int>(ValuesA);
LogFirst<float>(ValuesB);
LogFirst<bool>(ValuesC);
}
1, 5, 1,
When we need to get a C-style array from a span, we can access a pointer to the first element using the data()
 method.
Typically, the use case for this will be to generate arguments for a third-party library, in which case we’ll typically also need to include the size:
#include <span>
#include <iostream>
void HandleArray(int Array[], size_t Size){
for (size_t i{0}; i < Size; ++i) {
std::cout << Array[i] << ", ";
}
}
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
HandleArray(Span.data(), Span.size());
}
1, 2, 3, 4, 5,
Note this technique will create a shallow copy of the array. If we want a deep copy of all elements in the array, we’ll need to use one of the techniques covered in the previous lesson on C-style arrays.
When we need to create a standard library container such as an array or span, the easiest way typically involves iterator pairs.
We cover iterators in more detail but, for now, we can just note that spans include a begin()
and end()
method, denoting where their elements start and where they end.
Standard library containers such as std::vector
and std::array
have a constructor that can accept such a pair of iterators, and will use them to initialize themselves with a copy of everything in that range:
#include <iostream>
#include <vector>
#include <span>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
std::vector<int> Vec{
Span.begin(), Span.end()
};
}
The typical scenario where we’ll want to convert a span to a vector or array is in order to pass it as a function argument. In those cases, we shouldn’t create an intermediate copy of the array - we can just pass the constructor arguments as a list, and let our function call create the vector:
#include <iostream>
#include <vector>
#include <span>
void HandleVector(std::vector<int> Vec){
for (int x : Vec) { std::cout << x << ", "; }
}
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
HandleVector(
{Span.begin(), Span.end()}
);
}
1, 2, 3, 4, 5,
Spans can be created from other spans in the usual way:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
std::span<int> Another{Span};
for (int x : Another) {
std::cout << x << ", ";
}
}
These spans can also be restricted to just including a subset of the original span. For example, using the first()
method, we can restrict the sub span to just viewing an initial number of records, defined by the argument we pass:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
std::span<int> First3{Span.first(3)};
std::cout << "First Three: ";
for (int x : First3) {
std::cout << x << ", ";
}
}
First Three: 1, 2, 3,
The last()
method returns a span that includes records from the end of the collection. Below, we return a view of the last 3Â elements:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
std::span<int> Last3{Span.last(3)};
std::cout << "Last Three: ";
for (int x : Last3) {
std::cout << x << ", ";
}
}
Last Three: 3, 4, 5,
Finally, we can pass a starting position and record count to the subspan()
method, constraining the view to any range. Below, we return a view that starts at index 1, and contains 3Â elements:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
std::span<int> Middle3{Span.subspan(1, 3)};
std::cout << "Middle Three: ";
for (int x : Middle3) {
std::cout << x << ", ";
}
}
Remember, spans are just a view of an underlying record. This includes spans created from other spans. A subspan is just restricting the view to a smaller set of elements in the underlying array. The elements we’re viewing are still the elements in that array, and by accessing them through the span, we are still accessing the elements of the underlying array.
Views are a large part of modern C++, and a span is just one type of view. We’ll cover views in more detail later in this course, including more robust ways to create them, and how we can use them to make complex tasks much easier.
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.