std::array, C arrays and std::span
A std::vector is the right default for a sequence, and it is not the only one. When the size is fixed and known at compile time, a C array or a std::array keeps the elements inside the object, with no allocation and no pointer. When a function needs to read a range of elements it does not own, std::span describes it without choosing the container.
The three types sit next to each other in function signatures: a C array decays to a pointer, std::array is a complete value type, and std::span is a pointer and a length. This lesson uses each one where it fits and ends with the const mismatch a span makes easy to write.
A C array on the stack
A C array holds a fixed number of elements inside the enclosing object. The size is part of the type, the elements are contiguous, and the array does not know how long it is at run time.
sizeof(values) is the total size in bytes and sizeof(values[0]) is the size of one element, so their quotient is the element count. That trick works only where the array has not decayed to a pointer, which is why it lives in the function that declares it.
A range-for loop walks the elements without an index, and the compiler knows the bounds. Passing the array to a function turns it into a pointer to the first element and loses the size, which is the problem std::span solves in the third section.
#include <iostream>
int main()
{
int values[4]{3, 1, 4, 1};
int total = 0;
for (int value : values) {
total += value;
}
std::cout << "count: " << sizeof(values) / sizeof(values[0]) << '\n';
std::cout << "total: " << total << '\n';
}
g++ -std=c++23 -Wall -Wextra c_array.cpp -o c_array
./c_array
count: 4
total: 9
std::array is a value
std::array<int, 4> is a struct that holds a C array and adds size(), begin(), end() and the rest of the container interface. The size is a template argument, so it is part of the type and costs nothing at run time.
Unlike a C array, std::array copies like any other value: the assignment below creates a second array with its own elements, and changing one leaves the other alone. A C array cannot be copied or assigned at all.
size() returns the element count, so the sizeof division disappears from the code. The elements are still on the stack when the array is a local, and the type works as a member of a class or an element of a vector.
#include <array>
#include <iostream>
int main()
{
std::array<int, 4> values{3, 1, 4, 1};
std::array<int, 4> copy = values;
copy[0] = 100;
std::cout << values[0] << ' ' << copy[0] << ' ' << values.size() << '\n';
}
g++ -std=c++23 -Wall -Wextra std_array.cpp -o std_array
./std_array
3 100 4
std::span is a window
std::span<const int> is a pointer to the first element plus a length. It does not own the elements and does not allocate; it describes a contiguous range that the caller keeps alive for the duration of the call.
A span is constructed from a C array, from a std::array and from a std::vector, so one function accepts all three. That is why it replaced the pattern of passing a pointer and a count as two parameters.
The program sums a C array of three and a std::array of three with the same function. The parameter is const, so the function can read the elements and cannot change them.
#include <array>
#include <iostream>
#include <span>
int sum(std::span<const int> values)
{
int total = 0;
for (int value : values) {
total += value;
}
return total;
}
int main()
{
int raw[3]{1, 2, 3};
std::array<int, 3> fixed{4, 5, 6};
std::cout << sum(raw) << ' ' << sum(fixed) << '\n';
}
g++ -std=c++23 -Wall -Wextra span.cpp -o span_demo
./span_demo
6 15
A span can write through
Drop the const and a span<int> lets the function change the caller's elements. The function itself owns nothing, but the elements it points at belong to the caller, so the changes are visible after the call.
The range-for uses int& so that the loop variable is a reference to each element rather than a copy. The same rule as lesson 021 will state for auto: without the & the loop would double copies and print the original values.
This is the useful half of a span: a function that fills a caller-provided buffer, or transforms a sequence in place, without allocating and without knowing whether the buffer is a C array, a std::array or a vector.
#include <iostream>
#include <span>
void double_all(std::span<int> values)
{
for (int& value : values) {
value *= 2;
}
}
int main()
{
int values[3]{1, 2, 3};
double_all(values);
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra span_write.cpp -o span_write
./span_write
2 4 6
A const array is not a writable span
A span<int> promises to write through the pointer, so it cannot be built from an array whose elements are const. The compiler refuses the conversion and names the two types.
The program below sums a const std::array with a function that takes std::span<int>, and the call does not compile. The fix is to take std::span<const int>, which is the same function with a weaker promise.
This is the const correctness of lesson 012 applied to a view: a read-only function should say so in its parameter, and only then will it accept const data.
#include <array>
#include <span>
int sum(std::span<int> values)
{
int total = 0;
for (int value : values) {
total += value;
}
return total;
}
int main()
{
const std::array<int, 3> values{1, 2, 3};
return sum(values);
}
g++ -std=c++23 -Wall -Wextra const_span.cpp -o const_span
const_span.cpp: In function 'int main()':
const_span.cpp:16:16: error: could not convert 'values' from 'const std::array<int, 3>' to 'std::span<int>'
16 | return sum(values);
| ^~~~~~
| |
| const std::array<int, 3>
Exercises
-
Write a function that takes std::span
and returns the largest element, then call it with a C array and with a vector. - Change the parameter of sum in const_span.cpp to std::span
and compile. Which call becomes valid? - Copy a std::array, change one element and print both. Then try the same with a C array and explain the compiler error.
- Change the parameter of sum in const_span.cpp to std::span
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Where do the elements of a C array live?
Answer
Inside the array object, with no separate allocation
The array is the elements, which is why copying it copies them and why its size is fixed.
-
What does
std::arrayadd to a C array?Answer
Value semantics, a size member and the container interface
The class wraps the array and can be copied, compared and passed by value.
-
What does a
std::spandescribe?Answer
A range of elements it does not own
The span is a pointer and a length over storage that belongs to someone else.
-
Can a span write through to the elements?
Answer
Yes, when its element type is not const
The element type of the span decides whether the elements can be modified.
-
What does the C-array program print?
Answer
count: 4 and total: 9
The array has four elements, and the loop adds 2, 3 and 4 with the first one.
-
What does the std::array program print?
Answer
3 100 4
The program prints the size, a modified element and the element count it later computes.
-
What does this program print?
#include <iostream> #include <span> #include <vector> int sum(std::span<const int> values) { int total = 0; for (int v : values) total += v; return total; } int main() { int raw[]{1, 2, 3}; std::vector<int> values{4, 5, 6, 0}; std::cout << sum(raw) << ' ' << sum(values); }Answer
6 15
The same function sums the three-element array and the four-element vector, giving 6 and 15.
-
What does this program print?
#include <iostream> #include <span> void twice(std::span<int> values) { for (int& v : values) v *= 2; } int main() { int values[]{1, 2, 3}; twice(values); bool first = true; for (int v : values) { std::cout << (first ? "" : " ") << v; first = false; } }Answer
2 4 6
The function doubles each element through the span, and the caller sees the new values.
-
What does calling
sum(values)on aconst std::array<int, 3>report?Answer
could not convert 'values' from a const std::array to a std::span<int>
A writable span needs writable storage, and the const array cannot provide it.
-
Why is a span parameter better than a pointer and a count?
Answer
The length travels with the address, so they cannot disagree
One object carries both facts, and the function cannot be called with a length that does not match.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev