C++ std::span | Contiguous Memory View (C++20)

C++ std::span | Contiguous Memory View (C++20)

이 글의 핵심

Practical guide to std::span: non-owning contiguous views, APIs, and lifetime rules.

What is span?

std::span is a lightweight view of a contiguous memory region introduced in C++20. It provides a safe, unified interface by providing a size and pointer to an array, vector, or contiguous memory.

#include <span>

void process(std::span<int> data) {
    for (int x : data) {
        std::cout << x << " ";
    }
}

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

process(arr);  // arrangement
process(vec);  // vector

Why do you need it?:

  • Unified Interface: Treats arrays, vectors, and C arrays as one type.
  • Safety: Boundary check possible including size information
  • Performance: No copy, just reference (pointer + size)
  • Conciseness: No need to pass pointer and size separately
// ❌ Traditional method: pointer + size (inconvenient, possible error)
void process(int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        std::cout << data[i] << " ";
    }
}

int arr[] = {1, 2, 3};
process(arr, 3);  // Pass the size manually

// ✅ span: integrated and secure
void process(std::span<int> data) {
    for (int x : data) {  // range based for available
        std::cout << x << " ";
    }
}

process(arr);  // Size automatic inference

Characteristics of span:

  • Non-owning: Does not own the memory, only references it
  • Lightweight: Stores only pointer and size (usually 16 bytes)
  • Copyable: Copying costs are very low
  • View: Original data can be modified (const span is read-only)
std::vector<int> vec = {1, 2, 3, 4, 5};

std::span<int> sp{vec};
sp[0] = 10;  // Edit original vec

std::cout << vec[0];  // 10 (edited)

Structure of span:

// Conceptual Implementation
template<typename T>
class span {
    T* data_;
    size_t size_;
    
public:
    span(T* data, size_t size) : data_(data), size_(size) {}
    
    size_t size() const { return size_; }
    T* data() const { return data_; }
    T& operator const { return data_[index]; }
    
    // iterator
    T* begin() const { return data_; }
    T* end() const { return data_ + size_; }
};

Default use

#include <span>
#include <vector>

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

// full span
std::span<int> sp1{v};

// partial span
std::span<int> sp2{v.data() + 1, 3};  // {2, 3, 4}

// size
std::cout << sp1.size() << std::endl;  // 5

Practical example

Example 1: Function parameters

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

// unified interface
void printData(std::span<const int> data) {
    for (int x : data) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3};
    std::vector<int> vec = {4, 5, 6};
    std::array<int, 3> stdArr = {7, 8, 9};
    
    printData(arr);     // 1 2 3
    printData(vec);     // 4 5 6
    printData(stdArr);  // 7 8 9
}

Example 2: Subrange

#include <span>

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// first 5
std::span<int> first5{v.data(), 5};

// last 5
std::span<int> last5{v.data() + 5, 5};

// subspan
std::span<int> sp{v};
auto middle = sp.subspan(3, 4);  // {4, 5, 6, 7}

Example 3: Bounds checking

#include <span>

void safeAccess(std::span<int> data, size_t index) {
    // boundary check
    if (index < data.size()) {
        std::cout << data[index] << std::endl;
    } else {
std::cout << "out of range" << std::endl;
    }
}

int main() {
    std::vector<int> v = {1, 2, 3};
    safeAccess(v, 1);   // 2
    safeAccess(v, 10);  // Out of range
}

Example 4: 2D array

#include <span>

void processMatrix(std::span<int> data, size_t rows, size_t cols) {
    for (size_t i = 0; i < rows; ++i) {
        for (size_t j = 0; j < cols; ++j) {
            std::cout << data[i * cols + j] << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    std::vector<int> matrix = {
        1, 2, 3,
        4, 5, 6,
        7, 8, 9
    };
    
    processMatrix(matrix, 3, 3);
}

span operation

std::span<int> sp{v};

// size
auto size = sp.size();
auto bytes = sp.size_bytes();

// access
int first = sp.front();
int last = sp.back();
int* ptr = sp.data();

// partial range
auto sub1 = sp.first(3);      // first 3
auto sub2 = sp.last(3);       // last 3
auto sub3 = sp.subspan(2, 3); // [2] to 3

Frequently occurring problems

Problem 1: Lifespan

// ❌ Dangling
std::span<int> getDanglingSpan() {
    std::vector<int> v = {1, 2, 3};
    return std::span{v};
    // v extinction
}

// ✅ Reference disambiguation
std::span<int> getSpan(std::vector<int>& v) {
    return std::span{v};
}

Problem 2: const

std::vector<int> v = {1, 2, 3};

// read only
std::span<const int> sp{v};

// ❌ No editing possible
// sp[0] = 10;  // error

// ✅ Modifiable
std::span<int> sp2{v};
sp2[0] = 10;

Issue 3: Size

// fixed size span
std::span<int, 3> fixedSpan{arr};

// dynamic size span
std::span<int> dynamicSpan{vec};

// ❌ Size mismatch
// std::span<int, 5> sp{arr};  // error if arr size is 3

Problem 4: Pointer conversion

int* ptr = getData();
size_t size = getSize();

// ✅ Wrapping with spans
std::span<int> sp{ptr, size};

// safe access
for (int x : sp) {
    std::cout << x << " ";
}

span vs pointer

// ❌ Pointer (no size information)
void process(int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        std::cout << data[i] << " ";
    }
}

// ✅ span (including size)
void process(std::span<int> data) {
    for (int x : data) {
        std::cout << x << " ";
    }
}

Practice pattern

Pattern 1: Read-only view

class DataProcessor {
public:
    // const span: read-only
    double calculateAverage(std::span<const double> data) const {
        if (data.empty()) return 0.0;
        
        double sum = 0.0;
        for (double value : data) {
            sum += value;
        }
        return sum / data.size();
    }
};

// use
std::vector<double> values = {1.0, 2.0, 3.0, 4.0, 5.0};
DataProcessor processor;
double avg = processor.calculateAverage(values);

Pattern 2: Sliding Window

template<typename T>
void processWindows(std::span<T> data, size_t windowSize) {
    if (data.size() < windowSize) return;
    
    for (size_t i = 0; i <= data.size() - windowSize; ++i) {
        auto window = data.subspan(i, windowSize);
        
        // window processing
        std::cout << "Window [" << i << "]: ";
        for (const auto& value : window) {
            std::cout << value << " ";
        }
        std::cout << '\n';
    }
}

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

Pattern 3: Buffer Wrapper

class Buffer {
    std::vector<uint8_t> data_;
    
public:
    Buffer(size_t size) : data_(size) {}
    
    // Full buffer view
    std::span<uint8_t> asSpan() {
        return std::span{data_};
    }
    
    // read-only view
    std::span<const uint8_t> asSpan() const {
        return std::span{data_};
    }
    
    // partial view
    std::span<uint8_t> slice(size_t offset, size_t length) {
        return std::span{data_}.subspan(offset, length);
    }
};

// use
Buffer buffer(1024);
auto view = buffer.asSpan();
view[0] = 0xFF;

FAQ

Q1: What is span?

A: A lightweight view of a contiguous memory region in C++20. Pointers and sizes are provided together to provide a secure and unified interface.

std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp{vec};  // pointer + size

std::cout << sp.size() << '\n';  // 5
std::cout << sp[0] << '\n';      // 1

Q2: Does span copy data?

A: No. A span is a non-owning view, meaning it only references the original data. Copy cost is very low (only copy pointer + size).

std::vector<int> vec(1000000);  // big vector

// Create span: no copy (pointer + size only)
std::span<int> sp{vec};

// span copy: very fast (only copies 16 bytes)
std::span<int> sp2 = sp;

Q3: Where is span used?

A:

  • Function parameters: Integrate arrays, vectors, and C arrays
  • Partial range: slicing, windowing
  • Safe Pointer: Boundary check possible with size information included
// unified interface
void process(std::span<int> data) {
    // Array, vector, std::array are all possible
}

int arr[] = {1, 2, 3};
std::vector<int> vec = {4, 5, 6};
std::array<int, 3> stdArr = {7, 8, 9};

process(arr);
process(vec);
process(stdArr);

Q4: Is the span size fixed?

A: Supports both dynamic size and fixed size.

// Dynamic size (default)
std::span<int> dynamicSpan{vec};

// Fixed size (compile time)
std::span<int, 3> fixedSpan{arr};

// Fixed size is verified at compile time
// std::span<int, 5> wrongSpan{arr};  // error: size mismatch

Q5: How do you manage the lifespan of a span?

A: span is a non-owning view, so you need to be careful about the lifetime of the original data.

// ❌ Dangling span
std::span<int> getDanglingSpan() {
    std::vector<int> vec = {1, 2, 3};
    return std::span{vec};  // vec disappears!
}

// ✅ Safe to use
std::span<int> getSpan(std::vector<int>& vec) {
    return std::span{vec};  // vec is owned by the caller
}

Q6: What is the difference between const span and span?

A:

  • const std::span<int>: span itself is const (cannot point to other memory)
  • std::span<const int>: The data pointed to is const (data cannot be modified)
std::vector<int> vec = {1, 2, 3};

// span<const int>: data read only
std::span<const int> sp1{vec};
// sp1[0] = 10;  // Error: Data cannot be modified
sp1 = std::span<const int>{};  // OK: span itself can be modified

// const span<int>: span itself is const
const std::span<int> sp2{vec};
sp2[0] = 10;  // OK: Data can be modified
// sp2 = std::span<int>{};  // Error: span itself cannot be modified

Q7: Can span be converted to a pointer?

A: It is possible. You can get a pointer with the data() method.

std::span<int> sp{vec};

// Get pointers
int* ptr = sp.data();

// Legacy API calls
legacyFunction(ptr, sp.size());

Q8: What are span learning resources?

A:

Related posts: string_view, array, vector.

One-Line Summary: std::span is a lightweight, non-owned view of a contiguous region of memory, providing a safe, unified interface.


Good article to read together (internal link)

Here’s another article related to this topic.

  • C++ span | “Array View” C++20 Guide
  • C++ Aggregate Initialization | “Aggregate initialization” guide
  • C++ subrange | “Subrange” guide

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intent of the code clear?
  • Are there enough test cases?
  • Is it documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, span, view, array, C++20, etc.


  • See the entire C++ series
  • C++ Adapter Pattern Complete Guide | Interface conversion and compatibility
  • C++ ADL |
  • C++ Aggregate Initialization |