std::span in C++20: More Details

A std::span represents an object that refers to a contiguous sequence of objects. Today, I want to write about its not-so-obvious features.

A std::span, sometimes also called a view, is never an owner. This contiguous memory can be a plain array, a pointer with a size, a std::array, a std::vector, or a std::string. A typical implementation consists of a pointer to its first element and a size. The main reason for having a std::span<T> is that a plain array will decay to a pointer if passed to a function; therefore, the size is lost. This decay is a typical reason for errors in C/C++.

Automatically deduces the size of a contiguous sequence of objects

In contrast, std::span<T> automatically deduces the size of contiguous sequences of objects.

// printSpan.cpp

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

void printMe(std::span<int> container) {
    
    std::cout << "container.size(): " << container.size() << '\n';  // (4)
    for(auto e : container) std::cout << e << ' ';
    std::cout << "\n\n";
}

int main() {
    
    std::cout << std::endl;
    
    int arr[]{1, 2, 3, 4};              // (1)
    printMe(arr);
    
    std::vector vec{1, 2, 3, 4, 5};     // (2)
    printMe(vec);

    std::array arr2{1, 2, 3, 4, 5, 6}; // (3)
    printMe(arr2);
    
}

The C-array (1), std::vector (2), and the std::array (3) have int‘s. Consequently, std::span also holds int’s. There is something more interesting in this simple example. For each container, std::span can deduce its size (4).

This was a short reminder about std::span. For the full story, read my previous post “std::span in C++20: Bounds-Safe Views of Sequences of Objects“.

A std::span can have a static extent or a dynamic extent.

Static versus Dynamic Extent

By default, std::span has a dynamic extent:

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

When a std::span has a static extent, its size is known at compile time and part of the type: std::span. Consequently, its implementation needs only a pointer to the first element of the contiguous sequence of objects.

Implementing a std::span with a dynamic extent consists of a pointer to the first element and the size of the contiguous sequence of objects. The size is not part of the std::span type.

The next example emphasizes the differences between the two kinds of spans.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Embedded Programming with Modern C++": January 2025
  • "Generic Programming (Templates) with C++": February 2025
  • "Clean Code: Best Practices for Modern C++": May 2025
  • Do you want to stay informed: Subscribe.

     

    // staticDynamicExtentSpan.cpp
    
    #include <iostream>
    #include <span>
    #include <vector>
    
    void printMe(std::span<int> container) {        // (3)  
        
        std::cout << "container.size(): " << container.size() << '\n';
        for (auto e : container) std::cout << e << ' ';
        std::cout << "\n\n";
    }
    
    int main() {
    
        std::cout << '\n';
    
        std::vector myVec1{1, 2, 3, 4, 5};        
        std::vector myVec2{6, 7, 8, 9};
    
        std::span<int> dynamicSpan(myVec1);          // (1)
        std::span<int, 4> staticSpan(myVec2);        // (2)
    
        printMe(dynamicSpan);
        printMe(staticSpan);
    
        // staticSpan = dynamicSpan;    ERROR        // (4)
        dynamicSpan = staticSpan;                    // (5) 
    
        printMe(staticSpan);                         // (6)
    
        std::cout << '\n';
        
    }
    

    dynamicSpan (line 1) has a dynamic extent, while staticSpan (line 2) has a static extent. Both std::spans return their size in the printMe function (line 3). A std::span with static extent can be assigned to a std::span with dynamic extent, but not vice versa. Line 4 would cause an error, but lines 5, and 6 are valid.

    There is one particular use case of std::span. A std::span can be a constant range of modifiable elements.

    A Constant Range of Modifiable Elements

    For simplicity, I name a std::vector and a std::span range. A std::vector models a modifiable range of modifiable elements: std::vector. When you declare this std::vector as const, it models a constant range of constant objects: const std::vector. You cannot model a constant range of modifiable elements. This is where std::span comes into play. A std::span models a constant range of modifiable objects: std::span. The following image emphasizes the variations of (constant/modifiable) ranges and (constant/modifiable) elements.

    // constRangeModifiableElements.cpp
    
    #include <iostream>
    #include <span>
    #include <vector>
    
    void printMe(std::span<int> container) {
        
        std::cout << "container.size(): " << container.size() << '\n';  
        for (auto e : container) std::cout << e << ' ';
        std::cout << "\n\n";
    }
    
    int main() {
    
        std::cout << '\n';
    
        std::vector<int> origVec{1, 2, 2, 4, 5};
    
        // Modifiable range of modifiable elements
        std::vector<int> dynamVec = origVec;           // (1)
        dynamVec[2] = 3;
        dynamVec.push_back(6);
        printMe(dynamVec);
    
        // Constant range of constant elements
        const std::vector<int> constVec = origVec;     // (2)
        // constVec[2] = 3;        ERROR
        // constVec.push_back(6);  ERROR
        std::span<const int> constSpan(origVec);       // (3)
        // constSpan[2] = 3;       ERROR
    
        // Constant range of modifiable elements
        std::span<int> dynamSpan{origVec};             // (4)
        dynamSpan[2] = 3;
        printMe(dynamSpan);
    
        std::cout << '\n';
    
    }
    

    The vector dynamVec (line 1) is a modifiable range of modifiable elements. This observation does not hold for the vector constVec (line 2). Neither can constVec change its elements nor its size. constSpan (line 3) behaves accordingly. dynamSpan (line 4) models the unique use case of a constant range of modifiable elements.

    Finally, I want to mention two dangers you should know when using std::span.

    Dangers of std::span

    The typical issues of std::span are twofold. First, a std::span should not act on a temporary and second, the size of the underlying contiguous range of a std::span should not be modified.

    A std::span on a Temporary

    A std::span is never an owner. Therefore, a std::span does not extend the lifetime of its underlying data. Consequently, a std::span should only operate on an lvalue. Using std::span on a temporary range is undefined behavior.

    // temporarySpan.cpp
    
    #include <iostream>
    #include <span>
    #include <vector>
    
    std::vector<int> getVector() {                          // (2)
        return {1, 2, 3, 4, 5};
    }
    
    int main() {
    
         std::cout << '\n';
        
        std::vector<int> myVec{1, 2, 3, 4, 5};              // (1)
        std::span<int, 5> mySpan1{myVec};                  
        std::span<int, 5> mySpan2{getVector().begin(), 5};  // (3)
    
        for (auto v: std::span{myVec}) std::cout << v << " ";
        std::cout << '\n';
        for (auto v: std::span{getVector().begin(), 5}) std::cout << v << " ";  // (4)
    
         std::cout << "\n\n";
        
    }
    

    Using a std::span with a static extent or a std::span with a dynamic extent on the lvalue is fine. When I switch from the lvalue std::vector in line 1 to a temporary std::vector, given by the function getVector (lines 2), the program has undefined behavior. Both lines 3 and 4 are not valid. Consequently, executing the program exposes the undefined behavior. The output of line 4 does not match with the std::vector, generated by the function getVector().

    Changing the Size of the Underlying Contiguous Range

    When you change the size of the underlying contiguous range, the contiguous range may be reallocated, and the std::span refers to stale data.

    std::vector<int> myVec{1, 2, 3, 4, 5};
    
    std::span<int> sp1{myVec};
    
    myVec.push_back(6);  // undefined behavior
    

    What’s Next?

    In my next post, I dive once more in the formatting library of C++20.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,