C++ Arrays vs std::array vs std::span: Choose the Right Interface

Run the same five integers through raw-array, std::array and std::span interfaces, then choose from their ownership, size and lifetime contracts.

KnowledgeGate Team

Exam prep & CS education

Updated 2 Sep 20266 min read

int a[5], std::array<int, 5> and std::span<const int> can all expose five integers, but they express different ownership, size and lifetime contracts. Each interface exposes the sequence {4, 1, 7, 1, 3} differently in real code and exam-style snippets. If the wider sequence is unfamiliar, start with the C++ Tutorial: Complete Learning Path in 12 Weeks.

1. Start with three questions: who owns, who knows the size, who may outlive whom?

Ownership asks which object controls the element lifetimes. Extent asks where the size lives: in the type, in a runtime value, or nowhere after a function-boundary conversion. Lifetime asks whether a callee merely borrows storage that must remain alive.

The raw array and fixed array contain the same values:

int raw[5]{4, 1, 7, 1, 3};
std::array<int, 5> fixed{4, 1, 7, 1, 3};

The full sum is 4 + 1 + 7 + 1 + 3 = 16. The middle slice at indices 1, 2, 3 is {1, 7, 1}, so its sum is 1 + 7 + 1 = 9.

Form

Owns elements

Extent visible in declared type

Copy behaviour

Natural use

int[5]

yes

yes before decay

copies all 5 elements if the containing object is copied

legacy or low-level storage

std::array<int, 5>

yes

yes

value copy of all 5

fixed-size owning value

std::span<const int>

no

dynamic in this spelling

copies only the view

borrowing contiguous data

2. Raw arrays: five elements in the caller, pointer plus count in the usual callee

A common interface is int sum_raw(const int* data, std::size_t count). Call it as sum_raw(raw, std::size(raw)).

For indices 0 through 4, the accumulator is 0 -> 4 -> 5 -> 12 -> 13 -> 16. The caller's array owns the five elements. The function receives only an address and the separately supplied value 5, which the caller must keep consistent.

Writing int sum_bad(const int data[5]) does not enforce five elements. In a parameter list, data[5] is adjusted to const int*, so the callee cannot recover the original extent. sizeof(raw) / sizeof(raw[0]) = 5 works only where raw is still an array. No byte count is assumed because sizeof(int) is implementation-dependent.

An advanced alternative is template<std::size_t N> int sum_preserved(const int (&data)[N]). Passing raw deduces N = 5, but this is less uniform than a range-style boundary. The Pointers in C for GATE guide gives more background on decay and pointer-based access.

3. std::array: make fixed-size storage a value

std::array<int, 5> is an owning fixed-size aggregate, and 5 is part of its type. Use int sum_array(const std::array<int, 5>& data). Calling sum_array(fixed) returns 16, while const& avoids a copy. By contrast, auto copy = fixed; copy[0] = 40; makes copy equal to {40, 1, 7, 1, 3}, while fixed remains {4, 1, 7, 1, 3}. The type supports .size() == 5, range-for loops and standard algorithms. Its operator[] does not itself promise bounds checking.

std::array<int, 4> and std::array<int, 5> are different types. That distinction is valuable when the extent is invariant, but awkward when one algorithm should accept several contiguous sizes.

4. std::span: one borrowed interface for contiguous ranges

std::span is a C++20 non-owning view of a contiguous sequence. One function can accept several owners:

int sum_view(std::span<const int> data) {
    int total = 0;
    for (int value : data) total += value;
    return total;
}

Both sum_view(raw) and sum_view(fixed) return 16. Given std::vector<int> dynamic{4, 1, 7, 1, 3};, sum_view(dynamic) also returns 16.

Now form auto middle = std::span{fixed}.subspan(1, 3);. It refers to indices 1 through 3, views {1, 7, 1}, and produces 9. Creating or copying the span copies no integers. If fixed[2] changes from 7 to 70, the same view becomes {1, 70, 1} and the sum becomes 1 + 70 + 1 = 72.

std::span<const int> prevents mutation through this view, not through every alias. A span neither owns nor extends the source lifetime. It is also not automatic bounds safety: indexing must remain within the view, and a bad pointer-range contract can create an invalid view.

Three lanes send {4,1,7,1,3} into sum_raw, sum_array and a non-owning std::span view; full ranges sum to 16 and the middle slice to 9.

5. Worked example: process the same data three ways

The C++20 program passes identical inputs to each function:

#include <array>
#include <cstddef>
#include <iostream>
#include <iterator>
#include <span>
#include <vector>

int sum_raw(const int* data,std::size_t count) {
    int total=0;
    for(std::size_t i=0;i<count;++i) total+=data[i];
    return total;
}

int sum_array(const std::array<int, 5>& data) {
    int total=0;
    for(int value:data) total+=value;
    return total;
}

int sum_view(std::span<const int> data) {
    int total=0;
    for(int value:data) total+=value;
    return total;
}

int main() {
    int raw[5]{4, 1, 7, 1, 3};
    std::array<int, 5> fixed{4, 1, 7, 1, 3};
    std::vector<int> dynamic{4, 1, 7, 1, 3};

    std::cout<<sum_raw(raw,std::size(raw))<<'\n';
    std::cout<<sum_array(fixed)<<'\n';
    std::cout<<sum_view(raw)<<'\n';
    std::cout<<sum_view(fixed)<<'\n';
    std::cout<<sum_view(dynamic)<<'\n';
    std::cout<<sum_view(std::span{fixed}.subspan(1,3))<<'\n';
}

The expected lines, in order, are 16, 16, 16, 16, 16, 9. The raw call carries an address and count that must agree. The std::array call carries ownership and exact extent in its type. The dynamic-extent span carries a borrow and current length, so one algorithm accepts all three contiguous sources. The calculation is identical, but the advertised preconditions differ.

Situation

Prefer

Reason

Local fixed table of exactly 5 owned values

std::array<int, 5>

Value semantics and size in the type

Borrow any contiguous integer sequence without copying

std::span<const int>

One pointer-and-extent style interface for several owners

Interoperate with an API already expressed as pointer plus count

raw pointer/count at that boundary

Matches the external contract; wrap it in a span internally when valid

Size changes and the object owns its elements

std::vector<int>

Neither std::array nor std::span is a growable owner

Decision tree picking std::array for fixed owned size, std::vector for growth and std::span<const int> for a borrowed contiguous range.

6. Traps: decay, dangling views and false safety claims

  • False extent: void inspect(int values[5]) does not enforce five elements. Pass a count, preserve extent with an array reference or template, or accept a span.

  • Dangling view: std::span<const int> bad() { std::array<int, 3> local{2, 4, 6}; return local; } returns a view to local. The array is destroyed on return, so the span dangles. Span improves interface shape, not lifetime ownership.

  • Unnecessary mutation: if the function only reads, prefer std::span<const int> over std::span<int> so the signature states its intent.

  • False container replacement: span requires contiguous storage and provides no capacity or growth operations.

  • Wrong fixed abstraction: using std::array to accept any length forces separate std::array<int, 4> and std::array<int, 5> instantiations, or a template. Prefer span when runtime extent is part of the input.

7. How exam-style and interview snippets test the choice

Common tasks ask you to identify array-to-pointer adjustment, predict whether a copy changes the original, compute a subspan result, or choose an interface from ownership and extent requirements.

For an output question, consider:

std::array<int, 5> a{4, 1, 7, 1, 3};
auto s = std::span{a}.subspan(1, 3);
a[2] = 70;

A loop printing s produces 1 70 1, not 1 7 1, because s is a view. Its sum is 72. If the code then uses auto b = a; b[2] = 700;, the original a[2] stays 70 because b owns a copied value.

For a compile-time choice, non-template void f(const std::array<int, 5>&) accepts std::array<int, 5> but not std::array<int, 4>. void g(std::span<const int>) can accept either contiguous array. Confirm the syllabus and permitted language standard with the official source for your own exam or assessment.

8. The short version and the next step

Choose std::array<T, N> for fixed-size ownership and value semantics. Choose std::span<T> or std::span<const T> for a temporary contiguous borrow. Keep raw pointer-plus-count interfaces mainly where an existing boundary requires them. If ownership must grow, use an owning dynamic container.

Next, rewrite sum_view as count_above(data, threshold). With threshold 3, only 4 and 7 qualify, so every full five-element source returns 2. Continue with C++ Programming, broaden your practice through Coding for Placements, or browse Coding & Skills.