There’s a rather obscure feature in C99 which allows you to put the ‘static’ keyword between the brackets when declaring array-like parameters to functions, as in
1 2 3 4 5 |
void transform(uint8_t buffer[static 42]) { ... } |
What this means, in a nutshell, is that the caller of this function is expected to pass an array of at least 42 uint8_t’s. Calling ‘transform’ like this would be perfectly OK:
1 2 3 4 5 6 7 |
uint8_t buffer1[42]; transform(buffer1); uint8_t buffer2[100]; transform(buffer2); |
whereas this would constitute a breach of contract:
1 2 3 4 |
uint8_t buffer3[10]; transform(buffer3); // Error |
Likewise, passing a NULL pointer is not allowed, as it — by definition — doesn’t point to anything, not even a single element:
1 2 3 |
transform(NULL); // Error |
THE QUEST FOR MEANING
While the semantics of this feature are pretty clear, the purpose isn’t. The C99 language standard doesn’t give a hint, either.
Even when ‘static’ is used, the called function ‘transform’ doesn’t know the exact size of the array. It only knows that it comprises at least 42 elements, so it can only reliably utilize 42 elements. There’s nothing ‘transform’ can do with any (potential) extra elements, because there might be none. So what’s different to this?
1 2 3 4 5 |
void transform(uint8_t buffer[42]) { ... } |
This question haunted me for days (if not weeks) and I just couldn’t find a satisfying answer. But let’s put it aside for a moment, as I still have a lot to critisize regarding the technicalities.
THE SYNTAX
First off, why on earth ‘static’? In C, the keyword ‘static’ has already overloaded meanings: a) module scope (an object or function declared ‘static’ is private to a translation unit) and b) lifetime (a static object defined within a function retains its value across invocations). Thanks to C99, ‘static’ now can also mean “at least”. Yuck!
Had I been forced to specify this feature, I would have used the ‘>=’ operator instead of ‘static’:
1 2 3 4 5 |
void transform(uint8 buffer[>=42]) { ... } |
THE DANGERS
One of the biggest and most infamous blunders in engineering is when you don’t have a single point of truth; that is, the same information is stored in more than one place. Over time, inconsistencies arise in the copies which can lead to all kinds of havoc. All software engineers are reminded again and again to honor the DRY principle.
According to the grammar, ‘static’ can appear in function declarations as well as function definitions. Nobody prevents you from declaring ‘transform’ like this in your header file:
1 2 3 |
void transform(uint8_t buffer[static 41]); |
and like so in a C file:
1 2 3 4 5 |
void transform(uint8_t buffer[static 42]) { ... } |
The C99 standard says nothing about such inconsistencies, whether it constitutes an error, which one takes precedence, whether the compiler has to emit a warning message.
Another problem is that C99 also introduced variable-length arrays (VLAs) and hence an array’s size is not required to be a compile-time constant, like it used to be in C89 and like it still is in C++. Therefore, the C99 grammar blesses such abominations:
1 2 3 4 5 6 7 |
int my_global = 42; void transform(uint8_t buffer[static my_global]) { ... } |
What’s the contract now? At least 42 elements (the value of ‘my_global’ at program start-up) or the value of ‘my_global’ when ‘transform’ is actually called:
1 2 3 4 5 6 7 |
int my_global = 42; ... my_global = 100; uint8_t buffer4[42] transform(buffer4); // OK? |
Again, if you’d forced me to specify this feature, I’d required the size specifier to be a compile-time constant:
1 2 3 4 |
void transform(uint8 buffer[>=my_global]); // Error void transform(uint8 buffer[>=42]); // OK |
AN EPIPHANY
While ranting about this feature, I suddenly had an epiphany. Let’s revisit the question I asked above: What’s really the difference between
1 2 3 4 5 |
void transform(uint8_t buffer[42]) { ... } |
and
1 2 3 4 5 |
void transform(uint8_t buffer[static 42]) { ... } |
since my doubts regarding the usefulness of this feature are 100% founded in this question. Previously, I believed that in the second case, there are at least 42 elements, while in the first case there are exactly 42 elements. Not a big difference, because what benefit would a compiler be able to derive from this subtle difference?
But I obviously fell into a trap that I warned my readers a long time ago: believing that array parameters are really arrays, which is mistaken, of course, because arrays cannot be passed directly to functions. From the C language’s point of view, the former interface of ‘transform’ is equivalent to
1 2 3 4 5 |
void transform(uint8_t* const buffer) { ... } |
which means that ‘buffer’ is a pointer to a single ‘uint8_t’, nothing more, nothing less. The compiler can’t make any assumptions beyond that, it’s obliged to ignore the fact that you indicated that there are 42 elements. Specifically, all these declarations are equivalent and effectively declare a function taking a pointer to a ‘uint8_t’:
1 2 3 4 5 6 |
void transform(uint8_t buffer[1]); void transform(uint8_t buffer[42]); void transform(uint8_t buffer[1000]); void transform(uint8_t buffer[]); |
By contrast, the version of ‘transform’ that uses ‘static’ tells the compiler that a pointer to the first of at least 42 elements is passed. Armed with this knowledge, the compiler can now perform certain optimizations, the most likely one being accessing memory in units much larger than a ‘uint8_t’, to save memory access times. Ah!
ADVICE
Even though we now understand the motivation behind this feature, I still wouldn’t recommend using it. Just like it’s the case with many other so-called “optimization” features (e. g. ‘inline‘ and ‘restrict‘) you, as a developer, have to keep a promise but get nothing dependable back from the compiler. As usual, my advice is this: if you think something needs to be optimized, prove it first; if evidence shows that optimization is necessary, optimize in a dependable way — don’t rely on something that might change from one compiler version to another, especially if it comes with an hideous interface that invites inconsistencies.
ONE MORE THING
Thinking about this odd feature once again, what it all boils down to is that ‘static’ with function parameters actually tries to deliver what regular array-like parameters can’t, namely inform the compiler that the pointer that is passed is really an array, not just a pointer to a single element. But there’s already a feature in the C language (available since C89) that achieves that, namely pointers to arrays:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
typedef uint8_t BUFFER42[42]; void transform(BUFFER42* buffer) { ... } BUFFER42 buffer42; transform(&buffer42); typedef uint8_t BUFFER10[10]; BUFFER10 buffer10; transform(&buffer10); // Compile-time error |
Contrary to ‘static’ with array parameters, pointers to arrays is a clean, proven, and type-safe concept. Since the compiler knows how many elements the array comprises, it can apply the same optimizations that the ‘static’ with array parameters feature promises, that is, accessing multiple elements at once when traversing the array. There’s really no reason to put ‘static’ between your array brackets, trust me.