Table of Contents

In this article, we’ll look at std::span which is more generic than string_view and can help work with arbitrary contiguous collections.

A Motivating Example  

Here’s an example that illustrates the primary use case for std::span:

In traditional C (or low-level C++), you’d pass an array to a function using a pointer and a size like this:

void process_array(int* arr, std::size_t size) {
  for(std::size_t i = 0; i < size; ++i) {
    // do something with arr[i]
  }
}

std::span simplifies the above code:

void process_array(std::span<int> arr_span) {
  for(auto& elem : arr_span) {
    // do something with elem
  }
}

The need to pass a separate size variable is eliminated, making your code less error-prone and more expressive.

And in essence: std::span<T> is:

  • a lightweight abstraction of a contiguous sequence of values of type T,
  • more or less implemented as struct { T * ptr; std::size_t length; },
  • a non-owning type (i.e. a “reference type” rather than a “value type”).

This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.

Construction of std::span  

std::span lives in its own new header <span>. It’s defined as follows:

template<class T, std::size_t Extent = std::dynamic_extent>
class span;

To create this object, you have two basic options: static extent and dynamic:

Static Extent  

When you know the size at compile-time:

int arr[] = {1, 2, 3, 4, 5};
std::span<int, 5> arr_span {arr};
//std::span<int, 2> arr_span2 {arr}; // error size doesn't match

Run @Compiler Explorer

Here, the 5 is an integral part of the type. You’ll get a compiler error if you try to initialize this span with an array of different sizes.

Dynamic Extent  

When you only know the size at runtime, like when working with vectors:

int arr[] = {1, 2, 3, 4, 5};
std::vector v { 1, 2, 3, 4, 5};
std::span<int> arr_span {arr}; // dynamic extent
std::span<int> vec_span {v};   // also!

See @Compiler Explorer

Notice the absence of size in the type? That’s the dynamic extent in action.

Sizeof span  

The interesting part about span is that when the size is static, then the type is smaller as there’s no need to store the size of the sequence:

int arr[] = {1, 2, 3, 4, 5};
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int, 5> arr_span {arr};
std::span<int> other_span {arr};
std::span<int> vec_span{vec};

std::cout << std::format("sizeof arr_span: {}\n", sizeof(arr_span));
std::cout << std::format("sizeof other_span: {}\n", sizeof(other_span));
std::cout << std::format("sizeof vec_span: {}\n", sizeof(vec_span));

Run @Compiler Explorer

The output on GCC is:

sizeof arr_span: 8
sizeof other_span: 16
sizeof vec_span: 16

More construction options  

For completeness, let’s now revise other construction options following the list of available constructors:

Default Constructor  

This constructs an empty span.

std::span<int> empty_span;

.data() returns nullptr and the size() returns 0 in this case.

From Iterators and Size  

Creates a span from a starting iterator and a size.

std::vector<int> vec = {1, 2, 3};
std::span<int> from_iterator_and_size(vec.begin(), /*count*/2); // 1, 2

std::span<int> from_iterator_and_size2(vec.begin(), /*count*/3); // 1, 2, 3

And also in CTAD version:

std::vector vec = {1, 2, 3};
std::span from_iterator_and_size(vec.begin(), /*count*/2); // 1, 2
 
std::span from_iterator_and_size2(vec.begin(), /*count*/3); // 1, 2, 3

See @Compiler Explorer

From Two Iterators  

Constructs a span from a range specified by two iterators.

std::span<int> from_two_iterators(vec.begin(), vec.end());

And using CTAD:

std::span from_iterator_and_end(vec.begin(), vec.end());

See @Compiler Explorer

From C-style Array  

For C-style arrays.

int arr[] = {1, 2, 3};
std::span<int> from_array(arr);
std::span from_array2(arr);

.data() returns std::data(arr)

See @Compiler Explorer

From std::array  

Can construct both from non-const and const std::array.

std::array<int, 3> std_arr = {1, 2, 3};
std::span<int, 3> from_std_array(std_arr);
// CTAD:
std::span from_std_array2(std_arr);

const std::array<int, 3> const_std_arr = {1, 2, 3};
std::span<const int> from_const_std_array(const_std_arr);

See @Compiler Explorer

From Contiguous Range  

Using this constructor, you can pass in any contiguous range like std::vector.

std::vector vec {1, 2, 3, 4, 5};
std::span<int> from_range(vec); // covers entire vec
// CTAD:
std::span from_range2(vec);ec

See @Compiler Explorer

Conversion from Another Span  

Can be used for type conversions if the types are compatible.

std::span<int> int_span(vec);
std::span<const int> const_span = int_span; // conversion

Passing spans  

Spans are lightweight objects intended to pass by value. But we have two options to preserve the constness of its elements:

void print(span<const char> outbuf);
void transform(span<char> inbuf);

In other words, you can pass span<const T> to indicate constant elements, and “read only” access, or pass span<T> to allow read/write access.

For example:

void transform(std::span<char> outbuf) {
    for (auto& elem : outbuf) {
        elem += 1;
    }
}

void output(std::span<const char> outbuf) {
    std::cout << "contents: ";
    for (auto& elem : outbuf) {
        std::cout << elem << ", ";
//        elem = 0; // error!
    }
    std::cout << '\n';
}

int main() {
    std::string str = "Hello World";
    std::span<char> buf_span(str);

    output(str);
    transform(buf_span);
    output(buf_span);
}

Run @Compiler Explorer

The output:

contents: H, e, l, l, o,  , W, o, r, l, d, 
contents: I, f, m, m, p, !, X, p, s, m, e, 

The example above shows that str nicely converts into a span and is passed to the output function. And later, the buf_span is also converted (char to const char) when passing to output.

Subspans  

You can easily create subviews/subspans of existing spans:

void printSpan(std::span<const int> sp) {
    for (auto&& elem : sp)
        std::cout << elem << ' ';
    std::cout << '\n';
}

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6};
    std::span<int> sp(arr, 6);

    // subspan(start, count):
    std::span<int> sub = sp.subspan(2, /*count*/2);
    printSpan(sub);

    // fist(count):
    std::span<int> subFirst3 = sp.first(3);
    printSpan(subFirst3);

    // last(count):
    std::span<int> subLast4 = sp.last(4);
    printSpan(subLast4);
}

Play @Compiler Explorer

The output:

3 4 
1 2 3 
3 4 5 6 

Showing basic properties  

Here’s another example that prints bnasic information about spans:

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::array sarr {1, 2, 3, 4, 5};
    std::span<int, 5> arr_span {arr};
    std::span<int> sarr_span {arr};
    std::span<int> vec_span{vec};

    auto span_info = [](std::string_view str, auto sp) {
        std::cout << 
           std::format("{}\n sizeof {}\n extent {}\n size in bytes: {}\n",   
           str, sizeof(sp), sp.extent == std::dynamic_extent ? "dynamic" : "static", 
                             sp.size_bytes());
    };
    span_info("arr_span", arr_span);
    span_info("sarr_span", sarr_span);
    span_info("vec_span", vec_span);
}

Run @Compiler Explorer

The output from GCC:

arr_span
 sizeof 8
 extent static
 size in bytes: 20
sarr_span
 sizeof 16
 extent dynamic
 size in bytes: 20
vec_span
 sizeof 16
 extent dynamic
 size in bytes: 20

Comparing with std::string_view  

  1. Generality: std::span can be used with any type, not just char types.
  2. Mutability: Unlike std::string_view, a std::span can modify the data it views (unless you define it as a span of const).
  3. Extent: std::span can have either static or dynamic extent, string_view is always “dynamic”.

Guidelines  

Some of the guidelines related to spans:

Summary  

In this article we looked at std::span introduced in C++20. This type offers a handy way to work with contiguous sequences like arrays or containers. We can say that it’s a more generic approach than std::string_view as it allows read/write access (if needed).

Back to you

  • Have you tried std::span?
  • Do you use other “Reference”/view types from the Standard Library?

Share your comments below.