Skip to content

Instantly share code, notes, and snippets.

@olibartfast
Last active January 20, 2026 09:19
Show Gist options
  • Select an option

  • Save olibartfast/7bcb16a2c994ee718223e0a58ca809d0 to your computer and use it in GitHub Desktop.

Select an option

Save olibartfast/7bcb16a2c994ee718223e0a58ca809d0 to your computer and use it in GitHub Desktop.
std::span in Modern C++

std::span in Modern C++

A comprehensive guide to std::span introduced in C++20.

What is std::span?

std::span is a non-owning view over a contiguous sequence of objects. It provides a safe, lightweight way to reference arrays or array-like data structures without taking ownership.

Key Characteristics

  • Non-owning: Doesn't manage lifetime of the data
  • Lightweight: Just a pointer + size (typically 16 bytes)
  • Contiguous memory only: Works with arrays, vectors, std::array
  • Type-safe: Better than raw pointers
  • Header: <span>

Basic Usage

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

void process(std::span<int> data) {
    for (int& val : data) {
        val *= 2;  // Modify elements
    }
}

int main() {
    // Works with C-style arrays
    int arr[] = {1, 2, 3, 4, 5};
    process(arr);
    
    // Works with std::vector
    std::vector<int> vec = {1, 2, 3, 4, 5};
    process(vec);
    
    // Works with std::array
    std::array<int, 5> stdArr = {1, 2, 3, 4, 5};
    process(stdArr);
    
    // Works with pointer + size
    int* ptr = new int[5]{1, 2, 3, 4, 5};
    process(std::span<int>(ptr, 5));
    delete[] ptr;
}

Creating Spans

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

// From container (deduced)
std::span s1(data);

// Explicit type
std::span<int> s2(data);

// From pointer + size
std::span<int> s3(data.data(), data.size());

// From iterators
std::span<int> s4(data.begin(), data.end());

// Fixed-size span
std::span<int, 5> s5(data);  // Compile-time size check

Member Functions

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

// Size and capacity
s.size();           // 6
s.size_bytes();     // 6 * sizeof(int) = 24
s.empty();          // false

// Element access
s.front();          // 1
s.back();           // 6
s[2];              // 3
s.data();          // pointer to first element

// Iterators
auto it = s.begin();
auto end = s.end();
for (auto val : s) { /* ... */ }

Subspans

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

// First N elements
auto first3 = s.first(3);           // {1, 2, 3}
auto first5 = s.first<5>();         // {1, 2, 3, 4, 5} - compile-time

// Last N elements
auto last3 = s.last(3);             // {6, 7, 8}
auto last4 = s.last<4>();           // {5, 6, 7, 8} - compile-time

// Subspan(offset, count)
auto middle = s.subspan(2, 3);      // {3, 4, 5}
auto fromIndex2 = s.subspan(2);     // {3, 4, 5, 6, 7, 8}
auto compile = s.subspan<2, 3>();   // {3, 4, 5} - compile-time

Const Correctness

void read_only(std::span<const int> data) {
    for (const int& val : data) {
        std::cout << val << ' ';
    }
    // data[0] = 10;  // ❌ Compile error
}

void read_write(std::span<int> data) {
    data[0] = 10;  // ✅ OK
}

std::vector<int> vec = {1, 2, 3};
read_only(vec);   // ✅ Implicit conversion to const span
read_write(vec);  // ✅ Mutable access

Dynamic vs Static Extent

// Dynamic extent (default)
std::span<int> dynamic_span;  // Size known at runtime

// Static extent
std::span<int, 5> static_span;  // Size known at compile-time

// std::dynamic_extent
std::span<int, std::dynamic_extent> same_as_dynamic;

Advantages Over Alternatives

vs. Pointer + Size

// ❌ Old way - error prone
void process(int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        data[i] *= 2;
    }
}

// ✅ Modern way - safe and clean
void process(std::span<int> data) {
    for (int& val : data) {
        val *= 2;
    }
}

vs. const std::vector&

// ❌ Forces specific container type
void process(const std::vector<int>& data);

// ✅ Works with any contiguous container
void process(std::span<const int> data);

vs. Templates

// ❌ Code bloat (instantiated for each container type)
template<typename Container>
void process(const Container& data);

// ✅ Single function, no code bloat
void process(std::span<const int> data);

Common Use Cases

Function Parameters

// Accept any contiguous sequence
double average(std::span<const double> values) {
    if (values.empty()) return 0.0;
    
    double sum = 0.0;
    for (double val : values) {
        sum += val;
    }
    return sum / values.size();
}

// Usage
std::vector<double> vec = {1.0, 2.0, 3.0};
std::array<double, 3> arr = {1.0, 2.0, 3.0};
double c_arr[] = {1.0, 2.0, 3.0};

average(vec);    //
average(arr);    //
average(c_arr);  //

Working with Subarrays

void process_chunk(std::span<int> chunk) {
    // Process a portion of data
}

std::vector<int> large_data(1000);
process_chunk(std::span(large_data).subspan(100, 50));

Multi-dimensional Views

template<typename T>
class Matrix2D {
    std::span<T> data_;
    size_t rows_, cols_;
    
public:
    Matrix2D(std::span<T> data, size_t rows, size_t cols)
        : data_(data), rows_(rows), cols_(cols) {}
    
    T& operator()(size_t row, size_t col) {
        return data_[row * cols_ + col];
    }
    
    std::span<T> get_row(size_t row) {
        return data_.subspan(row * cols_, cols_);
    }
};

Important Caveats

Dangling References

std::span<int> dangerous() {
    std::vector<int> temp = {1, 2, 3};
    return temp;  // ⚠️ DANGER! temp is destroyed
}
// Using the returned span leads to undefined behavior

Non-Contiguous Containers

std::list<int> list = {1, 2, 3};
// std::span<int> s(list);  // ❌ Won't compile
// std::list is not contiguous

Comparison with Other Views

Feature std::span std::string_view Raw pointer
Ownership Non-owning Non-owning Non-owning
Modifiable Yes (if not const) No Yes
Bounds checking .at() available .at() available No
Type-safe Yes Yes No
Works with Any contiguous Strings only Any

Practical Example

#include <span>
#include <vector>
#include <numeric>
#include <algorithm>

class DataProcessor {
public:
    // Process any contiguous integer sequence
    static void normalize(std::span<int> data) {
        if (data.empty()) return;
        
        int max_val = *std::max_element(data.begin(), data.end());
        if (max_val == 0) return;
        
        for (int& val : data) {
            val = (val * 100) / max_val;
        }
    }
    
    static int sum(std::span<const int> data) {
        return std::accumulate(data.begin(), data.end(), 0);
    }
};

int main() {
    std::vector<int> vec = {10, 20, 30, 40, 50};
    
    DataProcessor::normalize(vec);
    int total = DataProcessor::sum(vec);
    
    // Works with subarrays too
    auto first_half = std::span(vec).first(vec.size() / 2);
    DataProcessor::normalize(first_half);
}

Summary

std::span is the modern C++ way to pass arrays to functions. It provides:

  • Type safety without runtime overhead
  • Flexibility to work with any contiguous container
  • Clean, readable code
  • No unnecessary copies or template bloat

Use std::span<T> for mutable access and std::span<const T> for read-only access.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment