Iterators, Algorithms, and Ranges

Introduction to std::span

Functions are sometimes needed to inspect or modify contiguous sequences of values without acquiring ownership of their storage and without caring which concrete container the values inhabit. The C++ standard library has std::span for just such situations. std::span is a very small object that refers to an existing contiguous sequence; it doesn't allocate, it doesn't own, and it doesn't extend the lifetime of the data it refers to.

First, let's construct an ordinary std::vector, and then form a span to refer to its entire contents:

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

int main()
{
    std::vector v{ 10, 20, 30, 40, 50 };
    // Create a non-owning view over contiguous elements
    std::span s{ v };

    for (int n : s)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

The vector v owns five integers. The declaration std::span s{ v } constructs a span referring to these same five integers. Inside the loop, the name s is like a lightweight sequence, with s.begin() and s.end() producing iterators, the range based for loop walking those iterators, and each element being printed.

This program will output 10 20 30 40 50.

The span contains only a pointer and a length; it does not copy the integers.

A span also refers to a specified section of a sequence. The subspan member function produces a span with a specified starting offset and a given count:

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

int main()
{
    std::vector v{ 10, 20, 30, 40, 50 };
    std::span s{ v };

    std::span middle{ s.subspan(1, 3) };

    // Visit each element in the range directly
    for (int n : middle)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

s.subspan(1, 3) asks for a span starting at the second element of s and spanning three elements. The resulting span middle thus refers to the values 20, 30, and 40. The original vector and the first span are unaffected; the second span is simply a new view into the same elements: middle will output 20 30 40.

Because a span does not own its elements, the lifetime of the sequence it points to must exceed the lifetime of any span referring to it. The following snippet illustrates what can go wrong if that is violated:

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

std::span<int> makeSpan()
{
    std::vector v{ 1, 2, 3 };
    return std::span{ v };
}

int main()
{
    // Initialize s with the value returned by makeSpan
    std::span s{ makeSpan() };
    std::cout << s[0] << '\n';

    return 0;
}

In makeSpan, a local vector is created, and a span is generated that refers to its elements. When the function returns, the vector ceases to exist, releasing its memory. The span still exists, but its pointer and length refer to memory that is no longer valid.

Dereferencing s[0] leads to undefined behavior; the program might print a random value, crash, or coincidentally appear correct.

It is crucial to ensure that a span never outlives the data it refers to.

Span can also be initialized from C style arrays or from std::array, both resulting in the same kind of small, non owning view:

#include <array>
#include <iostream>
#include <span>

int main()
{
    // Keep the array size as part of the type
    std::array a{ 5, 15, 25, 35 };
    std::span s{ a };

    std::cout << s.size() << '\n';
    std::cout << s.front() << '\n';
    std::cout << s.back() << '\n';

    return 0;
}

s.size() tells us that s refers to 4 elements. s.front() accesses the first element, and s.back() accesses the last. Both are normal element accesses, creating neither a copy nor any new allocation. These same member functions are available if the span was created from a vector or a subspan.

By taking a function argument that is a std::span, we can pass it a vector, array, or another span, and the function can treat them all uniformly:

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

void printAll(std::span<int> s)
{
    for (int n : s)
        std::cout << n << ' ';
    std::cout << '\n';
}

int main()
{
    // Use dynamic storage for a resizable sequence
    std::vector v{ 7, 14, 21 };
    printAll(v);

    return 0;
}

The parameter s to printAll now refers to the three elements of v. The span is used in a range based for loop just as you would use any other span. No copy of v is made; only the small span object is passed. If we had instead taken std::vector<int> v by value, a full copy of the vector would have been made, a cost that std::span completely avoids while still providing uniform element access.

A span can also be empty; construct a span by calling default construction or by supplying an empty container for the default construction of a span, and s.size() will be zero, and s.begin() == s.end(). A range based for loop simply will not execute, as is correct.

Exercise: Introduction to std::spanStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Introduction to std::span

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Add a printAll call using a subspan beginning at index 1. Predict the selected values and explain the lifetime of the non-owning view.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

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

void printAll(std::span<int> s)
{
    for (int n : s)
        std::cout << n << ' ';
    std::cout << '\n';
}

int main()
{
    // Use dynamic storage for a resizable sequence
    std::vector v{ 7, 14, 21 };
    printAll(v);

    return 0;
}

Hints

Hint 1

The span parameter views the elements owned by v and carries their count. Passing v does not copy those elements into a new owning container.

Hint 2

Construct a span over v, then use subspan(1) for the additional call. It starts at the second element but still borrows storage from the same live vector.

Worked solution

The additional call passes a subspan beginning at index 1. It views only 14 and 21 without allocating or copying elements. v remains alive during both calls, so both spans have valid backing storage.

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

void printAll(std::span<int> s)
{
    for (int n : s)
        std::cout << n << ' ';
    std::cout << '\n';
}

int main()
{
    // Use dynamic storage for a resizable sequence
    std::vector v{ 7, 14, 21 };
    printAll(v);

    printAll(std::span<int>{v}.subspan(1));
    return 0;
}

Example output

7 14 21 
14 21 

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.