Array Views using std::span

A detailed guide to creating a "view" of an array using std::span, and why we would want to
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

3D Concept Art
Ryan McCombe
Ryan McCombe
Posted

In 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.

Spans Prior to C++20

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

Creating Spans

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.

Span Size

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.

Element Access

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 a View

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

The const Keyword with Spans

In 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 
}

Iteration

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.

Multiple Array Types

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,

Span Templates

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, 

Creating C-Style Arrays from Spans

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.

Creating Vectors and Arrays from Spans

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,

Creating Spans and Subspans from Spans

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.

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Arrays and Linked Lists
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Multidimensional Arrays and std::mdspan

A guide to std::mdspan, which allows us to interact with arrays as if they have multiple dimensions
Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved