11 ms·
I have never understood why the C community does not rally around what seems to me to be the obvious answer to the main problem with C: stop conflating pointers
by lisper 2y ago
I have never understood why the C community does not rally around what seems to me to be the obvious answer to the main problem with C: stop conflating pointers and arrays, or at least deprecate it. Make arrays a separate data type, not a synonym for a pointer, and require the system to track the size of arrays either at compile time or at run time (or both). Make a[x] be a bounds-checked array reference rather than a synonym for an unchecked dereference of a+x. It seems stupidly obvious to me that this is the Right Thing. Yes, it would be a non-backwards-compatible change, but the benefits seem to me to vastly outweigh the costs.
- elteto 2y agoBecause you can't do that and still call the resulting language C. It would be a different language incompatible will _all_ existing C code. Pointer decay is a fundamental mechanic in C. If you use arrays in C, and you pass said arrays as an argument to a function, then you are dealing with pointer decay.
- lisper 2y ago> Because you can't do that and still call the resulting language C. Says who? Non-backwards-compatible changes are made to language standards all the time. It's not pain-free, but neither is the status quo. Besides, who cares what the language is called? Change the name if that's what it takes, but stop conflating pointers and arrays. The cost of that has been literally billions of dollars in losses due to buffer overflows over the decades.
- usrnm 2y ago> The cost of that has been literally billions of dollars in losses due to buffer overflows over the decades. How much, do you think, would rewriting all existing C code cost?
- junon 2y agoNothing, if new standards are opted in via a #pragma.
- lisper 2y agoWhy do you think that would be necessary?
- usrnm 2y agoIt's the only way to really solve the problem. Simply creating a safer alternative won't help, they already exist. The real problem is the vast ocean of already existing critical unsafe code
- candiodari 2y agoThe whole reason C code is used is that it can be used for free. In other words, infinitely more than they're spending now. More than even the CCP is willing to spend to protect state secrets.
- colonwqbang 2y agoThere are many good "better C" alternatives already. Rust, zig, D. They address other common types of bug like resource leaks, overflows, use after free. If you're ok with rewriting code, you have terrific options. And of course there is C++, the most famous attempt to fix C in a somewhat compatible way by adding more features. It is debatable whether this effort resulted in a better language. C++ has all the bits needed to check array bounds by default but chooses not to do so... The problem is that the huge existing stock of C code is written in C and not rust, zig, D, etc. The same would be true for your proposed "better C" language and any other incompatible iteration of C. If you can come up with a way to add these guarantees to C without needing significant rewriting, I can assure you that most C programmers would be very interested.
- lisper 2y ago> There are many good "better C" alternatives already. Rust, zig, D. All of these are very different from C. What I'm proposing is just one small change to the existing C language. > If you can come up with a way to add these guarantees to C without needing significant rewriting, I can assure you that most C programmers would be very interested. I can pretty much guarantee that they would not because this is easy: phase in the changes. Start by turning array-pointer conflation into a mandatory warning rather than undefined behavior or whatever it is now. Then wait a few years. Then turn it into an error that you can muffle using -C2024 or whatever. I actually don't know whether array-pointer conflation is required by the standard or if it's undefined behavior (I'm pretty sure it's one or the other). But if it's the latter then you don't actually have to change the standard to make this happen, all you need to do is write a compiler that does the Right Thing. AFAIK no such compiler exists. But there is just no excuse for this: % gcc -v Apple clang version 14.0.0 (clang-1400.0.29.202) ... % more test.c int main () { int x[10]; return *(x+20); } % gcc -Wall test.c %
- retrac 2y agoIn assembly, arrays and structs reduce to base address, plus offset times scaling factor. C provides a thin veneer over that. Scaling derived from size of type. Offset is the array index. The basic programming model of C is to view memory as an array.
- mhh__ 2y agoI don't know how much C code actually uses it tbh. Most things just take a pointer. Wouldn't be easy by any means but it could be done at the scale of the Linux kernel if anyone cared enough.
- elteto 2y agoIf you use arrays in C you are using pointers and pointer decay. There’s not a lot of useful C code that doesn’t use arrays.
- mhh__ 2y agoThat's true but this is mostly the easy case where things decay in relatively trivial fashion as opposed to the "they're different but actually the same" aspect which is genuinely a bitch of change.
- LeFantome 2y agoI think that is part of his point. Most functions take a pointer, even if they are expecting an array.
- eru 2y agoThe Linux kernel already uses not standard C, but a dialect of C defined by the GCC flags they use. But if you are willing to use something-like-C-but-not-shackled-by-backwards-compatibility, then why stop at arrays and pointers? Just move all the way to D or Zig (or even Rust). These are all languages designed (partially) so that you can port an existing C system bit-by-bit over into them. Many people who can afford that, are doing that, of course. And that's why you don't really hear much about backwards incompatible developments for C. What would be the point?
- nine_k 2y agoOK, don't call it C, call it C[*]. It will accept any existing C program unmodified. With a per-file switch, say, #define __FEATURE_WEAK_ARRAYS, it will start to discriminate T* and T[], make it an error to mix them in assignments, or to pass one instead of the other if both the function definition and the function call are in files with this feature on. It will not complain about functions defined elsewhere. Then, say, #define __FEATURE_STRICT_ARRAYS the compiler will complain about mixing arrays and pointers as function arguments, no matter where the function is defined. It would require updated stdlib headers, for instance. Additionally, #define __FEATURE_MULTI_ARRAYS would enable syntax for fixed-size multidimensional arrays, Fortran-style. Now uint8[3][2][10] foo; would allocate 60 elements, and access to foo[1][2][3] would involve one memory dereference, not three. More support would be needed: sizeof, support for slices, safe array copies and length checks. Nothing extraordinary. Having this implemented would make a terrific master's degree work.
- pklausler 2y agoWhy do you think that foo[1][2][3] requires more than one memory reference?
- nine_k 2y agoAFAIK in regular C a T[][][] foo would be a the same as T ** foo, so it gets implemented as a pointer to an array of pointers to arrays of pointers, each pointing to contiguous allocations of multiple Ts, each not necessarily near the other. So you need three dereferences to get to an element. T value = foo[1][2][3]; // becomes: foo -> (T **) (T **) -> (T *) (T **) (T *) ... (T *) -> (T) (T *) (T) ... (T) (T) <-- This one! This allows for jagged arrays, yay! So useful. This is in stark contrast to a Fortran-style array, which is allocated as one contiguous piece, all dimensions folded up for linear access with one dereference.
- deleted 2y ago[deleted]
- 2y ago
- skissane 2y ago> Because you can't do that and still call the resulting language C You could if you introduced a new type of "safe array". e.g. int[] is a traditional C array which decays to int* int[@] is a "safe C array" which is syntactic sugar for struct { size_t __count; int* __items; }, and as such can't decay int[] and int[@] would not be directly interoperable, except by converting both to int* – maybe casting an int[@] to int* would automatically extract the __items member. (The [@] syntax was chosen at random, if you don't like it, pick something else.)
- pif 2y agoYou can define such a struct yourself. You do not need to modify the language.
- SpaghettiCthulu 2y agoBut your struct cannot provide an indexing operator (right?)
- adrianN 2y agoBy having it defined at the language level you increase the chances that your dependencies use the same struct.
- orf 2y agoSo why do we have so many issues with arrays then, since everyone can individually fix it themselves? Hmm…
- Xeamek 2y agoC programmers NO! You can't do that, that would break compatibility! also C programmers Wow, You are trying to compile this old program on new toolchain? Here is a list of 10k errors due to changed defaults flags for compilation, dependencies breaking by going from 9.4.1 to 9.4.2 and also, your code contains platform-specific extensions anyway. But C is portable guys! It really is!
- jeroenhd 2y agoBut that's the thing, code bases are full of non-standard C, but they never break because the C standard changes, only because their weird hacks are falling out of favour. Fixing a design flaw in C will break all existing code, instead of the code of the dozens of companies relying on weird hacks they wrote ten years ago. Once Zig becomes stable, I think it may have a chance of slowly fixing existing C code bases, by its virtue of being able to co-exist with existing code bases. On the other hand, a lot of C programmers don't want to change because they don't see a problem, so the presence of those will probably ruin any chance Zig has of improving the situation.
- samatman 2y agoOther than the pessimistic finish, my prediction is that this is exactly what will happen. The change will be a generational one. C will never entirely disappear, but over time, C code will be old code. People will occasionally write new C for what amounts to aesthetic reasons, and there will be a robust niche (doubtless well-paid) of maintainers of legacy C projects. All the kernels currently written in C will still exist, and still be composed mostly or entirely of C. But new work in embedded, drivers, implementation of programming languages, netork stacks, will be in Zig instead. With some Rust as well, but I figure we've seen enough of that arc to tentatively conclude that what Rust is displacing is mostly C++, not C. But when I say generational, I mean it. You're quite right that there is a legion of C programmers who like it and intend to stick with it. But they'll retire one day.
- jeroenhd 2y agoExcept for maybe in kernel projects, C code already is old code. As far as I can tell, (modern) C++ is the oldest common language for new projects. Rust is already part of the Linux kernel, being used for rather complex things like GPU drivers in Asahi. Android rewrote their Bluetooth stack into Rust and Windows is actively replacing existing operating system components with Rust as well. I don't think we'll need to wait for Zig to start replacing old C code, it's already being replaced with Rust now. I think Zig would've been a better choice for some parts, but it's taking too long to become stable for it to be included in large projects. Still, it may be useful for people working on embedded stuff, as embedded code seems to be stuck with 90s C when it comes to language support, if someone can manage to write a compiler backend for that specific embedded chip.
- LegionMammal978 2y agoIn fact, per the standard, a+x is already an array reference: you aren't allowed to go outside the bounds of the array immediately containing the pointee, on pain of UB. (Arrays do exist in the object model, and you can take pointers to them of type T (*)[N]; you just can't copy them around by value, and the name of an array decays to its first element pointer.) Compilers just typically don't track array bounds at runtime because of (a) performance and (b) big ABI incompatibilities. There's nothing in the language itself that stops them.
- lisper 2y ago> on pain of UB But this is exactly the problem. UB can be anything, including nothing. This should be at the very least an optional warning. But here is what happens with a fairly current C compiler: % gcc -v Apple clang version 14.0.0 (clang-1400.0.29.202) ... % more test.c int main () { int x[10]; return *(x+20); } % gcc -Wall test.c %
- fweimer 2y agoThese kinds of warnings need optimizations enabled. $ gcc -O2 -Wall -c t.c t.c: In function ‘main’: t.c:3:14: warning: array subscript 20 is outside array bounds of ‘int[10]’ [-Warray-bounds] 3 | return *(x+20); | ^~~~~~~ t.c:2:11: note: at offset 80 into object ‘x’ of size 40 2 | int x[10]; | ^ Not sure how to get Clang to warn. It clearly recognizes the undefined behavior.
- dzaima 2y agoClang appears to have -Warray-bounds-pointer-arithmetic for this, though not enabled on -Wall nor -Wextra. (fwiw, clang has -Weverything to turn on literally every warning, including conflicting ones, for finding what flags there are)
- lisper 2y agoYeah, Clang warns on x[20] but not *(x+20) even with -Wall and -O2. It's kinda weird.
- dzaima 2y agoFor the people who actually care about using C, there can be no unified concept of "array". Any single "the system" will just plain and simply be unusable in a good majority of situations. Some people will want to store the size in a type smaller than size_t (and potentially place it not adjacent to the data pointer for better packing in a struct; or perhaps even bit-pack it). Some will want to place the size relative to the data instead of the pointer (esp. flexible array members). Some will want to store half (or a third, etc) of the element count, the array being used as multiple back-to-back arrays. Then you'll have questions on pointers in the middle of an array, indexable by positive and negative indices. Never mind the pretty significantly increased register shuffling of having to pass the size across functions. For projects that are fine with doing things in the single way forced upon you and don't care about how things are done as long as it's safe, C is already rather clearly not the language to choose. C++ as-is can already pretty trivially allow adding bounds-checked array types, and compilers can even be configured to warn on all pointer indexing (https://godbolt.org/z/W8sqGW5sh https://godbolt.org/z/W8sqGW5sh), achieving the entirety of your proposal while not locking people into a single data structure. (granted, some may not want to expand to C++ "just" for one or two features (me included), but here allowing customizable array data structures is basically the only sane option, and C would have to take a rather significant amount from C++ to allow for such itself)
- lisper 2y ago> Some people will want to store the size in a type smaller than size_t So? My point here is that this should be the compiler's responsibility, not the programmer's. Why could not a compiler bitpack the same way -- or even better than -- a programmer could?
- dzaima 2y agoThe compiler cannot change the length field's size if a reference to the array (or a struct where it's contained) is ever passed to an unknown function, as that function has to be able to read the length from memory based just on the spelled-out type. Not a problem when passing the array by value (i.e. two registers of the data pointer and length), but then any packing automatically does not apply anyway.
- zzo38computer 2y agoArrays are a separate data type than pointers, although in many contexts you can use an array where a pointer is expected and it will work, and this feature is useful. Bounds-checking can sometimes be useful, and can perhaps have a switch to control its working. Some instruction sets (such as Flex) have tagged memory. In Flex, a pointer contains the address of a block (and a pointer can also be designated is read-only, disallowing writing through the pointer). There is also a "reference" consisting of a pointer and a displacement; I suppose this "reference" can be used to implement C pointers. If you use this, then the computer will automatically do bounds-checking and will result in an error if you make an access that is out of bounds. Tagged memory also allows to easily find the pointers in memory without needing to know the types of variables, which can be helpful for some uses, e.g. to detect improper use of realloc/free. Furthermore, null pointer can be zero (without the tag bits), which is automatically an error when used as a pointer because it is not a pointer.
- marmakoide 2y agoThat would break most C code handling hardware directly, like on MCUs
- lieks 2y agoC99 did that. void f(int len, char s[len]); void g(float m[static 16]); The first one is for variable-length arrays, the second one for fixed-length. TCC even implements array bounds-checking. But because a lot of people were stuck using C89 for several decades (due to old compilers), and the syntax isn't that great, nobody even knows they exist. Personally, I think Dennis Ritchie's[a] proposed syntax[b] was much better: void f(char s[?]); [a] The creator of C. [b] https://www.bell-labs.com/usr/dmr/www/vararray.html https://www.bell-labs.com/usr/dmr/www/vararray.html
- segfaultbuserr 2y ago> and the syntax isn't that great, nobody even knows they exist. Linux kernel maintainers certainly do - they even invented a non-standard extension of C99's VLA notation (their own invention, not implemented in GCC)... In newer versions of the package "Linux man-pages", most libc functions are documented using a non-standard extension of that syntax. For example, the prototype of memcpy() now reads [0]: void *memcpy(void dest[restrict .n], const void src[restrict .n], size_t n); It means this function accepts two arrays (dest[] and src[]), with n bytes of data type "void", src[] is read-only ("const"), and both src[] and dest[] are non-overlapping ("restrict"). Two non-standard notations are used here: a "void" array with elements of unknown type (not allowed in C99), also, ".n" means "a variable in the argument list, but defined after this variable" (which is not allowed in C99). The equivalent C99 definition would be something similar to: void *memcpy(size_t n, char dest[restrict n], const char src[restrict n]); As more people are exposed to these man pages, the C99 syntax hopefully will have more publicity. Finally, C23 interestingly states that: > 15. Application Programming Interfaces (APIs) should be self-documenting when possible. In particular, the order of parameters in function declarations should be arranged such that the size of an array appears before the array. The purpose is to allow Variable-Length Array (VLA) notation to be used. This not only makes the code's purpose clearer to human readers, but also makes static analysis easier. Any new APIs added to the Standard should take this into consideration. [0] https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/commit/man3/memcpy.3?id=c64cd13e002561c6802c6a1a1a8a640f034fea70 https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/... [1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2611.htm https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2611.htm
- pornel 2y agoThis article is 15 years old now, and nothing has changed: https://digitalmars.com/articles/C-biggest-mistake.html https://digitalmars.com/articles/C-biggest-mistake.html (and of course the problem wasn't new 15 years ago either.) It wasn't fixed then. It won't be fixed now. C is valued for not changing. C is valued for backwards compatibility with the most obscure platforms with unmaintained compilers. The C userbase is self-selected to like C exactly the way it is.
- pjmlp 2y agoThe irony is that C does change, we are at C23 now, but not in the ways that would actually improve its safety.
- deleted 2y ago[deleted]
- abtinf 2y agoAre you referencing this? https://www.digitalmars.com/articles/C-biggest-mistake.html https://www.digitalmars.com/articles/C-biggest-mistake.html
- lisper 2y agoYep.
- fijiaarone 2y agoThat’s like saying “the problem with aircraft is that they keep crashing when there is not enough lift under the wings, so the obvious answer is to get rid of wings”. Only, it would even be stupider if your grasp of programming was as great as your understanding of aerodynamics.
- lisper 2y agoIronic that you would pick that metaphor because I happen to have both a pilot's license and a Ph.D. in computer science. So I really snookered someone.
- flohofwoe 2y agoOnce you have "proper" arrays, you'll also need "array-references" e.g. fat pointers carrying a length (aka slices), and if you want to avoid unsafe pointer/array conversions you'll also need a typed allocator function, and all of that also requires a new stdlib and probably an extended ABI, or at least a standard for how the new types are layed out in memory, and passed into and out of functions. At that point the whole thing is so different from C that we should give it a new name - maybe, I dunno "Zig"? ;) PS: There was actually a quite recent bounds-checking proposal by (I think) Apple Clang folks that works with annotations and IMHO looks pretty good (in the sense of "I would actually use it in my libraries"): https://discourse.llvm.org/t/rfc-enforcing-bounds-safety-in-c-fbounds-safety/70854 https://discourse.llvm.org/t/rfc-enforcing-bounds-safety-in-...
- pjmlp 2y agoYes and even Dennis Ritchie failed to have WG14 caring about them. Not that his proposal was perfect, but WG14 didn't even bothered to improve upon it.
- pjmlp 2y agoBecause typing &myarray[0] is too much typing. /s Even Dennis Ritchie tried to add fat pointers, but failed to have the new C overlords care about his proposals. Despite what we might think about their feature set, it is quite relevant that Alef, Limbo and Go were designed by the UNIX folks, and all of them don't repeat the same mistake with arrays and strings.
- psychoslave 2y agoInteresting hint on Dennis Ritchie attempt. Here is a related thread: https://news.ycombinator.com/item?id=39677581 https://news.ycombinator.com/item?id=39677581
- zik 2y agoThis has been done quite a few times before, but the resulting languages are not C. Also saying that's "the main problem with C" seems to miss the mark. As a C programmer I wouldn't call this the crux of any particular problem. It's weird and problematic in multiple ways but surely not "the main problem".
- MaxBarraclough 2y agoSee also Walter Bright's 2009 article C's Biggest Mistake, on this topic. https://www.digitalmars.com/articles/C-biggest-mistake.html https://www.digitalmars.com/articles/C-biggest-mistake.html Recent discussion: https://news.ycombinator.com/item?id=40392371 https://news.ycombinator.com/item?id=40392371 Discussion thread about the article, 10 months ago: https://news.ycombinator.com/item?id=36564535 https://news.ycombinator.com/item?id=36564535
- vbezhenar 2y agoIs it main problem with C? I never had any issues with arrays. For me problems with C are as follows: 1. Inadequate standard with lots of unspecified behavior. I want behavior matching my architecture. Wrap my integers, always. I want my bit fields to be laid out in a predictable way, so I can actually use that feature to work with protocols rather than using shifts and bitwise operations. I must not research whether compiler will throw away my `while (true)` loop. Compiler must be predictable. 2. Bad standard library backed by the language standard (so I can't just throw it away, compiler will replace my loops with memcpy or will replace any standard library call with particular semantics). 3. Unexpected runtime cost. For example global variables are initialized to zero and that requires special code inserted before main. 4. Bad syntax. Switch must not allow fall-through, not by default anyway. `()` must be treated as function with zero arguments, not as a function with unspecified arguments. 5. Lack of namespaces and modules. I definitely don't want compiler to track array lengths. C is not Rust.
- JonChesterfield 2y ago1. Is what gets in my way the most when writing C. The whole undefined behaviour story is a wreck. Not hugely relevant but zero initialised globals don't imply code inserted before main. They get allocated at link time into a continuous blob which is zeroed by the loader. Non-zero values take up space in the binary but also involve no code generation. Global constructors, C++ style, usually do involve walking an array of function pointers before main though.
- pif 2y ago> I want behavior matching my architecture. Then use assembly. C is meant to abstract CPU architecture away.
- vbezhenar 2y agoUsing just assembly is not realistic. I'd love C-like language which compiles to assembly in an understandable way. I mean that optimizations are certainly useful and needed. I want to write `24 * 60 * 60` and expect for compiler to multiply those values at compile time. I want to write debug logging which could be disabled for release build and all relevant variables and functions be excluded. Basically there are reasonable optimizations which I would expect for compiler to do. Anyway it's not like there's a choice in reality. Manufacturer provides SDK and examples with C, using another language for most projects is not realistic.
- mattgreenrocks 2y agoConflation of arrays and pointers is only the tip of the iceberg. A big problem lies in the many varieties of undefined behavior and the relative ease one can invoke it, even when trying to avoid it.
- glitchc 2y agoAn outright ban would break a lot of existing code out there. Deprecation may not help, as people can simply turn off the guard if optional or use an older compiler if not. The problem with C is the massive mountain of legacy code that operates all of our computer systems. C is everywhere, in some form or other.
- pmalynin 2y agoMaybe not the C community at large, but companies / teams / projects that care about security have done this. For example, iBoot uses Firebloom which is basically C with fat pointers and bounds checking.