- The one-minute version
- C-style arrays
std::arraystd::span(C++20)- Robotics scenarios
- Tricky questions and common mistakes
- Reference
| Type | Owns memory? | Knows its size? | Size fixed at... | When to use |
|---|---|---|---|---|
int arr[N] |
Yes (stack) | Sometimes (decays!) | Compile time | Legacy code, embedded, talking to a C API |
std::array<T, N> |
Yes (stack) | Yes (constexpr) | Compile time | Fixed-size buffer with a type-safe size (CAN frame, IMU) |
std::vector<T> |
Yes (heap) | Yes | Runtime | Variable size, owning |
std::span<T> |
No | Yes | Either | A non-owning view over any contiguous storage |
Rule of thumb: functions that read or write a contiguous range without taking
ownership should accept std::span<T> or std::span<const T>. Storage that lives in
the function or object should be std::array or std::vector.
int joint_targets[6] = { 0, 0, 0, 0, 0, 0 }; // 6 joints, all zero
joint_targets[2] = 45; // OK
// joint_targets[6] = 0; // UB: out of bounds, no diagnosticA C array is a contiguous block of N objects. There's no built-in size method, no
bounds check, and no way to know — once it's been passed to a function — how big it
originally was.
int arr[] = { 1, 2, 3, 4, 5 };
std::cout << sizeof(arr) << '\n'; // 20 (5 * sizeof(int))
constexpr auto n = sizeof(arr) / sizeof(arr[0]); // 5
for (int* p = arr; p != arr + n; ++p) {
std::cout << *p << '\n';
}The moment you pass arr to a function, it decays to int* and forgets its size:
void print(int arr[]) { // identical to: void print(int* arr)
std::cout << sizeof(arr) << '\n'; // 8 on x86-64 — that's sizeof(int*), not the array!
}This is the single most common C-array bug. Fixes, in order of how modern they are:
void print(int* arr, std::size_t n); // pass size explicitly
template <std::size_t N> void print(int (&arr)[N]); // reference-to-array keeps N
void print(std::span<const int> arr); // C++20: just use a spanSee decay.md for the full picture on array-to-pointer decay.
C++ doesn't have true multidimensional arrays — it has arrays of arrays.
int grid[3][5] = {
{ 1, 2, 3, 4, 5 },
{ 6, 7, 8, 9, 10 },
{ 11, 12, 13, 14, 15 },
};When passing one to a function, every dimension except the leftmost is part of the type:
void process(int (&grid)[3][5]); // takes exactly 3x5
void process(int grid[][5], int rows); // any number of rows, columns must be 5For runtime-sized 2-D data, prefer std::vector<std::vector<T>> (simple but two
allocations and not contiguous), a flat std::vector<T> with manual row indexing
(one allocation, cache-friendly), or std::mdspan (C++23). Avoid the
new int*[rows] pattern from older tutorials — it's a manual-memory minefield.
std::array<T, N> is a thin wrapper over T[N] that behaves like a real value
type — it knows its size, can be copied and returned, supports iterators and
algorithms, and never decays to a pointer.
#include <array>
#include <algorithm>
#include <numeric>
std::array<double, 7> joint_pos = {}; // value-initialized to zero
joint_pos.fill(0.0); // explicit fill
auto avg = std::accumulate(joint_pos.begin(), joint_pos.end(), 0.0) / joint_pos.size();
constexpr auto size = joint_pos.size(); // constexpr! known at compile time
auto& first = joint_pos.front();
auto& last = joint_pos.back();
auto& fourth = joint_pos.at(3); // bounds-checked; throws on bad index
auto& fifth = joint_pos[4]; // not bounds-checkedYou can return a std::array from a function (impossible with a C array) and pass it
by value cheaply for small sizes. Initialization deduces nicely with std::to_array
(C++20):
auto frame = std::to_array<uint8_t>({0xDE, 0xAD, 0xBE, 0xEF});
// → std::array<uint8_t, 4>std::span<T> is a non-owning view over a contiguous range. It's a pointer plus
a length — usually a 16-byte struct on 64-bit machines. It accepts a C array, a
std::array, a std::vector, a raw (ptr, size) pair, anything contiguous.
#include <span>
void normalize(std::span<float> v) {
float n = 0.0f;
for (float x : v) n += x * x;
n = std::sqrt(n);
for (float& x : v) x /= n;
}
int main() {
float a[3] = {3, 4, 0};
std::array b = {3.0f, 4.0f, 0.0f};
std::vector c = {3.0f, 4.0f, 0.0f};
normalize(a); // C array → span
normalize(b); // std::array → span
normalize(c); // std::vector → span
normalize({c.data() + 1, 2}); // a sub-range, no copy
}std::span<const T> is read-only — use it for input parameters; std::span<T> for
output/in-out. Pass spans by value, not by reference.
Subviews are cheap:
std::span<const uint8_t> packet = ...;
auto header = packet.first(8); // first 8 bytes
auto payload = packet.subspan(8); // rest
auto crc = packet.last(4); // last 4 bytesSpans also have an optional compile-time extent: std::span<int, 6> is exactly six
elements and the size is constexpr. Most of the time you want the dynamic-extent
form (std::span<T>), which is what the constructors above produce.
CAN classic frame payload. A classic CAN frame has exactly 8 data bytes — use
std::array<uint8_t, 8> because the size is a hard part of the protocol:
struct CanFrame {
uint32_t id;
uint8_t dlc; // 0..8
std::array<uint8_t, 8> data;
};
void send(const CanFrame& f) {
write_to_socket(f.id, std::span(f.data).first(f.dlc)); // only DLC bytes
}Sliding window of IMU samples. A fixed-capacity ring buffer that lives on the stack:
constexpr std::size_t WINDOW = 100;
std::array<ImuSample, WINDOW> imu_window{};Lidar scan filtering. The filter shouldn't care whether the scan came from a ROS
message, a recorded .bag, or a simulator — accept any contiguous range:
void downsample(std::span<const float> ranges,
std::span<float> out);Image rows. Iterating over rows of a frame buffer without copying:
for (int y = 0; y < height; ++y) {
std::span<uint8_t> row{frame.data() + y * stride, width};
sobel(row);
}1. "Why is sizeof(arr) wrong inside my function?"
Because the array decayed to a pointer at the call site. sizeof(arr) inside the
function gives the size of a pointer (usually 8 bytes), not the array. Fix: pass a
std::span, or use a T (&)[N] reference-to-array parameter.
2. Returning a span to a local.
std::span<int> bad() {
int local[4] = {1, 2, 3, 4};
return local; // DANGLING — local dies at return
}std::span owns nothing. The same trap exists with temporaries:
std::span<const int> view = std::vector{1, 2, 3}; // DANGLINGThe vector is destroyed at the end of the full expression. std::span does
not extend its lifetime (unlike a const T& to a single object).
3. sizeof on a char* vs a string literal.
const char s1[] = "robot"; // array of 6 chars (incl. '\0')
const char* s2 = "robot"; // pointer
sizeof(s1); // 6
sizeof(s2); // 8 (size of pointer on x86-64)
strlen(s1); // 5 in both cases4. The classic int index bug.
for (int i = 0; i < sizeof(arr); ++i) { ... } // BUGsizeof(arr) is bytes, not elements. Use std::size(arr) (C++17) or a range-for.
5. std::array<T, 0>. Legal and useful in generic code — it has zero elements,
empty() returns true, but front()/back() are undefined behavior. Don't call
them without checking.
6. std::span<int> vs std::span<const int> for a const argument.
void read(const std::span<int> s); // `s` itself is const, but the elements are mutable
void read(std::span<const int> s); // the elements are read-only — what you usually wantFor an input parameter, std::span<const T> is almost always the right choice.
7. Spans don't own — and don't deep-copy. Copying a span copies the pointer-and-length pair. Two spans aliasing the same buffer are fine for reads, a data race for writes from different threads.
8. Initializer-list pitfall.
void foo(std::span<const int> s);
foo({1, 2, 3}); // OK — braced init constructs a temporary array... that dies at ;Works as a function argument because the temporary lives until the end of the call expression. Storing the span across statements is a bug; see point 2.
9. Iterating with the wrong index type.
std::array<int, 5> a{};
for (int i = 0; i < a.size(); ++i) {} // signed/unsigned warning: a.size() is size_tUse std::size_t, or — better — for (auto& x : a).
10. Variable-length arrays (VLAs) are not standard C++.
void f(int n) {
int buf[n]; // gcc extension only — NOT portable C++
}Use std::vector<int>(n) if you really need a runtime size. Static analyzers will
flag VLAs because they let an attacker-controlled n blow the stack.
11. Subspan past the end.
std::span<int> s = ...; // size 10
s.subspan(20); // UB — debug builds may assert, release won'tAlways validate the offset/length when the source is external (a packet length, a file header, a user input).
12. std::array is not a std::vector. Resizing, push_back, and clearing
don't exist — the size is baked into the type. If you need to grow, you need a
vector (or a small-buffer-optimized variant like boost::container::small_vector).