C++ / a working model

03 / 163   ·   C++11   ·   8 min

Array Parameter Decay: Declaration Adjustment and Array-to-Pointer Conversion

Keep this sentence

An array is not a pointer, but a function parameter written in array form is adjusted to a pointer type, and in ordinary calls an array argument is often converted to a pointer to its first element. Bounds do not follow automatically; to keep length, use an array reference, a container, or C++20's span.

In this lesson
  1. Two rules produce a similar appearance
  2. Preserve bounds with types
  3. Multidimensional arrays convert only the outermost dimension
  4. Example
  5. Exercise

Two rules produce a similar appearance

In void f(int a[10]), the parameter type is adjusted to int*, declaring the same function as void f(int* a). The 10 in parentheses does not constitute a length check at the call; an array of only two elements is not rejected because of that number. Inside the function body, a is a pointer variable from the start.

This is parameter adjustment at the declaration level; array-to-pointer conversion at the call site is a separate rule. A true array object owns a fixed number of elements; a pointer object merely stores a location. Calling both “arrays are pointers” misses important differences in assignment, sizeof, taking the address, and template argument deduction.

The similar spelling of array parameters and pointer parameters is a historical adjustment, not proof that the two types are the same. The bound written in the parameter is not a runtime check. Once inside the function, you have a pointer, and the original array extent is already gone unless you preserved it some other way.

Preserve bounds with types

template<std::size_t N> void f(int (&a)[N]) takes an array reference; N can be deduced from the argument, and the binding does not lose the array type. A fixed-length interface can also be written int (&a)[3], and the compiler will reject a mismatched length. The example uses this rule to count elements, independent of any pointer width.

std::array<T, N> can copy the whole array by value and can also be passed by reference; std::vector stores a runtime length. C++20's std::span<T> denotes a non-owning contiguous range, combining the lightness of a pointer interface with length information, but the caller must still guarantee that the referred-to storage outlives the span.

If the type still says array, the bound is still there. References to arrays, std::array, containers, and span are the usual ways to stop the bound from disappearing. Span is light like a pointer-plus-length pair; it does not own the elements and does not extend their lifetime.

Multidimensional arrays convert only the outermost dimension

After conversion, int grid[2][3] is int (*)[3], a pointer to a row, not int**. Row width participates in pointer arithmetic: adding one advances a whole row. A two-level pointer usually points at a separate set of pointer objects, a completely different representation. You cannot fix a wrong interface model with a cast.

sizeof, taking the address of an array, and binding an array reference all preserve array identity. When designing an interface, first ask whether you need ownership, mutation, and length, then choose a container, a reference, or a view; do not drop the originally clear array bounds and then guess them back inside the function.

A nested array is a contiguous block of inner arrays. Pointer-to-pointer layout is a different data model and is not what int[2][3] is. Keep the inner dimension in the type whenever pointer arithmetic must step by a whole row.

Pitfalls

  • sizeof on an array-form parameter yields the size of the adjusted pointer type; dividing by the element size cannot recover the element count.
  • Returning a pointer or span to a local array does not extend the array's lifetime; keeping the length is not the same as keeping the storage.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <cstddef>
#include <iostream>
#include <type_traits>

void set_first(int values[3]) {
    static_assert(std::is_same<decltype(values), int*>::value, "adjusted");
    values[0] = 9;
}

template<class T, std::size_t N>
constexpr std::size_t count_of(const T (&)[N]) { return N; }

int main() {
    int values[3] = {1, 2, 3};
    static_assert(count_of(values) == 3, "bound retained");
    set_first(values);
    assert(values[0] == 9);
    std::cout << count_of(values) << ' ' << values[0] << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-array-decay.cpp -o example && ./example

Expected result

3 9

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Write a function-template parameter that only reads int matrix[2][3] while retaining row and column counts, and explain why int** is unsuitable.

Show a reference answer

Write template<std::size_t R, std::size_t C> void inspect(const int (&m)[R][C]); at the call, R is deduced as 2 and C as 3. The matrix stores six ints contiguously and does not store two row pointers; int** indirection requires pointer objects to exist, so it is not the correct type for this layout.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog