Array decay to pointer concept explained.

Your Array Forgets Its Own Length at the Function Boundary

I remember sitting in a windowless server room during a high-frequency trading sprint, staring at a debugger that insisted a buffer was empty when I knew for a fact I’d just populated it. It wasn’t a logic error or a hardware glitch; it was the silent, insidious reality of array decay to pointer. You think you’re passing a structured block of memory with a fixed size, but the moment that array hits a function boundary, the compiler strips away its identity. It discards the bounds, forgets the type’s extent, and hands you a naked pointer—and if you aren’t explicitly tracking what was lost in that transition, you’re just building a time bomb.

I’m not here to give you a textbook definition or a sanitized lecture on syntax. I want to show you exactly how this mechanism behaves when the abstractions fail and the memory starts corrupting. We’re going to look at the actual mechanics of how the compiler treats your types, so you can stop guessing why your bounds checks are failing and start writing code that actually respects the underlying machine.

Table of Contents

The Vanishing Act Passing Arrays to Functions

The Vanishing Act Passing Arrays to Functions

The moment you pass an array into a function, it undergoes a fundamental identity crisis. You might write `void process(int arr[10])` thinking you’ve enforced a size constraint, but the compiler is laughing at you. It sees `int* arr`. The array doesn’t just lose its size; it loses its very essence as a contiguous block of known dimensions. This C++ array to pointer conversion is silent and absolute. Once that function call hits, the metadata that made it an array is gone, leaving you with nothing but a raw memory address.

This is where the `sizeof` operator becomes a trap. If you try to call `sizeof(arr)` inside that function, you won’t get the size of your buffer; you’ll get the size of a pointer—likely 8 bytes on a modern system. This discrepancy is a primary source of off-by-one errors and buffer overflows. If you aren’t explicitly passing the size as a separate argument, you are essentially flying blind. In modern C++, I generally tell people to stop doing this entirely and use `std::span` instead, but if you’re stuck in a legacy codebase, you need to respect the pointer that’s actually there.

The Sizeof Trap Why Your Array Size Just Disappeared

The Sizeof Trap Why Your Array Size Just Disappeared

This is where the logic breaks. You declare `int buffer[102]`, and `sizeof(buffer)` gives you 408 bytes. Everything feels safe. But the moment you pass that buffer into a function, you aren’t passing the container; you’re passing the starting address. Inside that function, `sizeof` no longer sees your 102 integers. It sees a single pointer. This sizeof array vs pointer discrepancy is a classic trap that catches even seasoned devs during a late-night refactor.

When you rely on `sizeof` to calculate loop bounds after a function call, you aren’t just making a mistake—you are inviting a buffer overflow. The compiler isn’t being malicious; it’s just following the rules of C++ array to pointer conversion. It has discarded the metadata about the array’s extent, leaving you with nothing but a raw memory address and no way to know where the valid data ends. If you want to stop playing this game, stop using raw pointers for bounds. Use `std::span`; it carries the size with it, so you aren’t left guessing at the memory address of array elements like you’re working on a 1970s mainframe.

How to Stop the Decay from Biting You

  • Stop using raw arrays for anything that needs to survive a function call. If you aren’t using `std::array` or `std::vector`, you aren’t writing modern C++; you’re just writing C with extra steps and more ways to crash.
  • Always pass the size explicitly if you insist on using raw pointers. The compiler isn’t going to save you, and it certainly isn’t going to tell you when you’ve wandered past the end of the buffer.
  • Use `std::span` (C++20) to bridge the gap. It’s a lightweight view that keeps the pointer and the size together in one object, preventing that exact moment where your array “forgets” how big it is.
  • Remember that `const` on the element type is not the same as `const` on the pointer. If you decay an array to a pointer, you need to be hyper-vigilant about whether you’re passing `T` or `const T` to avoid accidental mutations.
  • Use `static_assert` with `std::size()` when you’re working in template code. If you’re expecting a fixed-size array but the decay turns it into a pointer, the compiler can catch the mismatch before you ship the binary.

The Hard Truths

Once an array hits a function signature, its identity as a fixed-size block is gone; you are working with a raw pointer, and the compiler will not stop you from overstepping its bounds.

Never rely on `sizeof` inside a function to determine an array’s length; it will return the size of the pointer, not the buffer, leading to the kind of silent logic errors that haunt production logs.

If you need to preserve the array’s size and type safety, stop passing raw arrays and start using `std::array`, `std::span`, or pass by reference to a template—anything that keeps the metadata intact.

Stop Guessing, Start Verifying

To recap: array decay isn’t a feature; it’s a side effect of how the language handles memory addresses. When you pass an array to a function, you aren’t handing over the container; you are handing over a pointer to its first element. This is why `sizeof` suddenly returns the size of a pointer instead of your buffer, and why your bounds checking fails silently. If you want to avoid the inevitable segfault, stop treating raw arrays like high-level objects. Use `std::array` or `std::vector` to keep your metadata intact, or at the very least, pass the size explicitly as a separate parameter. You have to respect the pointer if you want the compiler to respect your memory.

Writing C++ is a constant negotiation with the machine. The language gives you immense power, but it won’t hold your hand when you step off a ledge. My advice? Stop relying on what you think the code is doing and start looking at what the compiler is actually seeing. Once you stop fighting the underlying mechanics and start designing around them, you move from someone who just writes code to someone who actually controls it. Don’t let a simple conversion be the reason your production system dies in the middle of the night. Master the rules, or they will eventually master you.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Execution policies explained for Par Unseq promises.

Par Unseq Promises More Than Par and Demands More From You