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 before 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 - the container we provided to the std::span
constructor.
In the previous example, the elements are owned by the C-style array called Values
. The std::span
we called Span
simply provides a lightweight wrapper, allowing us to access the underlying container in a friendlier way.
In other words, a span is an example of a view. We’ll see more examples of views throughout the course.
This has two effects:
We can demonstrate the second point below:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
// Modifying an object in the container:
Values[0] = 42;
// The change is reflected in the view:
std::cout << "First: " << Span.front();
}
First: 42
The opposite is also true - changing an element through the span also affects the original container that the span is viewing:
#include <span>
#include <iostream>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::span<int> Span{Values};
// Modifying an object through the view:
Span[0] = 42;
// The change is reflected in the container:
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
}
error C3892: 'Span': you cannot assign to a variable that is const
When using a view as a function parameter, it is a relatively important tenet of const
-correctness that we mark implement this technique:
#include <iostream>
#include <span>
void LogFirst(std::span<const int> Values) {
std::cout << Values.front();
}
int main() {
int Values[]{1, 2, 3, 4, 5};
LogFirst(Values);
}
As we covered above, spans are a view - they’re accessing elements within the original collection. If we don’t plan to modify those elements, we should mark them as const
just as if we were passing them by reference.
Less importantly, 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
}
error C2678: binary '=': no operator found which takes a left-hand operand of type 'const std::span'
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
}
error C3892: 'Span': you cannot assign to a variable that is const
error C2678: binary '=': no operator found which takes a left-hand operand of type 'const std::span'
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 chapter.
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
, a std::array
, or any custom type for which we provide appropriate methods.
This allows us to create functions that can provide a simple, consistent interface for working with any type of array.
Below, our LogFirst()
function accepts a std::span
, meaning it is not unnecessarily tying consumers to any specific array implementation. They can use whatever they want:
#include <span>
#include <iostream>
#include <vector>
#include <array>
void LogFirst(const 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, 8,
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. The Array
parameter in HandleArray()
will be pointing to the same memory location as the Values
variable in main()
.
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, such as memcpy()
.
When we need to create a standard library container such as an array or span, the easiest way typically involves iterators.
We cover iterators in more detail soon but, for now, we can just note that spans include a begin()
and end()
method, denoting where their elements start and where they end.
Most sequential containers will support iterators, and this includes standard library containers such as std::vector
and std::array
Both of these containers 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()
};
std::cout << "Vec Size: " << Vec.size();
}
Vec Size: 5
The typical scenario where we’ll want to convert a span to a vector or array is to pass it as a function argument.
In those cases, we shouldn’t create an intermediate copy of the array and then copy it, as that has a performance impact. Instead, 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,
first()
, last()
and subspan()
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 subspan 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 sub-span 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 original memory locations.
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.
In this lesson, we explored the functionality and purpose of std::span
in C++20. Spans provide a safe and efficient way to handle and manipulate arrays and other contiguous data structures without owning the data.
Key Takeaways:
std::span
was introduced in C++20 to provide a safer and more flexible way to handle arrays, especially when dealing with legacy C-style arrays.std::span
overcomes the limitation of C-style arrays by maintaining size information, allowing safer and more robust code.front()
and back()
.const
keyword can be used with spans to create read-only views of the data.std::span
is versatile, supporting various container types like std::vector
, std::array
, and C-style arrays.first()
, last()
, and subspan()
methods for more targeted data manipulation.std::span
A detailed guide to creating a "view" of an array using std::span
, and why we would want to
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.