C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data
Array Decay and Array Parameters
Chapter 9.1 established two facts and then carefully declined to put them together. The first is that an array's length lives in its type — delays is an int[4], and that four is the only record anywhere that the array has four elements, since the sixteen bytes themselves have no room for it. The second is that an array's address is simply its first element's address, with no header and no bookkeeping. Put those next to a function call and the problem is immediate. Passing an array does not pass sixteen bytes; it passes an address, and an address has type int pointer, which does not contain a four. So the parameter inside the function is not an array at all, and every piece of information about how long it was has been discarded at the boundary — silently, as part of an ordinary conversion the language performs without comment. This chapter measures that, including the one narrow symptom the compiler does warn about, and then gives the four ways to get the length back, three of which are C++ features and one of which is the answer you should normally reach for.
Intermediate16 min readC++Part IFundamentalsArrays
Part I · Module 9 · Chapter 9.2 · Array Decay and Array Parameters
1. The Two Facts That Collide
Chapter 9.1 measured both of these:
The length is in the type. delays has type int[4]. The sixteen bytes contain four ints and nothing else — Figure 1 in that chapter had nowhere to record the count.
The array's address is its first element's address. Measured true; there is no header.
Now pass it to a function. What can be passed is a value, and the value available is an address. An address's type is int*. And int* does not contain a four.
An array decays to a pointer to its first element when used as a value. The length is part of the type that was discarded.
That is the mechanism Chapter 8.4 §6 named and could not demonstrate: "a pointer's bounds are not part of its type." This chapter is where the bounds get lost.
2. Decay Is a Conversion
The word for it is decay, and it is a standard conversion the language applies silently, in the same family as Chapter 4.10's:
int delays[4] = {3, 7, 2, 9};
int* p = delays; // decay: int[4] -> int*
int* q = &delays[0]; // the same value, written explicitlyBoth are the same address. It happens wherever an array is used as a value — assigned, passed, returned, compared — and the two places it does not happen are worth knowing because they are the two places Chapter 9.1's measurements worked:
| Context | Decays? |
|---|---|
sizeof(delays) | no — gives 16 |
&delays | no — gives int (*)[4], a pointer to the whole array |
delays as a function argument | yes |
int* p = delays; | yes |
a range-based for over delays | no — which is why Chapter 5.6 worked |
That last row explains something Chapter 5.6 did not have to justify: a range-based for works on a built-in array precisely because the array does not decay there. The loop needs the length, and the length is in the type, and the type survives.
Measured with decltype:
decltype(delays) is int[4] : true
decltype(&delays) is int(*)[4] : true
decltype(p) is int* : true (after int* p = delays;)
sizeof(delays) = 16 sizeof(&delays) = 8&delays has type int (*)[4] — a pointer to the whole array, which still records the 4. It is the one pointer type that has not lost the length.
3. Measured: The Parameter Is Not an Array
#include <iostream>
static void takes_array(int a[]) // looks like an array...
{
std::cout << " inside, sizeof(a) = " << sizeof(a) << " bytes\n";
}
static void takes_pointer(int* p) // ...is exactly this
{
std::cout << " inside, sizeof(p) = " << sizeof(p) << " bytes\n";
}
int main()
{
int delays[4] = {3, 7, 2, 9};
std::cout << "in main, sizeof(delays) = " << sizeof(delays) << " bytes\n";
takes_array(delays);
takes_pointer(delays);
return 0;
}Measured output:
in main, sizeof(delays) = 16 bytes
inside, sizeof(a) = 8 bytes
inside, sizeof(p) = 8 bytes16 outside, 8 inside. And 8 is Chapter 8.1 §4's measured pointer size, not any fact about the array.
The two functions are indistinguishable. int a[] and int* p declare the same parameter type. The brackets in a parameter list are, as far as the language is concerned, decoration — you may even write int a[100] and the parameter is still int*, with the 100 ignored entirely. Measured: a function declared void f(int a[100]) accepted a one-element array, reported sizeof(a) = 8, and decltype(a) was int*.
That is the honest statement of the problem: void f(int a[]) is a lie told by the syntax. It looks like it takes an array. It takes a pointer.
4. The One Thing the Compiler Warns About
There is a narrow and welcome diagnostic here:
decay.cpp:5:52: warning: sizeof on array function parameter will return size of 'int *' instead of 'int[]' [-Wsizeof-array-argument]At the standing flags, -Wall -Wextra -Wpedantic. The compiler saw sizeof applied to a parameter declared with brackets and said so.
Read carefully what that does and does not cover.
It catches the symptom. Taking sizeof of a decayed parameter is the most direct way to discover the problem, and the compiler tells you.
It does not catch the consequence. A function that loops for (int i = 0; i < 4; ++i) on a parameter, because the author remembered the array had four elements, gets no diagnostic at all — and will read out of range the day somebody passes an array of three. Chapter 9.1 §6's measurement applies: a runtime index into a pointer is silent, and only a sanitizer catches it.
So the warning is a useful tripwire for the one obvious mistake, and the rest of the chapter is about not needing it.
5. What Decay Costs in Practice
Three consequences, in increasing order of how much trouble they cause.
sizeof becomes useless. Chapter 9.1's sizeof a / sizeof a[0] idiom gives 8 / 4 = 2 inside the function, regardless of the real length. Two is not a plausible-looking wrong answer, which is some comfort.
The function cannot validate anything. It cannot check an index, cannot stop at the end, cannot even tell whether it was given one element or a thousand. Every bound it uses must come from somewhere else.
And a range-based for stops working. Not because of a subtle rule but because the length is gone:
static void f(int a[]) { for (int v : a) { /* ... */ } } // does not compileThe diagnostic explains the whole chapter in one sentence:
error: cannot build range expression with array function parameter 'a' since parameter with array type 'int[]' is treated as pointer type 'int *'That refusal is the language being helpful. The loop needs a beginning and an end, and a pointer supplies only a beginning.
One note for later: a multidimensional array decays too, and only its first dimension is lost — int m[2][3] becomes int (*)[3], keeping the 3. Chapter 9.3 is where that matters.
6. Fix One: Pass the Length
The traditional answer, and the one every C interface uses:
static int sum_with_length(const int* a, std::size_t n)
{
int t = 0;
for (std::size_t i = 0; i < n; ++i) t += a[i];
return t;
}Measured: sum_with_length(delays, 4) gives 21.
It works and it is fragile. The length is now a second argument that the caller must supply correctly, and nothing checks that it matches. Measured: sum_with_length(delays, 400) on a four-element array produced zero diagnostics at the standing flags.
Two habits make it less fragile:
Derive the length at the call site rather than writing a literal. sum_with_length(delays, sizeof delays / sizeof delays[0]) — the idiom works there, because the caller still has the array type.
Put the pointer and the length adjacent in the signature, in that order, so the pairing is visible. const int* a, std::size_t n is the convention the standard library itself uses.
7. Fixes Two and Three: Keep the Type
C++ can do better than C here, because a reference does not decay.
A reference to an array of known size:
static int sum_by_ref(const int (&a)[4])
{
int t = 0;
for (int v : a) t += v; // range-for works: the size is in the type
return t;
}Measured: 21, and the range-based for compiles, because the parameter's type really is const int[4].
Read the declaration with Chapter 8.7 §2's right-to-left rule: a is a reference to an array of 4 const int. The parentheses are required — without them you get:
error: 'a' declared as array of references of type 'const int &'which is a different thing entirely, and not a legal one.
The cost is that it accepts only that one size. Passing a [6] array is a compile error, which is safe and inflexible.
A template over the size removes the inflexibility:
template <std::size_t N>
static int sum_any(const int (&a)[N])
{
int t = 0;
for (int v : a) t += v;
return t;
}Measured: 21 for the [4] array and 6 for a [6] array, from one definition. The compiler deduces N from the argument's type, so the length is never passed and never wrong.
Templates are Module 18's subject and this is a borrowing — declared, as in Chapters 5.6, 6.2, 8.4 and 8.8. The reason it is worth borrowing here is that it is the only one of the three C++ fixes that is both safe and general.
8. Fix Four: Do Not Use an Array
The answer you should normally reach for, and the reason this module's description says built-in arrays are taught "honestly because real interfaces still use them" rather than because you should write them.
| Instead of | Use | Carries its length? |
|---|---|---|
int a[4], fixed size | std::array<int, 4> | yes |
| a runtime-sized array | std::vector<int> | yes |
a char array of text | std::string | yes — Chapter 9.5 |
| a pointer + length pair for text | std::string_view | yes — Chapter 9.6 |
All four keep the length with the data rather than in the type, so it survives being passed, copied, returned and stored. A std::vector<int> parameter knows its own size inside the function; that is the whole difference.
Module 11 is where the containers arrive. Chapter 9.5 and Chapter 9.6 are the text cases, two and three chapters from here, and Chapter 9.6 is worth flagging now because it is exactly this chapter's problem solved for one data type: a std::string_view is a pointer and a length travelling together.
One form deliberately named and not taught: C++20 added std::span, which generalises that pairing to any contiguous sequence and is the direct modern answer to §6's fragile pointer-plus-length. This curriculum teaches C++17, so it is out of scope — but if you are reading a codebase that targets C++20 or later, a std::span<const int> parameter is what §6 should have been.
9. What to Actually Do
Expect an array parameter to be a pointer. int a[], int a[64] and int* a are the same thing.
Never take sizeof of an array parameter. The compiler warns, and the answer is the pointer's size.
Prefer a container. std::vector, std::array, std::string — the length travels with the data.
When you must take a pointer, take a length next to it, in that order, and derive it at the call site rather than writing a literal.
Use const T (&a)[N] with a size template when you genuinely need a built-in array and want the length kept — the only safe and general C++ fix.
Read void f(int a[64]) as a lie. The 64 is a comment.
Remember a range-based for works on an array and not on an array parameter — and that the refusal is the language telling you the length is gone.
10. Mistakes Worth Making Once
Taking sizeof of an array parameter. Measured: 8, not 16, and the compiler warns.
Writing void f(int a[4]) and believing the 4 is enforced. It is ignored entirely.
Hard-coding a bound inside a function because you remember the caller's size. No diagnostic, and it breaks the day a different array is passed.
Expecting a range-based for to work on an array parameter. It needs a length; a pointer has none.
Passing a length that does not match. sum_with_length(delays, 400) compiles and reads 1600 bytes.
Writing const int& a[4] for a reference to an array. That is an array of references; the parentheses in const int (&a)[4] are required.
Assuming decay is a mistake in the language. It made passing an array free when the alternative was copying it.
11. Interview Relevance
"What happens when you pass an array to a function?" It decays to a pointer to its first element, so the parameter's type is a pointer and the length is lost. The measurement to have ready is sizeof — 16 at the call site, 8 inside.
"Are void f(int a[]) and void f(int* a) different?" No, they declare the same function. Adding that int a[64] is also the same, with the 64 ignored, is the detail that shows you have met the problem rather than read about it.
"How do you pass an array without losing the length?" Four answers in increasing quality: a length parameter, a reference to an array of fixed size, a template over the size, or a container. Naming the container as the normal answer — and std::span if the codebase is C++20 — is the complete response.
"Why does a range-based for work on an array but not on an array parameter?" Because the loop needs the length, the length is in the type, and the parameter's type is a pointer. This question is a good test of whether the earlier answers were understood or recited.
12. What You Should Be Able to Say Now
- An array decays to a pointer to its first element when used as a value — and the length was in the type that got discarded.
- That is the mechanism Chapter 8.4 §6 named and could not show: a pointer's bounds are not part of its type.
- Decay does not happen in
sizeof, in&array, or in a range-basedfor— which is why Chapter 5.6 worked. - Measured:
sizeofis 16 at the call site and 8 inside the function. int a[],int a[64]andint* aare the same parameter type, and the number is ignored.- So
void f(int a[64])is a lie told by the syntax — the 64 is documentation. - The compiler does warn about one thing:
sizeof on array function parameter will return size of 'int *' instead of 'int[]'. - It catches the symptom, not the consequence — a hard-coded bound gets no diagnostic.
- Measured: all three C++ fixes work — length parameter gave 21,
const int (&a)[4]gave 21 with a working range-for, and the size template gave 21 and 6 from one definition. - The reference form needs parentheses:
const int (&a)[4], notconst int& a[4]. - The size template is the only safe and general C++ fix for a built-in array.
- The normal answer is a container — the length travels with the data.
std::spanis the C++20 answer to the pointer-plus-length pair; out of scope here.- Decay exists so that passing an array is free, which is why every C buffer function takes a size next to the pointer.
13. Exercises
Exercise 1 — predict the sizes. For double d[8] passed to void f(double a[]), give sizeof(d) at the call site, sizeof(a) inside, and the value of sizeof a / sizeof a[0] inside.
Exercise 2 — same or different? Say which of these declare the same function: (a) void f(int* a) (b) void f(int a[]) (c) void f(int a[4]) (d) void f(int (&a)[4]) (e) void f(int a[][3]).
Exercise 3 — where does decay not happen? List the three contexts in §2 where an array keeps its type, and say what each one is used for.
Exercise 4 — fix it four ways. int sum(int a[]); cannot know its length. Write all four fixes, and for each say what the caller must now supply.
Exercise 5 — read the signature. For int process(const std::uint8_t* data, std::size_t length);, say what the two parameters are logically, what is not checked, and what the first one probably was further up the call chain.
Exercise 6 — why the refusal. Explain why for (int v : a) compiles for int a[4] in main and not for a parameter declared int a[], in terms of what the loop needs.
Exercise 7 — the misleading form. A colleague writes void load(std::uint8_t buffer[64]); and says the signature documents the required size. Say what the compiler does with the 64, what a caller may legally pass, and the two signatures you would offer instead.
14. Summary
- An array decays to a pointer to its first element; the length was in the discarded type.
- That is Chapter 8.4 §6's mechanism, shown.
- No decay in
sizeof,&array, or a range-basedfor. - Measured:
sizeof16 outside, 8 inside;int a[]andint* aare the same parameter. void f(int a[64])is a lie — the number is ignored.- One warning exists:
-Wsizeof-array-argument, which catches the symptom and not the consequence. - Measured fixes: length parameter (21),
const int (&a)[4]with a working range-for(21), size template (21 and 6). - The reference form needs parentheses; the template is the only safe and general built-in-array fix.
- A container is the normal answer;
std::spanis C++20's. - Decay exists so passing an array is free — hence every C buffer function's size parameter.
One dimension is enough to lose a length. The next chapter adds a second, and two things change. The layout question becomes real — a two-dimensional array is still one contiguous run of bytes, so something has to decide whether the rows or the columns are adjacent, and C++ decides one way with consequences you can measure as offsets. And decay gets stranger rather than worse: passing a two-dimensional array loses the first dimension and keeps the rest, which means the parameter type is a pointer to an array, a declaration most people have to read twice.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
