A comprehensive guide to std::span introduced in C++20.
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.
- 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>
#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;
}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 checkstd::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) { /* ... */ }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-timevoid 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 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;// ❌ 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;
}
}// ❌ Forces specific container type
void process(const std::vector<int>& data);
// ✅ Works with any contiguous container
void process(std::span<const int> data);// ❌ 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);// 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); // ✅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));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_);
}
};std::span<int> dangerous() {
std::vector<int> temp = {1, 2, 3};
return temp; // ⚠️ DANGER! temp is destroyed
}
// Using the returned span leads to undefined behaviorstd::list<int> list = {1, 2, 3};
// std::span<int> s(list); // ❌ Won't compile
// std::list is not contiguous| 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 |
#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);
}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.