People will never move beyond C (among other reasons) because C allows precise control over memory allocation. Closures and precise control over memory allocation doesn't play very well together.
Urban myths by people frozen in time, systems programming languages predate C by a decade, and it isn't as if the world stopped outside Bell Labs in the 1970's.
Which systems (as in zero-runtime) language would you choose if not C (even if we disregard the fact that no SoC vendor support anything but C)?
- Ada? Many cool features, but that's also the problem - it's damn complicated.
- C++ combines simplicity of Ada with safety of C.
- Rust and Zig are only ~10 years old and haven't really stabilized yet. They also start to suffer from npm-like syndrome, which is much more problematic for a systems language.
- ATS? F#? Not all low-level stuff needs (or can afford) this level of assurance.
- Idris? Much nicer than ATS, but it's even younger than Rust and still a running target (and I'm not sure if zero-runtime support is there yet).
I mean, yes, C is missing tons of potentially useful features (syntactic macros, for one thing), but closures are not one of them.
Which functions are required for a C program to run aside from those that it run explicitly? memcpy? It's basically a missing CPU instruction. malloc will never get called unless you call it yourself.
> followed by history of systems programming languages, starting with JOVIAL in 1958.
All system languages (e. g. BLISS, PL/S, NEWT) designed as such before C was vendor-specific. Some of these had nice things missing from C, but none propagated beyond their original platform. And today we have no option but C.
> "Typescript for C" came out in 1983.
C++ is not just "not perfect", it is far worse in every way. Let's let people overload everything. And let's overload shift operator to do IO. And make every error message no less than 1000 lines, with actual error somewhere in the middle. Let's break union and struct type punning because screw you that's why. You say C macros are unreadable? Behold, template metaprogramming! C is not perfect, but it has the justification of being minimal. C++ is a huge garbage dumpster on fire.
Everything that runs before main(), and on exit, floating point emulation when needed and IEEE support, signal handlers, threading support since C11, at least, then depends on what else Compiler C has as extensions.
It is insane to assert that Rust hasn't "really stabilized yet" in comparison to C, a language which has been replaced twice (as C17 and C23) after Rust 1.0
C17 didn't introduce any changes to the language. C23 mostly standardized existing compiler-specific goodies. None of this broke existing code. When I run `sudo dnf upgrade gcc` I can be 99.9% sure my old code still compiles.
Compare e. g. https://github.com/rust-lang/rust/pull/102750/ (I'm not following Rust development closely, picked up the first one). Yes, developers do rely on non-guaranteed behavior, that's their nature. C would likely standardize old behavior. Basically all of the C standard is a promoted vendor-specific extension - for better or worse.
C17 ostensibly "didn't change" the language but it did still need to fix a bunch of stuff and it chose to do this by just issuing an entirely new language standards document.
C23 on the other hand landed a bunch of hard changes, and "it didn't break my code" only matches the reality most people observe with Rust too. The changes you mention didn't break my Rust either.
But some trivial C did break because C23 is a new language
In fact those Rust changes aren't language changes unlike C23, the first you linked is a choice for the compiler to improve on layout it didn't guarantee, anybody who was relying on that layout (and there were a few) was never promised this wouldn't change, next version, next week or indeed the next time they compiled the same code. You can even ask Rust's compiler to help light a fire under you on this by arbitrarily changing some layout between builds, so that stuff which depends on unpromised layout breaks earlier, reminding you to actually tell Rust e.g. repr(C) "I need the same layout guarantees for my type T as I'd get in C" or repr(transparent) "I need to ensure my type wrapper T has the same layout as the type I'm wrapping"
The second isn't a language change at all, it's a change to a library feature, so now we're down to "C isn't stable until the C library is forever unchanging" which hopefully you recognise as entirely silly. Needless to say nobody does that.
I'm trying to drag one program at $employer up to C99 (plus C11 _Generic), so I can then subsequently drag it to the bits of C23 which GCC 13 supports.
This all takes times, and having to convince colleagues during code reviews.
What C23 has done is authorise some of the extensions which GCC has had for some time as legitimate things (typeof, etc).
However the ability to adopt is also limited by what third party linters in use at $employer may also support.
True, at most I read excerpts when I have a question. Can you tell me what gave it away? I thought the saying is that C17 is the sane version of C11 and C23 has quite some changes, but is way to new to be counted on.
C17 is indeed a bug fix release. C23 finally removed some features that were deprecated a long time ago already in the first standardized version (auto as storage classifier, K&R function definitions, empty parenthesis as "arbitrary arguments") and also support for ones' complement. So yes, C is extremely backwards compatible.
> Closures and precise control over memory allocation doesn't play very well together.
How so? In C++ a lambda is just a regular type that does not allocate any memory by itself. You have in fact precise control over how/where a lambda is allocated.
True, but once a capture needs to survive the parent function scope you'll need to store it somewhere, either via a std::function object which has opaque rules on whether a heap allocation happens or via your own std::function implementation where you define the heap allocation rules but then will have to face discussions about why you're rewriting the stdlib ;)
Any C implementation of capturing lambdas has the same problem of course, that's why the whole idea doesn't really fit into the C language IMHO.
- Non-capturing isn't that much simpler to implement for the front-end, because they still need to distinguish between a file-scope name or a shadowing local in parent function. And once you have the code to do that distinction, it's not that far away from implementing captures.
- From implementor point of view I just don't like the idea of a new category of function qualifiers, so I used "_Wideof(int(void))" over "int(void) _Wide", and "int fn({...}, void)" over "int fn(void) _Closure/_Capture(...)".
- During prototype scope of the inner function, parameters may have VM type that reference from parent's local scope, so the capturing (or not) effectively start at prototype scope, earlier than C++'s model. My syntax has capture clause right before parameters so it is somewhat manage-able in one pass; for function qualifier syntax the parser probably need to either delay semantic checking the VMs or skip ahead to find _Closure/_Capture before processing the parameters.
- Both N3654-example6 and N3694-4.4.6 implicitly create capture context on stack then provide reference to it (N3654 with &_Closure(), N3694 with inner function's name). My version is lower level in that only the type of capture context (_Ctxof(fn)) is implicitly created, users then call a helper (modeled after va_start) to initialize the context object. I believe it also reduces surprises from by-value captures, since users clearly see where they initialize a context object relative to local variable changes.
- Sans syntax, N3654's "specified argument" is pretty close to my implementation, just need to change the source of context pointer from r10 to the argument in function prologue.
- I implemented wide function pointers as generalized "function pointers with context pointer pushed in r10", so it's also possible to make wide pointers from regular functions, they'll just be called with an extra nullptr in r10. I think it's more flexible this way.
What I did was mostly "the compiler will need these anyways" so should be adaptable to whatever WG14 settled on, or if I ever want to implement C++11 lambda.
Thanks! What I do not quite understand is the need for a capture clause that lists the captured variables. As a user, I do not want to list my captures. As a code reviewer I certainly do not want value capture that create a modifiable copy with the same name, or any ambiguity on whether something is a copy or not. I just want to access my variables. So what is really the point here?
Regarding "also possible to make wide pointers from regular functions", this exactly the reason why I like the qualifer version: The usual conversion rules for qualifiers would allow this conversion implicitly, while back-conversion is not allowed (or only with cast).
Anectodal and probably my bubble, but quite a few people had moved from C to C++ long time ago, but then got fed up with where modern C++ is heading (mostly around C++14 and C++17) and went back to C.
> Except it doesn't offer the safety alternatives that C++ has, thus it is a big unsafe bunch of code.
C++ adds more new unsafe concepts on top of C than C ever had though ;) (see std::view, std::range or good old iterator invalidation - at least in C the unsafety is in your face and not hidden under layers of stdlib code)
Nah - more that a lot of commercial code is written in it; and it doesn't make sense to replace (or rewrite) it at this time.
For example, I'm maintaining some 20 year old C code, which the employer adopted around 10 years ago. It will likely stay in use at least until the current product is replaced, whenever that may be.
Let me rephrase that: I feel myself addressed by "some people will never move beyond C, no matter what" and I prefer C over C++, because it is a much simpler language. Each time I am leaving the cozy world of C and write C++ I am annoyed and miss things.