Array Spans and 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.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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

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 - 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:

  1. Creating a span is inexpensive. There is a minimal performance overhead because a view does not need to copy any of the underlying elements.
  2. Because a view and the container it is viewing point to the same memory address, modifying an object through one affects the other

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

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

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

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, 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,

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. 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().

Creating Other Containers from Spans

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,

Creating Subspans using 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.

Summary

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.
  • Spans are lightweight and do not own the data they point to, allowing for efficient manipulation of arrays without data duplication.
  • The use of std::span overcomes the limitation of C-style arrays by maintaining size information, allowing safer and more robust code.
  • Elements within a span can be accessed using standard subscript notation and methods like front() and back().
  • Spans support iteration using range-based for loops, similar to standard C++ containers.
  • The 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.
  • Template-based techniques can be employed with spans to handle different data types more flexibly.
  • Spans can be converted back to C-style arrays or other containers, providing interoperability with different parts of a codebase.
  • Subspans can be created using first(), last(), and subspan() methods for more targeted data manipulation.

Was this lesson useful?

Next Lesson

Multidimensional Arrays and std::mdspan

A guide to std::mdspan, allowing us to interact with arrays as if they have multiple dimensions
3D art showing character concept art
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
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
Arrays and Linked Lists
A computer programmer
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:

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

Multidimensional Arrays and std::mdspan

A guide to std::mdspan, allowing us to interact with arrays as if they have multiple dimensions
3D art showing character concept art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved