Table of Contents
1. Introduction
2. Common Solutions: Basic Form of Macros
3. Dangerous Macros
4. The Real Reason: Array Decay
5. Solution – Enhanced Macros
6. Conclusion: From Usable to Robust
1. Introduction
In our daily embedded development, arrays are a data structure we cannot avoid. Whether defining a buffer or a configuration table, they often take the form of an array. When writing code, we often need information about the number of elements in the array.
Beginners can easily make mistakes when handling arrays: hardcoding.
Consider the following code:
/* Inside Embedded City */uint32_t rx_buff[64];
for (uin32_t i = 0; i < 64; i++){ /* TBI ... */ ;}
Here, 64 is a typical “magic number”. It has two fatal problems:
-
Maintenance Difficulty: Once the requirements change and the buffer needs to be expanded to 128, you need to search through the code to modify each instance of 64. Missing one instance could lead to a potential bug.
-
Ambiguity: There may be several instances of 64 in the code, some representing buffer size and others possibly representing packet length. Would you dare to replace them all indiscriminately?
2. Common Solutions: Basic Form of Macros
To solve this problem, we need a mechanism that allows the code toautomatically detect the size of the array instead of manually maintaining it. As embedded engineers, we are extremely sensitive to performance. We want this calculation to occur at compile time (completed statically by the compiler) and absolutely not to burden the CPU performance at runtime.
Based on this requirement, we can use the sizeof operator in conjunction with macros to achieve the above goal:
/* Inside Embedded City */#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
With this macro, the code becomes much more elegant:
/* Inside Embedded City */#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
uint32_t rx_buff[64];
/* Even if the definition of rx_buff changes, this does not need to be modified; the macro will automatically calculate */for (uin32_t i = 0; i < ARRAY_SIZE(rx_buff); i++){ /* TBI ... */ ;}
3. Dangerous Macros
This macro seems flawless; the total byte size divided by the byte size of a single element equals the number of elements. Simple, intuitive, and efficient. However, a feature of the C language can cause this seemingly perfect macro to fail instantly, even leading to hard-to-trace bugs.
This mechanism is the core concept we will discuss today — array pointer decay (Array Decay).
Let’s directly look at an incident of incorrect usage of the ARRAY_SIZE macro:
/* Inside Embedded City */void consume_rx_buff(uint32_t rx_buff[]){ /* 1. Calculate the array length */ uint32_t cnt = ARRAY_SIZE(rx_buff);
/* 2. Process array contents */ for (uint32_t i = 0; i < cnt; i++) { ; }}int main(void){ uint32_t rx_buff[64];
consume_rx_buff(rx_buff);
while(1) { ; } return 0;}
Stop and think for a second, how many times do you think the loop in consume_rx_buff will execute? Is it the number of elements, 64?
If you think so, you are mistaken. In a common 32-bit ARM system, such as the familiar blue pill (STM32F103C8T6 minimum system development board), the running result indicates thatthis loop will only execute once. Regardless of whether the array defined outside is of size 64, 128, or 1024, once inside the function, the calculated length will always be1.
4. The Real Reason: Array Decay
Why is the result 1? We need to execute that macro as the CPU would. Reviewing the definition of this macro:

The problem lies here; inside consume_rx_buff, the compiler sees that the variable rx_buff has changed. At this point:
-
The numerator sizeof(rx_buff): At this point, rx_buff is no longer that array of 64 elements. In the function parameter list, it has decayed into a pointer. In a 32-bit system, the size of a pointer is always 4 bytes.
-
The denominator sizeof(rx_buff[0]): Although rx_buff has become a pointer, C allows the use of the [ ] notation on pointers. The meaning of rx_buff[0] is to retrieve the first data pointed to by this pointer. Since rx_buff is of type uint32_t*, the data retrieved is guaranteed to be a uint32_t type integer, which still occupies 4 bytes.
-
The final calculation:

This is the source of the problem:The macro has not failed; it has simply calculated “pointer size divided by data size” instead of what you thought was “array size divided by data size”.
The essence of this mechanism is the implicit conversion of array type (Array Type) to pointer type (Pointer Type) during passing, a behavior known as Pointer Decay.
This decay is to ensure the efficiency of the C language. The specific reason for avoiding unnecessary overhead will not be elaborated here.
5. Solution – Enhanced Macros
Now we find ourselves in a dilemma:
-
We need the convenience and maintainability brought by the ARRAY_SIZE macro.
-
But this macro silently calculates incorrectly due to pointer decay when passing parameters, leading to serious logical bugs.
If there were a way for the ARRAY_SIZE macro to identify at compile time that the user passed an array type instead of a pointer and directly report an error to prevent compilation, that would be great!
There is, dear friends, there is. Below is an enhanced version of the ARRAY_SIZE macro:
/* Inside Embedded City */#define ARRAY_SIZE(arr) \ (sizeof(arr) / sizeof((arr)[0]) \ + sizeof(typeof(int[1 - 2 * \ !!__builtin_types_compatible_p(typeof(arr), \ typeof(&arr[0]))])) * 0)
To avoid unnecessary complexity, we will not delve into this macro here. Simply put, this macro deliberately creates a compilation error to forcefully intercept potential logical bugs that may occur at runtime during the compilation phase.
It is worth noting that this macro uses GCC’s “black magic”, meaning that readers need to enable GNU Extensions on their compiler to try it out and see if it supports the built-in functions involved in this macro.
6. Conclusion: From Usable to Robust
Thus, we have completed an exploration from the basics of arrays to magical macros. Let’s review our journey:
-
Starting Point: To avoid hardcoding (Magic Number), we introduced the ARRAY_SIZE macro.
-
Turning Point: Due to C language’s enforcement of “pointer decay” for efficiency, this macro silently calculates incorrectly when passing parameters.
-
End Point: We introduced an advanced macro with type checking, utilizing compiler features to intercept potential runtime bugs at compile time.

As embedded software engineers, we must not only write code that runs but also write robust code. That complex macro is not just for show; it reflects the importance of type checking. In fact, many bugs in C language arise from this.
—— E N D ——

Inside Embedded City
Cheng Zhinai is currently employed at a major new energy vehicle company, focusing on low-level software drivers. Friends are welcome to add the author on WeChat for communication.
Receive information on autumn recruitment forms👇

WeChat ID: hubertno23
