5 ms·
Checked-size array parameters in C
- aaaashley 10mo agoFunny thing about that n[static M] array checking syntax–it was even considered bad in 1999, when it was included: "There was a unanimous vote that the feature is ugly, and a good consensus that its incorporation into the standard at the 11th hour was an unfortunate decision." - Raymond Mak (Canada C Working Group), https://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_205.htm https://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_205.htm
- jacquesm 10mo agoIt wasn't considered bad, it was considered ugly and in the context given that is a major difference. The proposed alternative in that post to me is even more ugly so I would have agreed with the option that received the most support, to leave it as it was.
- moefh 10mo agoIt was always considered bad not (just) because it's ugly, but because it hides potential problems and adds no safety at all: a `[static N]` parameter tells the compiler that the parameter will never be NULL, but the function can still be called with a NULL pointer anyway. That's is the current state of both gcc and clang: they will both happily, without warnings, pass a NULL pointer to a function with a `[static N]` parameter, and then REMOVE ANY NULL CHECK from the function, because the argument can't possibly be NULL according to the function signature, so the check is obviously redundant. See the example in [1]: note that in the assembly of `f1` the NULL check is removed, while it's present in the "unsafe" `f2`, making it actually safer. Also note that gcc will at least tell you that the check in `f1()` is "useless" (yet no warning about `g()` calling it with a pointer that could be NULL), while clang sees nothing wrong at all. [1] https://godbolt.org/z/ba6rxc8W5 https://godbolt.org/z/ba6rxc8W5
- jacquesm 10mo agoInteresting, I wasn't aware of that and thought the compiler would at least throw up a warning if it had seen that function prototype.
- moefh 10mo agoIt's not intuitive, although arguably conforms to the general C philosophy of not getting in the way unless the code has no chance of being right. For example, both compilers do complain if you try to pass a literal NULL to `f1` (because that can't possibly be right), the same way they warn about division by a literal zero but give no warnings about dividing by a number that is not known to be nonzero.
- jacquesm 10mo agoRight, so if the value is known at compile time it will flag the error but if it only appears at runtime it will happily consume the null and wreak whatever havoc that will lead to further down the line. Ok, thank you for pointing this out, I must have held that misconception for a really long time.
- OneDeuxTriSeiGo 10mo agoNote that the point of [static N] and [N] is to enforce type safety for "internal code". Any external ABI facing code should not use it and arguably there should be a lint/warning for its usage across an untrusted interface. Inside of a project that's all compiled together however it tends to work as expected. It's just that you must make sure your nullable pointers are being checked (which of course one can enforce with annotations in C). TLDR: Explicit non-null pointers work just fine but you shouldn't be using them on external interfaces and if you are using them in general you should be annotating and/or explicitly checking your nullable pointers as soon as they cross your external interfaces.
- MobiusHorizons 10mo agoWow, that’s crazy. Does anyone have any context on why they didn’t fix this by either disallowing NULL, or not treating the pointer as non-nullable? I’m assuming there is code that was expecting this not to error, but the combination really seems like a bug not just a sharp edge.
- o11c 10mo agoBetter option: just wrap it in a unique struct. There are perhaps only 3 numbers: 0, 1, and lots. A fair argument might be made that 2 also exists, but for anything higher, you need to think about your abstraction.
- pixl97 10mo agohttps://en.wikipedia.org/wiki/Zero_one_infinity_rule https://en.wikipedia.org/wiki/Zero_one_infinity_rule
- piekvorst 10mo agoNice article, never seen that. I’ve always thought it’s good practice for a system to declare its limits upfront. That feels more honest than promising ”infinity” but then failing to scale in practice. Prematurely designing for infinity can also cause over-engineering—like using quicksort on an array of four elements. Scale isn’t a binary choice between “off” and “infinity.” It’s a continuum we navigate with small, deliberate, and often painful steps—not a single, massive, upfront investment. That said, I agree the ZOI is a valuable guideline for abstraction, though less so for implementation.
- o11c 10mo agoThere's a reason I prefer "lots" over "infinity". For your "quicksort of 4 elements" example, I would note that the algorithm doesn't care - it still works - and the choice of when to switch to insertion sort is a mere matter of tuning thresholds.
- kragen 10mo agoThe zero-one-infinity rule is not applicable to the number of bytes in Poly1305 nonces and ChaCha20 keys. They are exceptions.
- Veserv 10mo agoPointer to array is not only type-safe, it is also objectively correct and should have always been the syntax used when passing in the address of a known, fixed size array. This is all a artifact of C automatically decaying arrays to pointers in argument lists when a array argument should have always meant passing a array by value; then this syntax would have been the only way to pass in the address of a array and we would not have these warts. Automatic decaying is truly one of the worst actual design mistakes of the language (i.e. a error even when it was designed, not the failure to adopt new innovations).
- jacquesm 10mo agoFully agreed, and something that is hard to fix. This guy is trying really hard and with some success: https://news.ycombinator.com/item?id=45735877 https://news.ycombinator.com/item?id=45735877
- wild_pointer 10mo agoThis guy is doing something else completely. In his words: > In my testing, it's between 1.2x and 4x slower than Yolo-C. It uses between 2x and 3x more memory. Others have observed higher overheads in certain tests (I've heard of some things being 8x slower). How much this matters depends on your perspective. Imagine running your desktop environment on a 4x slower computer with 3x less memory. You've probably done exactly this and you probably survived the experience. So the catch is: Fil-C is for folks who want the security benefits badly enough. (from https://news.ycombinator.com/item?id=46090332 https://news.ycombinator.com/item?id=46090332) We're talking about a lack of fat pointers here, and switching to GC and having a 4x slower computer experience is not required for that.
- jacquesm 10mo agoThis is not about performance.
- Veserv 10mo agoI am actually not talking about the lack of fat pointers. That is almost entirely orthogonal to my point. I am talking about the fact that what would be the syntax for passing a array by value was repurposed for automatically decaying into a pointer. This results in a massive and unnecessary syntactic wart. The fact that the correct type signature, a pointer to fixed-size array, exists and that you can create a struct containing a fixed-size array member and pass that in by value completely invalidates any possible argument for having special semantics for fixed-size array parameters. Automatic decay should have died when it became possible to pass structs by value. Its continued existence continues to result in people writing objectively inferior function signatures (though part of this it the absurdity of C type declarations making the objectively correct type a pain to write or use, another one of the worst actual design mistakes). Fat pointers or argument-aware non-fixed size array parameters are a separate valuable feature, but it is at least understandable for them to not have been included at the time.
- nikeee 10mo agoGCC also has an extension to support references to other parameters of the function: #include <stddef.h> void foo(size_t n, int b[static n]); https://godbolt.org/z/c4o7hGaG1 https://godbolt.org/z/c4o7hGaG1 It is not limited to compile-time constants. Doesn't work in clang, sadly.
- fuhsnn 10mo agoClang is working on a different version with annotations https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3656.pdf https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3656.pdf
- eqvinox 10mo agoIt also only works with that order, not if the size is after the array :(
- kragen 10mo agoNo, you can predeclare the size; this compiles with no warnings: #include <string.h> #include <unistd.h> void foo(size_t n; const char s[static n], size_t n) { write(1, s, n); } int main(int argc, char **argv) { foo("hello, ", 7); if (argc > 1) foo(argv[1], strlen(argv[1])); foo("\n", 1); return 0; } However, it still compiles with no warnings if you change 7 to 10! Clang does not support this syntax.
- Arch-TK 10mo agoThat weird feeling when you realise that the people you hang out with form such a weird niche that something considered common knowledge among you is being described as "buried deep within the C standard". What's noteworthy is that the compiler isn't required to generate a warning if the array is too small. That's just GCC being generous with its help. The official stance is that it's simply undefined behaviour to pass a pointer to an object which is too small (yes, only to pass, even if you don't access it).
- loeg 10mo agoThe other fun wart with `static` is C++ doesn't support it. So it has to be macro'd out in headers shared with C++. https://godbolt.org/z/z9EEcrYT6 https://godbolt.org/z/z9EEcrYT6
- pjmlp 10mo agoAnd probably never will, because C++ compatibility with C beyond what was done initially, is to one be close as possible but not at the expense of better alternatives that the language already offers. Thus std::array, std::span, std::string, std::string_view, std::vector, with hardned options turned on. For the static thing, the right way in C++ is to use a template parameter, template<typename T, int size> int foo(T (&ary)[size]) { return size; } -- https://godbolt.org/z/MhccKWocE https://godbolt.org/z/MhccKWocE If you want to get fancy, you might make use of concepts, or constexpr to validate size at compile time.
- loeg 10mo agoI guess. I think it's mostly not a very useful part of C, so it doesn't see much adoption in C anyway. In these applications, size and T are fixed -- you'd just take `std::span<uint8_t, XCHACHA20POLY1305_NONCE_SIZE>` rather than templating.
- uecker 10mo agoThe C feature does not have to be fixed-size though.
- Philpax 10mo agoExcited to for Walter to drop by and extol the virtues of fat pointers :-) For reference: https://digitalmars.com/articles/C-biggest-mistake.html https://digitalmars.com/articles/C-biggest-mistake.html
- halayli 10mo agoon a similar note, there are also these field attributes that are very helpful for catching similar issues: https://clang.llvm.org/docs/AttributeReference.html#counted-by-counted-by-or-null-sized-by-sized-by-or-null https://clang.llvm.org/docs/AttributeReference.html#counted-...
- kazinator 10mo agoThe pointer-to-array solution is okay, with the caveat that pointer-to-array typedefs should be avoided. The problem is that they are attractive for reducing repeated declarations: typedef unsigned char thing_t[THING_SIZE]; struct red_box_with_a_hook { thing_t thing1, thing2; } void shake_hands_with(thing_t *thing); That is all well. But thing_t is an array type which still decays to pointer. It looks as if thing_t can be passed by value, but since it is an array, it sneakily isn't passed by value: void catch_with_net(thing_t thing); // thing's type is actually "usnsigned char *" // ... unsigned char x[42]]; catch_with_net(x); // pointer to first element passed; type checks
- uecker 10mo agoIndeed, I think one should avoid such typedefs. You could wrap it in a struct.
- dfawcus 10mo agoyet if one does that, and has suitable warnings turned on, GCC will complain: array2.c: In function ‘main’: array2.c:25:17: warning: passing argument 1 of ‘arr_fn2’ from incompatible pointer type [-Wincompatible-pointer-types] 25 | arr_fn2(array); | ^~~~~ | | | char * array2.c:13:22: note: expected ‘char (*)[15]’ but argument is of type ‘char *’ 13 | void arr_fn2(Arr_15 *arr) { | ~~~~~~~~^~~ The above was -Wall -Wextra Or are you just referring to the function where one defines it as apparently 'pass by value'?
- 1over137 10mo agoAnyone know if there's a flag to tell clang to treat `void fn(int array[N])` as if it was `void fn(int array[static N])`?
- int_19h 10mo agoI don't think such a thing exists simply because it would break so many headers if you turned that on, it wouldn't be particularly useful.
- anonymousiam 10mo agoThis would not be the first time that the "static" keyword in C was reused for something "new" (relative to the original K&R pre-ANSI C). https://developer.arm.com/community/arm-community-blogs/b/embedded-and-microcontrollers-blog/posts/using-the-static-keyword-in-c https://developer.arm.com/community/arm-community-blogs/b/em...
- eqvinox 10mo ago"Would"? It's already in the standard.
- Animats 10mo agoBut only for constant size arrays. You could just declare struct Nonce { char nonce_data[SIZE_OF_NONCE]; } and pass those around to get roughly the same effect.
- jjgreen 10mo agoCracked me up the first time I saw "nonce" used like this: https://www.slangsphere.com/what-is-nonce-in-british-slang/ https://www.slangsphere.com/what-is-nonce-in-british-slang/
- EPWN3D 10mo agoUnfortunately you cannot use static in array typedefs, which really blows. So you have to have an extra constant to keep track of the array size in order to use it. If it worked on typedefs, you could just make the array parameter the appropriate type and derive the array's count with sizeof(array_type_t).