Table of Contents

In this article, we’ll look at std::span, which has been available since C++20. This “view” type is more generic than string_view and can help work with arbitrary contiguous collections.

Updated in Feb 2025: added section about returning spans and C++26 improvements (.at() and creatoion from initializer list).

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

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:

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.

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

C++26 span::at()  

With C++26, std::span gains the at() method, which provides bounds-checked access to elements, similar to std::vector::at(). This ensures safer indexing by throwing std::out_of_range if an invalid index is accessed.

Have a look:

#include <span>
#include <vector>
#include <iostream>
#include <stdexcept>

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    std::span<int> span = data;

    try {
        std::cout << "Element at index 2: " << span.at(2) << "\n";
        std::cout << "Trying to access index 10...\n";
        std::cout << span.at(10) << "\n";
    } catch (const std::out_of_range& e) {
        std::cerr << "Exception caught: " << e.what() << "\n";
    }
}

See @Compiler Explorer

Key benefits of this new feature:

  • Safety: Prevents undefined behavior by ensuring out-of-bounds access throws an exception.
  • Consistency: Aligns std::span with other standard containers (std::vector, std::array, etc.) that already have at().
  • Debugging Aid: Makes debugging easier by immediately identifying invalid accesses.

C++26 span over an initializer list  

With C++26, std::span can now be constructed from an std::initializer_list, closing a gap that previously existed between std::vector and std::span as function parameters. The change comes from the following proposal: P2447

While std::string_view can be constructed directly from a string literal, std::span<const T> couldn’t be constructed from a braced initializer list {1, 2, 3}. This led to inconsistencies when using spans in function parameters.

Consider this example before C++26:

void process(std::span<const int> sp);

process({1, 2, 3}); // Error: No viable conversion
process(std::vector<int>{1, 2, 3}); // OK but unnecessary allocation
process(std::span<const int>(std::initializer_list<int>{1, 2, 3})); // Works but verbose

The C++26 Solution

With C++26, std::span<const T> now has a constructor accepting std::initializer_list<T>, allowing direct usage of braced initializer lists:

void process(std::span<const int> sp);

process({1, 2, 3}); // Now works in C++26!
process({});        // Also works, creating an empty span

This makes std::span<const T> a better drop-in replacement for const std::vector<T>& in function parameters, improving usability and reducing unnecessary copies.

See the full example:

#include <span>
#include <vector>
#include <print>

void process(std::span<const int> sp) {
    std::println("sp size is {}", sp.size());
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    process(data);
    process({1, 2, 3}); // Now works in C++26!
    process({});        // Also works, creating an empty span
}

Check at Compiler Explorer

Important Notes on Dangling

Just like std::string_view, std::span does not extend the lifetime of temporary data. If used incorrectly, it can lead to dangling references:

std::span<const int> sp = {1, 2, 3}; // Dangles! The initializer list is a temporary.

This means std::span over an initializer list is best used for function parameters, where the span is only needed within the function’s scope.

Returning std::span  

While spans are great as input parameters, they can also show their strength as a return type. This can be a middle ground between references and copies, with additional flexibility for handling missing values.

Consider the following example, where there’s a config manager, and we return a Config using getConfig member function:

#include <span>
#include <vector>
#include <iostream>
#include <map>

class ConfigManager {
private:
    std::map<std::string, std::vector<uint8_t>> configs;

public:
    ConfigManager() {
        configs["database"] = {
            'h','o','s','t','=','l','o','c','a','l','h','o','s','t',';',
            'p','o','r','t','=','5','4','3','2'
        };
    }

    std::span<const uint8_t> getConfig(const std::string& name) const {
        auto it = configs.find(name);
        if (it != configs.end()) 
            return it->second; 
        return {}; 
    }
};

int main() {
    ConfigManager manager;
    auto dbConfig = manager.getConfig("database");
    for (auto byte : dbConfig)
        std::cout << static_cast<char>(byte);

    auto errCfg = manager.getConfig("error");
    for (auto byte : errCfg)
        std::cout << static_cast<char>(byte);
}

See @Compiler Explorer

By using span we can clearly indicate a “view” type. Additionally, spans support many containers, so even if we change the internal representation of the config entry, we can still return a span object (assuming it’s still contiguous…).

Without spans, I’d had to return a reference to a vector, which might be tricky when there’s no entry found. See my other article about this issue: Improving Code Safety in C++26: Managers and Dangling References - C++ Stories.

Advantages of Using std::span  

  • Performance: Like references, spans avoid unnecessary copies.
  • Safety: Being non-owning, spans make it clear that the caller doesn’t own the data.
  • Interoperability: Works seamlessly with other container types like std::array or raw arrays.
  • Simpler handling of missing data: Unlike returning a const std::vector&, where you must return a valid reference, std::span can simply return an empty span ({}).

Comparing with std::string_view  

  1. Generality: std::span can be used with any type, not just character 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.