The Development of the C Language (1993)
bell-labs.com
bell-labs.com
Editorialized title: “C is quirky, flawed, and an enormous success” – Dennis Ritchie
HN Guideline:
> [...] please use the original title, unless it is misleading or linkbait; don't editorialize.
embedded linux pay is fine but not great, not sure how it compares to full stack jobs.
full stack at least is more remote friendly as it does not need deal with hardware hands on which is not remote friendly, and, things made by full stack is potentially more scalable.
Neither side is objectively better or worse, but having experience in both has changed how I approach problems.
Software development is a very segmented world. In the various social circles I'm connected to, I know devs who were thrilled to finally be making 6 figures 7 years out of college (in line-of-business software in a regional hub) and devs who were disappointed not to have crossed 400 k$/yr in that same time span (at FAANG in the bay area).
The kernel has a lot of it's data structures and functions so my work revolves around using that instead of the built-ins.
You would appreciate that webapp buttom.
I’m not sure how I would begin making a career transfer. Would anyone happen to have any advice / experience on this? I would be really grateful!
(based in UK if that helps)
First, study C and/or Rust on your own. Maybe do a personal project or two.
Second, find something at work where Rust or C would bring some benefits. Tell your boss that you think this would work better in a language like Rust or C, and explain why. Volunteer to try to do it. (Note: Appearing too eager at this point might be a mistake.)
Do the second step a few times and you become the Rust/C expert. And you get paid as you do the work that helps you get better!
As for the second point, that is a great suggestion. However, I'm very limited by my current working environment (regulation, corp. restrictions, etc.) so it becomes a little more difficult.
I believe I will just have to push very hard for option one and continue to study areas of interest in my spare time. I'm reading xv6: a simple, Unix-like teaching operating system which is helping me grasp some practical applications using C.
If OP plays it wrong they can also look like a tonedeaf dunce. Definitely try to read the room.
That is how I see it. Others may see it differently.
I don't think I would have appreciated it at different times in my career, but for me, right now, I'm loving every minute of it.
The biggest issue I've faced (beyond the obvious issues of getting anything to work at all), is how to organize the concepts.
The "Data-Oriented Design" folks have had a huge impact on that. Specifically, talks from Andrew Kelley, Mike Acton and the book by R. Fabian.
The second thing is registers. Just toss the HAL mess, pick up the Reference Manual and start poking registers. It's so much more enjoyable (and reliable) for firmware work.
I don't know about job opportunities as I'm running my own hardware business, but if you're feeling pulled in this direction, I highly recommend taking a closer look.
The Development of the C Language (2003) - https://news.ycombinator.com/item?id=19338525 - March 2019 (10 comments)
The Development of the C Language - https://news.ycombinator.com/item?id=15134903 - Aug 2017 (22 comments)
The Development of the C Language* by Dennis Ritchie (1996) - https://news.ycombinator.com/item?id=11973627 - June 2016 (1 comment)
The Development of the C Language (1993) - https://news.ycombinator.com/item?id=10749358 - Dec 2015 (28 comments)
The Development of the C Language - https://news.ycombinator.com/item?id=3439843 - Jan 2012 (1 comment)
The Development of the C Language - https://news.ycombinator.com/item?id=2258287 - Feb 2011 (7 comments)
The Development of the C Language - https://news.ycombinator.com/item?id=726519 - July 2009 (1 comment)
The Development of the C Language (Dennis Ritchie) - https://news.ycombinator.com/item?id=365080 - Nov 2008 (1 comment)
Since 1979! And people keep complaining about being forced to use static analysis in C, on build pipelines.
"I know better", yeah, sure.
i will never understand why C programmers get so upset about C++. of course, the latest revisions of C introduce some new features not in C++, but nothing really major. if you want good type checking, compile your C code with C++, and fix all the type errors you will get.
Likewise, Microsoft introduced windowsx.h header file, to improve type safety while using C for Windows 3.x applications.
takes me back. i thought it was crap. i used to work at The Instruction Set (one of UK's biggest tech training companies at the time) and everyone hated the Windows/C course based on Petzold. my boss came up to me (somehow I was the windows guy in a unix company) and said "we need a new windows course" and me said "OK, i need a framemaker license and to work at home for a week" - worked out great.
this was early 90s, i suppose?
I mostly programmed on Windows 3.x with TPW and TC++, alongside OWL. Those nice Borland manuals.
For me, in what concerns C programming for Windows 3.x, the "Programmer's introduction to Windows 3.1" was a much better book.
Mainly due to its coverage of windowsx and message macros.
It's on the Internet Archive.
how do you know that?
So what? And GCC allegedly has been C++ for many years. Please take a look at the repo and tell me why this means anything (besides language wars being a waste of time).
My personal experience with C++ is that I seem to always end up peeling off my nice abstractions again later. Most of what it offers hasn't stuck for me, at least for systems programming. There's a lot of bad C code I've had to work on over the years, but overzealously architected C++ codebases take the crown for inflicting the most pain for sure. One recent experience was when I replaced 4 files and 200 lines of C++ classes with 4 lines of straight C code. Not even a function was necessary. And that was one of the less bad experiences because it was actually possible to fix.
In my most recent attempt to be open-minded about it I've ended up keeping a few short methods, which can be nice for code brevity at the call site, and there is less of a tax about having to come up with naming schemes. But I have otherwise found classes (and in particular methods) to be painful for two reasons: All the procedures operating on the class have to be declared in the class (or as static methods in a friend class, but then they have to be declared there). This includes private methods and is just one more level of annoyance for a small (and debatable) syntactic convenience. It's pretty f***ing bad to have to turn implementation details to the outside (it extends transitively to implementation types used in your private methods etc.), and that is a big reason why C++ projects have infamously longer compile times compared to C projects.
Another problem with methods is that it seems you can't define them as having "static" linkage, at least not with MSVC. I suppose this can increase link times and prevent the compiler from making some optimizations.
One other thing I did was trying to buy more in to RAII, for example doing ref-counting in an automated way. It's another area where I feel I've lost a lot of control over what happens (it's hard to get it right), and my codebase is slowly deteriorating.
Another big problem that I personally see is implicit "this". C++ would already be a much better language without this. It's a bad tradeoff IMO (and Python was right to make it explicit), I can't see a benefit of not typing "this->". It is misleading while reading, and frequently having to change method parameter names just to be able to access both is a real annoyance. (From which code style rules like "m_" prefix arise, which typically don't get followed 100% -- so you'll see locals with m_ and members without it -- and which add two more characters, making the implicit this even more useless).
After a few months I am back to writing simple structs with none of this counter-productive (at least for small teams) access protection, and simple plain functions.
That's only scratching the surface of C++ (it's only about "C with classes" so far). I do know "modern C++" to a degree, and when digging deeper into the trends from the last 1-2 decades it gets much worse. I've been following what the C++ committee is up to these days and it seems they are stubbornly penny-wise but pound-foolish. One recent example -- they have now improved the type inference of "this" !!with an added new syntax!! [0] so you can have "easier" CRTP patterns. Does anyone but the most extreme freaks still understand what actually happens there or is the C++ audience mostly an army of copy&paste coders?
As a proficient C programmer, it's so much easier to be annoyed about most of C++'s features, because they break so quickly when put under stress, and just being a bit more explicit with C-style code seems to often lead to more maintainable code and actually not that much more code, sometimes even less (not having to deal with all the crazy abstractions).
That all said, throwing in the occasional templated function or class for good measure can be incredibly powerful, much better than dozens or hundreds of lines of C macro generator hacks. It can be useful also when working with IDEs. But it's a slippery slope and mastering it is hard.
[0] https://www.sandordargo.com/blog/2022/02/16/deducing-this-cp... . There is also a youtube video somewhere.
i learned assembler and fortran, then abandoned them for C, and then abondoned C for C++. i did all that progression because it self-evidentially made me more productive.
The thing about C++ is that many of its features start with a good intention, but most of them are so specific that they have to go down one almost arbitrary route (non-orthogonal decisions, like enum class introducing at least 3 changes at once) and pessimize the other use cases. Good luck refactoring your codebase when you realize you have to change your approach and it's no longer supported by any kind of specialized syntax. That's a problem that C mostly doesn't have -- most of its features are needed when programming a computer, and they're minimal and orthogonal, with few ways to paint yourself in a corner.
changes to what? they don't clash with the original horrible enums at all - all your code that used original C-style enums will still work.
The mere existence of all those features costs a lot of time just to understand and navigate them. They can diminish productivity. C++ is a huge language. It has tons of features that suck up a lot of time until you understand the space where the features can be used to good effect.
And "good effect" often means just writing the same thing in fewer characters, or a little more type-safe (which often wouldn't be needed if the design were sane).
And if the requirements slightly change and that good feature breaks, enjoy your rewrite! Or add another set of abstractions or even macros to work around the breakage -- like MS did in case of enum class bitwise operators for example.
That's why many people restrict themselves to C entirely -- no time to waste on finding out why C++ features X, Y, and Z all don't work for the given problem. Time could be better spent than with obsessing over all the ways in which an arbitrary set of features can be abused to write the implementation in fewer lines of code, meanwhile making it harder to read & write. Just think about the algorithm & the data layout, and bang out the code.
You're most likely not aware (as most C++ coders unfortunately), but this advice is useless today since it's not possible to compile current-time C code with a C++ compiler, the two languages have diverged too much since around the mid-90's.
A C++ compiler only accepts a "common C/C++ subset", but this subset hasn't been updated to include C features that had been added after ca 1995.
A better advice is to simply use the highest warning level and enable warnings-as-errors, this gives you mostly the same type checking as in C++ (minus the void* conversions, but that's how it should be since a void* is basically an "any*").
VC++ is actually quite good, has SAL, /analyse and SFIR. Also much better than many other compilers, when looking beyond the big three.
Implicit conversations is a common source of errors. Certainly a nice feature for pentesting.
C is full of quirks. From cryptic "undefined behaviors" to a type system that isn't really a type system (more like "size hints for the compiler"), the language doesn't feel easy to use/debug. Add to this CPP macros, a universally recognized bad idea, a clunky import system, and lack of a single reference implementation of the compiler/libC, and you have a language that is harsh to defend.
Also, documentation is all over the place. If a function isn't described in `man`, I have no idea where else to actually look for it.
I used to think "C presents the most honest representation of the low-level mechanisms of the computer", but... even this is shaky. I've been programming for almost 15 years now, and I don't think I've ever seen a computer where memory is actually a continuous array of bits sorted by memory address. The C representation of memory (and all the pointer arithmetic) is not a real representation of your hardware, and this too is an abstraction.
So, setting aside the need to maintain 30+ year old code, what would be modern reasons to start a new project in C?
well, that is not the C memory model. C does not allow you to access bits in memory directly. maybe you meant bytes? or words? if so, many cpus have exactly that architecture.
of course it does what are you talking about?
https://developer.arm.com/documentation/100165/0201/Programm...
C makes no assumptions about the size of a byte
C does not support bit-addressing.
If you write a library in C, it can be easily exposed to a variety of high-level languages and platforms.
You might argue this is more a property of the C ABI than of C itself, but unless the project is large enough that it's worth doing it in C++ or Rust instead, it's still a very reasonable choice.
Also not everything is web. Sure, if you're writing API endpoints in C you're just shooting yourself in the foot, just use Python or Ruby or Go and call it a day. For things like embedded it's often your only reasonable choice.
I really hate how for microcontrollers the only two choices are either C++ or Micropython, I mean how about some fucking middle ground instead of two polar opposites? At least eventually everything will be rewritten in Rust I guess.
There's TinyGo as well. https://tinygo.org/
I'd say that's the middle ground for me.
Seems to do exactly what 99% of people need.
Operating systems written in Pascal are now obsolete. OSs in C are not.
Perl is much easier to replace because fewer things were dependent on it however even here Perl 5.x still pops up all over the place.
Run "file /usr/bin/* | grep -i perl | wc -l" on your computer. You will be surprised.
EDIT: if you want a histogram for all the types of programs in your system, run this
file -bL /usr/bin/* | cut -d' ' -f1-3 | sort | uniq -c | sortAn embedded Rust project is the same as a normal Rust project except that you mark it as not linking the standard library !#[no_std] and you define a main entry point and panic behaviour (there are helper crates for this).
You can still use the core and alloc crates which give you pretty much everything you need in an embedded system like strings and vectors. You also get to use modern tooling like vs code and rust-analyser instead of a different antiquated version of Eclipse for each hardware vendor.
I don’t think that Rust should only be used for big projects. You can use it for small projects and you really don’t need to get complicated with generics for application code. You need to put in the effort to get a fundamental understanding about what the borrow checker is trying to achieve and the rest may be easier than you think.
Why wouldn't you just use C for programming a microcontroller? Sure, it's not a great language for web backends, but microcontrollers are where it shines. You're probably not deploying 100,000 lines to a microcontroller for a personal project, so the lack of certain abstractions isn't going to be that painful. On the other hand, C lets you make the latency and memory usage 100% predictable, which can be a great asset.
Because as the OP states, it's an objectively (pun intended) terribly abstracted language. There is nothing 100% predictable about C except that you'll eventually get screwed because you didn't account for some random obscure thing that should never have even been possible to do. Any language that allows using static variables can have predictable memory consumption. There is nothing inherent to it that makes it better than a language that works at the same level but built to modern standards, except the piles upon piles of legacy code you can use.
C did a ton of things right, but it also did a ton of things wrong. Learning from that and moving on would be the sensible thing to do after 50 years.
Zig looks perfect so far, but it's too early to switch over yet.
Any other promising candidates?
Even just requiring users to integrate my hypophetical Ada library source distribution into their project's build system files would most likely drown me in support tickets ;)
Or D itself if you don't need a language as minimal as C. D is basically C++ redesigned and now that GCC includes D support by default I wonder whether it'll gain popularity.
PS: my main use of C is currently to write platform abstraction libraries with minimal size and runtime overhead, so need to talk directly to operating system APIs, plus WASM is a very important target. The libraries must be usable from other languages via automatic bindings generation (quite simple with a C API). Also for performance-oriented stuff, direct control over memory layout and lifetimes please.
Also personal opinion from 20 years of C++ experience: high level abstractions never pay off in the long run. Simple imperative code always wins when it comes to "malleability".
You're really exaggerating the problems. Does your negative opinion of C come from experience, or did you listen to the Rust evangelists who have an incentive to make the difficulty appear bigger than it is? Because it hasn't been my experience that C is this huge minefield of bugs that are impossible to explain or debug. You prevent a lot of bugs by actually understanding the language instead of coding by trial-and-error, the remaining bugs usually get caught quickly if you use an advanced compiler like GCC or Clang with the right flags (warnings and sanitizers), and for the occasional bug that slips through, the debugger tends to be helpful.
It's true that C has a bunch of historical footguns like gets and strcpy that you need to avoid. It's a very bad language to learn by trying random things and seeing what works. However, it's possible for a "mere mortal" to write good code. You just need to do more up-front learning than you could get away with in e.g. Python. If you pick a good book and listen to experienced programmers, they will tell you what to do and what to avoid.
And regarding abstraction—you can go very far with just structs and pointers, but you have to do things the C way rather than trying to write Java in C. If it's enough for Linux devs and their millions of lines of code, it will be enough for your personal microcontroller projects.
There is a very promising contender in the low level space that aims to fix some of C's problems, it's a new language called Zig. However, it's at a pretty early stage; even if it catches on, it will be many years from now. Right now, if you want to do low level work, you'll benefit from becoming good at C.
This has been the exact opposite of my experience. I’ve been writing C for 10 years and have yet to find a piece of code where I was surprised at what it did. That’s one thing I love about C, is it is entirely predictable. If it isn’t, my code is wrong. The language is rigorously specified. It is not hard to avoid undefined behavior.
Contrast that with languages like C++ or Python which hide gotchas all over the place. In Python, one cannot even rely on a variable being a certain type, and if it isn’t, the program explodes. C++ allows plus to not be the inverse to minus, allows for hidden custom memory allocators (overloading the new operator). Template metaprogramming is borderline sorcery past the simplest of use cases. C++’s interoperability with C is an accident waiting to happen with all the reallocations which can occur without the user being aware.
C lays flat out in front of the programmer all the unpredictable behavior that many other languages implement behind the programmer’s back. Sometimes that’s not desirable, and sometimes it is.
The thing is, I'm not convinced avoiding UB is easy. E.g. what's the behavior of the following code?
int16_t a = 20000;
int16_t b = a + a;Integer overflow will result, I’m pretty sure. The largest value a signed 16 bit (so, 15 bit) can hold is 32767, IIRC.
I can see where that’s unexpected for people whose brains aren’t wired in powers of 2. This is one area where I think Rust improves upon C, with its availability of overflow detection in arithmetic. It’s unfortunately verbose, but it enables greater safety.
Luckily most other languages haven't decided to copy C's implicit promotion rules & target-dependant integer sizes.
This is the new "Year of the Linux Desktop".
Yes. Unless somebody steps up and rewrites everything in Rust or Lisp or whatever, that's exactly what's going to happen. Lack of backwards compatibility with existing software will condemn programming languages to irrelevance on day one.
Are you sure about that? ChatGPT doesn't understand C. It wouldn't even have enough context to reason about UB even if it understood UB.
I look forward to a raft of CVEs over the next decade where ChatGPT is a root cause...
With ChatGPT you get exactly what it gives you which must be trusted as a source of truth. That's bad.
ChromeOS doesn't speak C, unless you mean shipping WASM libraries. (Not every Chromebook supports exposing the Linux environment).
iOS and Android, kind of speak C, but not if you care to actually ship an app.
It's not so bad. Manipulating strings is a pain in the ass so everything becomes a char and managing types is so annoying, especially dealing functions that could easily take an int or float, you either have to make a template or different versions of the function for each type. This makes me appreciate dynamically typed languages a lot. Those two issues are the only problems I seem to have, everything else has been easy and breezy
Besides those two things it's pretty nice. My code is a bit verbose because I'm not that great at it but I'm sure I could reduce the lines of code in my projects (the biggest one has 4000+ lines of code, but it does a lot) by using structs and more loops, but that's mostly a skill/experience issue.
By and large memory is a contiguous array and the C representation closely matches what is actually happening, so I am curious about which platforms you have worked on.
It really doesn't, though. Although your CPU might present system RAM as one contiguous array of bytes to your program, the C compiler follows different rules – see strict aliasing and other pointer dereference rules. For example, the following is Undefined Behavior and your C compiler may or may not generate the assembly you expect:
int x = *(int *)0x1234568;
Your CPU would happily execute the equivalent machine instructions and load from address 0x12345678, while a C compiler is free to replace your entire program with return 0;and most likely seg fault, or similar
2. If the CPU has an MMU, it won't segfault if the address is mapped to an accessible region of memory.
3. This is besides the point, because the CPU will execute the instruction and attempt to load from that address. A C compiler might emit the load instruction, or it might assume that this code branch will never be executed and can therefore be replaced with code that sends an angry email to your mother.
mov rax, qword ptr [0x12345678]
do what you think it would/should.And as for the C memory model, aliasing, and optimizations, I'm firmly in the camp that thinks the standards originally gave the compiler writers an inch to work on weird platforms and they've taken a mile when they work on reasonable ones. The intent of your integer to pointer cast is very clear, but it's been undefined to insanity. So now there is some variant of the following, which doesn't have UB but does the exact same thing less clearly:
uintptr_t i = 0x12345678;
int* p = 0;
memcpy(&p, &i, sizeof(int*));
int x = *p;
I'm sure some language lawyer will correct me on some obscure detail of the standard, but it could be fixed with some modification. The point to me is that using memcpy instead of pointer casts is NOT an improvement. The good compilers will generate the same code as the assembly above, so all they've done is made the C source less readable.The improvement comes when there are multiple accesses that could potentially point to the same memory. Consider a silly function:
void f(int16_t* a, int32_t* b) {
for (int32_t i = 0; i < 100; i++) {
b[i] = a[0] + i;
}
}
If type-based alias analysis is enabled, then the compiler can assume that a[0] does not alias b[i] because they are different pointer types. So it can hoist the load of a[0] outside the loop, improving efficiency. If strict aliasing is disabled, it cannot assume this, so it must reload a[0] each time: https://godbolt.org/z/E7jxfYsbxThe memcpy() makes it clear that the memory could alias anything, so it will generate the less efficient code even if strict aliasing is enabled: https://godbolt.org/z/KoPxK9fPj
Memory aliasing is a huge thorn in the side of the optimizer, because the compiler frequently has to allow for the possibility that different pointers will alias each other, even if they never will in practice. The code might end up being slower than necessary for no real reason. Strict aliasing is one of the few tools we have to tell the compiler that aliasing will not occur.
I don't think that C actually forbids this code:
*(int*)0x12345678
The rule is just: if you access it as an int, you have to consistently access as an int. You can't mix types from one access to the next, eg: *(long*)0x12345678
*(int*)0x12345678I can see the argument, but there's a much better way to indicate what you want with your example:
void f(int16_t* a, int32_t* b) {
const int16_t a0 = a[0];
for (int32_t i = 0; i < 100; i++) {
b[i] = a0 + i;
}
}
Now a clean (well defined) compiler could do what you asked.I've seen other people suggest that UB is a mechanism to have these magical backdoor conversations with the compiler to express optimization opportunities. I think that's absurd and reckless. Propose adding assertions or "declare" statements instead, and quit thinking of interpretive dance through a minefield as a method of communication.
*(int*)0x12345678
If not, give it time. It was only a few years ago when you were allowed to use a union for that kind of thing. I really believe they'll eventually make everything except unsigned integers be UB."Oh, the code was never correct. You just got lucky before."
Your point that C is stricter than asm of course still stands.
And every sane implementation does what everyone expects because its how memory mapped IO works (but you probably want a volatile in there and maybe a compiler or memory barrier as well depending on what the hardware guarantees about the access patterns for that particular range of addresses)
You're right, that was a bad example. Here's a better one:
int x, y;
ptrdiff_t diff = &x - &y;
This is Undefined Behavior, because &x and &y don't point to the same object.- CPU memory subsystems are very complex these days and represent a lot of shared mutable micro-architectural state, which makes it hard to reason about. That's not linear and the C language does not offer concepts which represent that complexity. Short of some prefetching intrinsics.
- Pretty much all memory will be virtually addressed, pushing you even further from the concept of flat linear memory.
- Pointer provenance [0] binds memory to types and allocations which doesn't map onto the concept of linear memory and a pointer is just an offset.
There are couple of arguments like that floating around and it just doesn't make a whole lot of sense. The C model is in fact a usable abstraction (and easy enough to peel off when required), otherwise it wouldn't have stuck around for so long. No amount of "network effects" and "free beer" arguments can discuss this away.
There is an argument that instruction sets might have developped a linear address space abstraction because of C, but I doubt it. Binding the IR closer to a specific physical layout would be very bad for portability and longevity of the code.
A lot of the UB the people grouse about can generally be ignored because 99% of the platforms out there have the same behavior in areas where the standard is extra permissive for obsolete exotic hardware. Tagged memmory is dead, 1's complement is dead, big-endian is mostly dead. All the UB associated with them is not that relevant most of the time. The downside is that people write code that takes a lot of liberties assuming behavior that the standard doesn't guarantee. A common one is unaligned access because x86 has always been permissive about it and it took until C11 to have power tools needed to manage it in the language.
The fact that many C developers keep confusing it with implementation dependent behavior gives me no confidence on their other opinions about the language.
Sure, but UB exists so that compilers can generally do whatever's fastest on that particular architecture.
Your signed add instruction traps? Great, do that. Does one on a different architecture overflow? Fine. Just emit it and it's conformant.
(Disclaimer: I really don't know all the details of the C standards, am not a language lawyer but know enough about the language to feel quite productive in it. Please fill me in or correct me where I'm wrong).
I considered rewriting it in Go a couple of times but just didn’t see the point.
I tried Rust at first but went back to C when I realized I was spending more time appeasing Rust than solving the actual problem, which wasn't really complicated enough to gain significant benefit from Rust's features.
It's the simplicity and power of C that I find attractive. I don't write it professionally at the moment, but I enjoy it. It's obviously not the right tool for the job most of the time for the reasons you give, but I miss its elegance.
I am a big fan of rust, but it's massive compared to C. I'd like to explore Zig some day.
1. C is relatively simple. Sure, not as simple as it could be (e.g. compared to something like Oberon-07) but in the grand scheme of language things, it is far on the simpler side of the spectrum. I can write a C parser relatively easy if i want to for example (and at some point years ago i did that to transpile a C project to C# to run under Sony's PSM platform that was based on Mono and allowed only C#).
2. Undefined behavior is annoying as it can break previously working code with newer versions of the same compiler (though language lawyers playing word games like the code already being broken are way more annoying - the code did the thing i wanted previously so as far as i am concerned it was not broken), but this is something that aside from "obvious" things (accessing invalid memory) i can probably count in my fingers the times i encountered in practice (i write "probably" because right now i can't remember any case, but i've being writing C for more than 20 years). Valgrind and Ubsan help with these so they are not much of a practical concern.
3. I find CPP macros to actually be very useful and a feature that a) i'd actually like expanded instead of being stuck in the 80s (let me store some state or have a loop, FFS) and b) were available on languages too (Free Pascal is a language i also use and does have some C-like macro support, which is more than what you'd find in other languages but still not to the same extent as C). D's mixins essentially being ubermacros are a thing that i liked with that language but sadly their stance on breaking things is something that kept me away from it.
4. A C compiler is available on pretty much everything that can compute things - or at least on pretty much everything i might think on targeting with C anyway (and chances are there are multiple C compilers instead of just one). If not, i can probably write a compiler myself - it'd be rather simple and not that great but i'd be more likely to finish it than a compiler for some other language.
4b. Very related, so it gets a "4b" instead of 5 :-P, but there are a bunch of IDEs and editors that "understand" C. I like IDEs, i like syntax completion, i like semantic highlighting, i like being able to easily rename an identifier, etc and C being easy to parse (see #2) means it has a lot of those. Let me correct that, i don't "like" IDEs, i love IDEs.
5. Most modern computers might not technically be like how C presents them to be, but they're close enough where any differences only matter if you're trying to perform microoptimizations to your microoptimizations - at which point you'd most likely be using a combination of compiler-specific heuristics and assembly code anyway.
6. In most systems where that'd be a concern, the C ABI is pretty much stable or at least there is a stable C ABI, allowing any code written in C to be usable by other languages as well as shared libraries to be able to expose an ABI that will remain backwards compatible and usable by other languages. Of course other languages can do that but they pretty much always do it through a C-fication of their APIs.
7. C compilers - even those that perform a dangerous (see #2) number of optimizations - tend to be very fast. I hate waiting the computer to finish doing things so i tend to prefer languages with fast compilers.
8. While i don't (always) need to maintain 30+ year old code, i do have existing C code that (seemingly, see #2) works and i don't see a reason to waste time rewriting that code in some other language. Even if it'd be broken chances are it'll be faster to fix it than rewrite it.
9. I am comfortable with C. For me being comfortable with a language important because it lets me focus on the thing i'm trying to use the language for instead of the language itself.
There might be other stuff i forgot, but the above should give you an idea why i personally write C. Though note that i don't see as any sort of perfect language, there are a lot of things i'd like it to do better - including the type system you mentioned as well as the compile-time code evaluation i wrote above, be it via CPP or by some other means - but it is good enough.
[0]: https://discourse.llvm.org/t/rfc-new-preprocessor-macro-dire...
This is arguably already possible, and was possible even before c99 added variadic macros. Although the code is a bit cumbersome to write.
It isn't Turing complete, because it will always terminate, but you can make the execution time (number of execution steps) arbitrary large exponential in respect to the number of source lines.
There are a few libraries that implement that.
https://github.com/rofl0r/chaos-pp: Quite high level implementation, that supports arbitrary precision decimal base arithmetic.
https://github.com/camel-cdr/boline: Mine implements 8/16/32/64 bit arithmetic, and low level control flow.
You can very often get away with using unary numbers and/or constant expressions to work around the limitations without needing a library.
Got any problem in mind? I've got some time on my hands to problem solve.
Well, i wrote "supposedly" because i didn't try it myself but found a post[0] that claims it is. The example is even about making loops.
But the point is that these aren't only way too hacky but also slow down compilation. I did use some of my own preprocessor hacks when i wanted to do some fancy stuff with it at the past to implement an RTTI system that allowed automatic serialization of structs with nesting and references and while it worked (x-macros FTW), it was cumbersome and slowed down compilation so much that at the end i found it both much simpler and faster (in compilation time) to replace a ton of preprocessor macros with a code generator and a couple of #includes that included the generated code.
[0] https://stackoverflow.com/questions/3136686/is-the-c99-prepr...
I don't think is not a bad idea. You can't solve language incompatibilities in the language it self. Textual macro languages solves this nicely.
CPP is what makes C and C++ work for projects aimed at multiple platforms or compiler vendors.
1. Multiple implementations providing a unified interface, selected by the build system. Aka the Henry Spencer approach: <https://www.usenix.org/legacy/publications/library/proceedin...>
2. Less-bad macros, e.g. cond-expand: <https://weinholt.se/articles/cond-expand-and-ifdef/>
Many of those could be solved by some other means, but C macros neatly encompass all of those.
It's not really that cryptic (aside from like strict aliasing, but -fno-strict-aliasing). There's some UB that might be considered unnecessary/too strict, but it still makes sense in its own right, and, if understood, is quite powerful, and leads to a bunch of neat optimizations.
> the language doesn't feel easy to use/debug
If debugging at the assembly level, stepping by instructions, it's actually quite nice (despite what everyone says about it not mapping well to hardware, in my experience there's still a pretty clear & immediately obvious correspondence between each C thing and assembly subsection, and vice versa)
> CPP macros, a universally recognized bad idea
I don't know, they're quite neat for things I have to do. Sure, a turing-complete compile-time language would be nice (I'm not saying that sarcastically, I even use a DSL for writing SIMD that is exactly that!), but it'd add a ton of complexity to mapping C source to assembly.
> Also, documentation is all over the place. If a function isn't described in `man`, I have no idea where else to actually look for it.
Use of the standard library grows less and less significant as the size of the C project grows. Besides that, cppreference.com has pretty much everything.
And yeah, as others have said, a linear sequence of bytes is still a thing every CPU presents. Yes, there's cache & whatnot, but there's like precisely no way to usefully map that to any user-controllable/visible thing, because it's pretty much not user-controllable and intended to be invisible (and varies across all hardware).
I wrote Metalang99 [1] as a compile-time language that is able to perform loops, recursion, etc. It's not Turing-complete though, as the C preprocessor is not Turing-complete.
And because most of the world uses C for low level stuff. You can say that Esperanto is a much better international language than English but what good does it do if nobody speaks it?
You do what your operating system vendor does.
Not few operating systems have a C interface. The implementation of binaries (see also application binary interfaces) depends on the operating system.
Shared libraries (e.g., DLL) are binaries, too.
C compiler developers have the ability to generate consistent[1] binary outputs.
In simpler terms, vendors of these compilers can reach a consensus on how to convert C code into binary files, known as Application Binary Interfaces (ABI).
It is not uncommon[2] to have a foreign function interface in C.
1. http://yosefk.com/c++fqa/defective.html
2. https://learn.microsoft.com/en-us/cpp/dotnet/calling-native-...
I also like the minimalist nature of the language itself. I get that for desktop applications, you usually want more integration with the operating system so you can say "I want a window here and a button here" rather than having to manually build the window from scratch, but that's not something that's a concern in most embedded systems.
I'm operating in a world of voltage inputs and outputs, memory mapped devices, registers, flags, and timings... with almost nothing between me and the hardware. A simple language makes a lot of sense here.
Hint, might check their libraries/SDKs before answering.
1. Yes they are microcontrollers.
2. Yes they use C/C++. (check the libraries/SDKs, 1 layer under the hood it's all .h/.cpp files, and most of the arduino calls are just #defines)
I don't understand why you're trying to cherry-pick like this.
"Their libraries are written in/for C"
Also, you've deliberately chosen a specific interpretation of my statement in order to manufacture an argument that doesn't exist. You should probably avoid doing that in the future.
I avoid whatever I feel like.
It's almost like I have some domain knowledge that you don't. Imagine coming in here with examples that aren't even microcontrollers as if that "debunks" what I said above. Like somehow magically I can just switch to a whole different platform. No problem, just crank out a new board spin and swap my whole toolchain over so I can... what... use a non-standard version of C in the arduino IDE for production code? If you think THAT'S a viable option, you've lost your mind.
Why continue to double down when you clearly have no idea what you're talking about?
> I avoid whatever I feel like.
You should "feel like" avoiding inventing arguments that hinge on misinterpretations of other people's statements. The fact that you don't makes you a problem.
Meanwhile, I can't avoid C even if I "feel like it"... because I write code for microcontrollers... which have libraries that are written in and for C.
2. Yes they use C/C++"
So how it is?
Why continue to double down when you clearly have no idea what you're talking about?
Those are not my words.
The things you mentioned aren't both microcontrollers, and they use C.
Why continue to double down when you clearly have no idea what you're talking about?
You shouldn't assume I get to choose the platform I'm working on. That's not how it works where I'm at, and if (when) I do get to choose, programming language is unlikely to be near the top of the list of criteria.
If you're going to be pedantic, you need to be both relevant and correct. You are neither.
As for low-level, sure that's no longer the case. It was a low-level language for K&R and their PDP-11 where they could tell precisely what will be assembler code for each line of their C code and how many CPU cycles it will take. That's no longer the case indeed.
You cannot be serious. "Well known list of bugs" would be more in line with the state of affairs.
It's true that almost nothing works the way it's presented: the computer doesn't necessarily actually do the instructions you specify, it does its machine commands that are compiled. It also doesn't necessarily even do them in the order they are specified. The memory isn't actually a big continuous space, it's mapped as virtual memory. The actual memory isn't used in that way either, there's a hierarchy of NUMAed caches between the CPUs and the actual memory.
But it's a useful abstraction. Partly because a lot of the above things are built so that the abstraction works. But also because we want it to look that way, and it's kinda natural to let programmers imagine a virtual machine that works that way.
You only need that abstraction if you're doing low level bit/byte bashing and I/O, or there's some chance you may run out of memory and need to handle that manually.
That applies to a tiny slice of all possible applications.
There are far more useful modern abstractions that don't need to make those assumptions.
But someone's got to write drivers and someone's got to write the thing that connects the higher levels to the metal.
That PDP-11 from the seventies had ADC/SBC (addition/subtraction with carry) in its instruction set, the result of MUL was twice the size of the inputs (i.e., multiplying two ints produced a long), and DIV produced both the quoitient and the remainder. None of that is visible from C and yet people keep clamoring that "C is close to the metal". Bah, humbug: while " * p++" and " * --p" idioms translate directly into an addressing mode particular for PDP-11 — most other architectures don't have autoincrement/decrements — there is no specific support for " * ++p " or " * p--" in the machine itself.
No amount of abstraction under any language will isolate you from such memory model.
I may be being pedantic or outright wrong (since it's been a while since I used C), but I don't think C can address memory by individual bit.
You have to read one or more bytes from memory, twiddle the bits in them, using C's bitwise operators (like !, &, | and tilde), and then write the changed bytes back to memory at the same addresses you read them from. At least for the earlier C versions I used, this was the case, IIRC.
And to read and write those bytes, you do it via scalar variables like ints or longs, or via structs or arrays, or via pointers. Or using library functions like memset().
Technically correct, but Rust was changing significantly from version to version prior to the 1.0 release some 8 years ago, notably the green thread runtime was removed.
The availability of new tools with significant advantages over the old tools is almost always a reason to consider the new tools for certain use cases, but the new tools are rarely just strictly better on literally everything, there are generally now use cases when you say “the new tool is a solid fit here” and other cases where you say “the old tool still hits the sweet spot better”.
And that’s before you consider massive existing code and infrastructure and and tooling and investment: which is very, very often a far higher order bit than C vs not-C.
A great example would be a JVM-caliber GC? Thats just such a win over malloc/free so often, but it doesn’t obsolete malloc and free across the board: it gives a thoughtful and mature team a whole new set of options.
Rust would be a (comparatively) recent example of a language that hits a lot of the sweet spots of e.g. C/C++ and brings some cool new stuff to the party, and might even represent a better default these days, but the idea that it strictly crushes them in full-stop everything is a political-style conversation not a reasoned engineering tradeoff conversation.
Even C++ which has been around forever and is give or take backwards compatible with C with good tools? Hasn’t obsoleted C.
More options is generally a good thing (there are exceptions).
2. Relatedly, it's more explicit than almost any other language. If a line of code doesn't look like a function call, it's not calling anything. There is no hidden control flow. These statements are not true in languages which support operator overloading or exceptions. The only real competitor to C here is Zig.
3. If I give a Linux user the source of a C program, they can probably compile it with the tools they already have. This will most likely be the case 20 years from now too, as long as I keep my C mostly standard-compliant. I'm not sure that code in newer, faster-moving languages like Rust will stay compilable as long.
4. It's a lingua franca. C libraries can be used from most programming languages without too much effort.
I probably wouldn't start a large project on a tight deadline in C, but I think it's a great language for writing new command-line utilities and for rewriting tricky algorithmic code from scripting languages. I've gotten 100x and even 1000x speedups from replacing a couple of Python functions with C.
The ease of use is about to improve with the C23 standard, which I'm very happy with. On the other hand, some tricky areas like aliasing are likely to stay tricky forever.
While it is true to a degree, I would also add that due to its low level of expressivity, you often have to introduce less efficient solutions simply because language deficiencies. Things like small string optimizations in C++ are simply not possible in C.
2 is true, but it comes at the expense of bad expressivity, see the former point.
3. Well, will it really compile to what you meant? If you have UB, it might still compile but the semantics of your program could change entirely depending on which compiler and which version you use.
Also, your Python point: well, that’s because you used python in the first place, which is very slow even among scripting languages.
I don't think this is true, I've seen a bunch of libraries implement SSO in C:
https://nullprogram.com/blog/2016/10/07/
https://github.com/stclib/STC/blob/master/include/stc/cstr.h
https://github.com/mystborn/sso_string/blob/master/include/s...
You don't have to use an inefficient solution. You can always roll your own optimized solution or use a library. I agree that C++ has some nice string optimizations built into the standard library, but it's not obvious to me that they're always better than the simplicity and predictability of a simple chunk of memory.
Besides, you generally don't write C code in the same way you write C++ but with more primitive tools. You often allocate a buffer once and operate on it; you don't emulate passing strings by value from function to function, doing lots of allocations and deallocations in the process.
> Well, will it really compile to what you meant? If you have UB, it might still compile but the semantics of your program could change entirely depending on which compiler and which version you use.
I'm not sure what point you're making here. If you have bugs in your program then it may work incorrectly, yes, but that's true no matter the language.
That’s not true. You for example can’t write a generic, efficient vector implementation in C - the language itself can’t do that. You either have to copy paste the same code for different sizes, or make use of some monstrous hack of a macro. Instead projects use hacks like conventionally placing the next/prev pointer in structs (linux kernel), and the like.
C++ is the de facto language for high performance computing, so I very much question that “you don’t write C as C++ part”, if anything you don’t write C++ as C as that would be inefficient.
There is nothing in C++ that is inherently more efficient than C.
On the topic of performance, the best counterargument to C++ from a C perspective would be that hand rolled code generation isn't all that bad in practice. It's just language theorists don't like that approach aesthetically.
This is not a hack, it is the way it should be in C.
std::vector (which is a really inefficient way of doing dynamic arrays btw) can be cleanly implemented with macros (see stb stretchy buf) or by splitting the element data from the housekeeping data:
int append(void *arr, size_t elemsize, size_t *capacity, size_t *size, const void *items_to_add, size_t num_items_to_add);Especially that that macro-hack from stretchy buf seems to do it in an even more naive way.
Splitting the element data is a different implementation with very different performance characteristics - it’s quite a bad thing if I have to resort to that due to a language inefficiency, especially in case of a language that is supposedly close to the hardware.
Yeah, I was thinking about your string example when I wrote that. For high performance numerical code, I can see the advantages in using C++.
I think we call this "No True Scotsman".
In real life lots of people write C because they want to or have to and they generate tons of bugs from bug classes that just aren't present at all in other languages.
I think the CVE database would disagree with that statement.
C23 hasn't been released yet so it's hard to talk about compatibility. It's probably going to take a few years before it's widely supported.
Why is that important to you?
In languages like C++ I potentially need to check every operator if it is overloaded, and find the place where that happens (I think I haven't seen any IDE support to help with 'resolving' overloaded operators, but maybe that has improved in the meantime).
I guess I will never understand the C and Java developers incredible fear of operator overloading.
Do you have the same reaction to user-defined functions? Because they are exactly the same thing. Is it because of the bad type system that won't let you know what operator you are using?
The answer is in the sentences right before the one you quoted:
Relatedly, it's more explicit than almost any other language. If a line of code doesn't look like a function call, it's not calling anything. There is no hidden control flow.
Consider the use-cases for C: operating system kernels, hard real-time software, low-level libraries, databases, embedded software. What is a common desire among these? Predictable low-latency and high throughput.
It's much easier to achieve these features if your language does not allow "magic." Implicit allocations, RAII, exceptions, overloaded operators; these are all examples of features which allow a library-writer to inject hidden control flow into your code. This can make it very difficult to analyze why code runs slowly or with unexpected random pauses, not to mention making it much harder to step through in a debugger.
And whether something is called or run inline is always decided by the compiler. Modern C doesn't promise you any relation between the way you break down your functions on your code and the actual function calls on the assembly it generates.
So, I keep seeing people complaining about overloading; always with the same reasons; that are patently not valid unless there's some implicit assumption they keep not stating. What is that assumption that breaks the equivalence between user-defined functions and operators?
The implicit part is the question of whether an operator is built-in or overloaded. In C, every operator is built-in, so you can look at a block of code and see that there are NO function calls in it. With something like C++, you must treat every operator like a function call.
With C, if I write:
a += b;
I can be VERY confident that this line of code will execute in constant time. With C++ (or other operator-overloaded language), I cannot. I need to know what the types of a and b are, and I need to go look up the += operator to see what it does for these types (and this is not one universal place, it's specific to the type).Furthermore, this may be the last line within a particular scope. With C I know that nothing else will happen, and that the control flow depends only on the surrounding scope. With C++, I don't know this! There may have been many objects created within this scope and now their destructors are firing and potentially very large trees of objects are being cleaned up and deallocated, and even slow IO operations running.
All programming requires people to follow reasonable conventions. In C++ if you make a dereference operator with non-constant time, or an equality operator which doesn't follow equality semantics, the programmer messed up. It's like giving a function a misleading name, like `doThis()` and it doesn't.
Note that Java is filled with these kinds of conventions, such as overloading `equals`. How can you be certain it actually obeys equality semantics? You have to trust the programmer.
In C, if I'm searching for how a certain thing may be called from a given function, I only have to look for /\w\(/ and don't have to ever think about anything else.
Honestly, operator overloading isn't really that bad (especially if an IDE can highlight which ones are), but it's still a thing that can affect how one has to go about reading code that might not even use it.
a = b + c;
Is the addition done by itself, so it costs 1 clock cycle? Is it merged into some complex operation so the net cost is less than 1 cycle? Is it completely optimized away at compile time, so it's infinitely faster?
Does the addition trigger some trap, that will run some distant code?
Is the addition by itself? Or are there store and load instructions that can stall for way more than 1000 cycles?
I doubt you can answer any of those questions. All you and everybody else keep repeating is you can micro-optimize C better because that line, that you expect to take something from 0 to 2000 cycles is certain to not do a call and return pair, that takes less than 10 cycles. All while the alternative is almost certain to do the exact same, but you would need to check it up.
Honestly, that argument doesn't make sense; and I keep understanding it as people complaining that they want to micro-optimize a program, but don't know if it's operating on native integers or 10-dimensional hypermatrices.
At the same time, every single person that is good at micro-optimizations look at the compiled binary as a first step, because C is a high-level language that has little relation to the code the compiler actually creates.
For a long time I did just shrug it away and file those complains as "those people don't even know the language they are using". But its universality forces me to consider that there is a reason for complaining, and maybe it's worthwhile to understand. Now, given that this is all the answer I get, it seems quite likely that even the ones complaining don't consciously know what the problem is... But one thing is certain here, the people repeating that execution time is well known didn't actually practice micro-optimizations based on that fact.
Those are generic instruction selection/optimization questions, which are always gonna be *additional* complexity to any and all operations everywhere. So there's still benefit in cutting down the complexity elsewhere.
> Is the addition by itself? Or are there store and load instructions that can stall for way more than 1000 cycles?
..those are questions about the loads & stores, not addition. On embedded, afaik loads & stores will be significantly closer in latency to arith too.
> At the same time, every single person that is good at micro-optimizations look at the compiled binary as a first step, because C is a high-level language that has little relation to the code the compiler actually creates.
Yes, but being able to have good intuition is still quite important, because one can think & read code much faster than compile & read assembly.
> the people repeating that execution time is well known didn't actually practice micro-optimizations based on that fact.
The question of operator overloading is mostly about reading code, not writing it. And it doesn't have to be micro-optimization either, any level of optimization will be affected by a call happening where you don't expect one (probably most importantly the kind where you scan over a piece of code to figure out if it does anything suspiciously bad (i.e. O(n^2) or excessive allocations or whatever thing may be expensive in the codebase in question) but it isn't worth the effort diving into assembly or figuring out how to get representative data for profiling the specific thing).
Or you could just be exploring a new codebase and wanting to track down where something happens, where it'd be beneficial to have to just scan through function calls and not operators.
every single person that is good at micro-optimizations look at the compiled binary as a first step
That isn't an option when you're writing portable code that runs on many different platforms, some of which may not even exist at the time you're writing it. Furthermore, micro-optimization isn't the only reason operator overloading is bad. The implicit flow control dramatically inflates the search space for what every single operation can do, making all code much more complicated to inspect at a glance. This carries over to debugging, where stepping through code is much more cumbersome when each operation can involve large amounts of indirection.
Of course, C functions can be passed as variables. Or in a wider scope they might be inline, macros, or ifdef'd to different functions. But those cases are _typically_ recognized as undesirable and avoided.
Java's a bit of a different story, which I can't figure out a good way to explain. It's hard to explain problems in large code bases, as a quick example rarely suffices. I've seen more than one bug caused because foo.bar(qux) called a different method of bar than the original programmer intended (both because foo's bar was overwritten and qux was a different type than expected).
Don't get me wrong, I would use operator overloading in a heartbeat if I was writing code for a math-y CS coding assignment. It's fine for code that will have a lifepsan measured in weeks / months with probably only 2 or 3 people ever looking at it.
Saying what you mean, as clearly and directly as possible, has it's perks in certain applications (large code bases, life critical code bases, code bases that will last for decades with dozens of programmers). Otherwise stated, cases where code is going to be read many times more than written.
To answer your question more directly: User definable functions aren't a problem. Re-definable functions are!
Why is that? I use scripting languages mostly in my day work (Ruby and some Python bc AI) and have found my productivity using command-line utilities is amazing with Ruby. Do you do it bc of performance, ease of use bc you are proficient, a mix of both or something else?
I reach for C when performance matters, for example when processing multi-GB files or looking for perceptual hashes that are similar. It can be a difference between minutes and hours of running time.
Why only Zig?
In C, if you for example write past the bounds of an array or otherwise do something that causes UB, there is no guarantee that the code you wrote in the source file is actually going to be what's ran.
If an attacker can clobber the stack (for example), the control flow you see in the source code and the actual control flow of the program are not the same.
In the worst case, an attacker can get your program to execute arbitrary code of their own choosing!
Maybe some consider this unrelated to the no implicit control flow thing, but I think when UB caused by a trivial mistake can alter your control flow, you have much bigger worries than an operator being sugar for calling a function.
I consider UB and arbitrary code execution exploits to be a case of implicit control flow!
except maybe allocating dynamic arrays, floating point ops if those don't exist in hardware. Then you have signal handlers that can be called on math errors, segmentation faults, ... . So basically every line in your code can implicitly call a function.
> If I give a Linux user the source of a C program, they can probably compile it with the tools they already have.
What is win32.h and why is it missing?
> This will most likely be the case 20 years from now too
What is Xlib.h and what do you mean I have to rewrite the apps front end from scratch?
I guess because I just don't agree with this viewpoint at all. I've been writing C on and off for over 20 years now and I simply haven't encountered the amount of distress and pain that I see others deal with, especially when related to memory handling or undefined behavior.
I wrote a piece of software in Win32 C for a gas integration company many years ago that did tons of string manipulation to recalculate reports coming out of another piece of software. It even included a custom built on-disk database which basically ended up being my own version of BDB. Scratch that, I wrote this software twice because my first version was lost in a disk crash and I had to hex dump the database format to recover my original implementation.
Last I recall that software ran at that company for over a decade and probably helped them make millions in revenue. I didn't have a single support ticket and to be honest the last time I talked to the owner I thought they had just stopped using it. I was very surprised that they were still very happy with it and it was working fine.
That's just one of many examples of projects I've built or debugged in C. I've regularly been able to fix issues in OS drivers, large projects like Asterisk, and things like deadlocks in toolkit-based GUI programs. It's actually easier for me to use C than most other programming languages because it's clearer to me what should be happening, especially when dealing with anything systems-related.
That's just my experience. I totally get that others don't share that same experience but to be honest I'm pretty tired of seeing all of the confused hatred for C.
Everything I wrote in C/Win32 is as much fresh as it had been 30 years back.
But as far as UB goes, that's cheating. We're playing on "easy mode". We know what that compiler is going to do, and that's all we need.
"Hard mode" for UB is when you have to worry about what a different, unknown, perhaps not-yet-written compiler is going to do with your code. What is the absolute worst that a compiler could do, within the rules, to your code? You and I don't worry about this, and it doesn't bite us. People writing library code do have to worry about it far more than we do.
So I agree that the concern is overblown. But I think that maybe we miss that it's a real concern, because it doesn't hit us.
The Lindy effect.
You can run C code from anywhere.
There are places where it is much easier and better to run C code than anything else.
All of these are related to how long C has been around. I think that's also the reason why we use JavaScript extensively.Because loops are fast.
I do scientific computing, where many people use python nowadays, and a few years ago it was matlab/octave. These languages feel "cramped" because they artificially force you to program in a certain way in order to avoid loops. While such a "vectorial" notation is often useful, many algorithms are better expressed using a loop notation, and C does not impose an artificial distinction between the two notations: both are as fast as they can be. The fact that python is not an appropriate language for low-level numerical computation is evident when you notice that most numeric algorithms in python are just interfaces to code written in other languages (C, C++ and Fortran).
Of course, C is not the right tool for the job either... Modern Fortran is, objectively, the ideal language for low-level numerical computing: it has native multidimensional arrays and a lot of other goodies, which C lacks.
Julia would also be a nice alternative, and I check it regularly. But I find the current interpreter too quirky. I would love to see different interpreters/compilers for this lovely language!
This pragmatism by compiler vendors to just ignore the C standard where it doesn't make much sense, and to extend the language where it helps to solve real-world problems is actually a pretty powerful argument for C.
(disclaimer: I switched back from C++ to C as my language of choice for writing libraries ca 2017, but also continue to write C++ (if necessary to talk to C++ libs) and a lot of Python and Typescript for simple cmdline tools and web stuff, also ObjC on Mac of course for talking to system frameworks, in recent years dabbled with Rust, Odin and Nim, in the long distant past also with C#, Java, Lisp and some Forth, and eventually hope to transition over to Zig for the stuff I currently use C for (maybe in 3..5 years?)
TL;DR: use the language that suits a problem best, and C is a very good tool to have in any language toolbox, because it can usually provide a solution where other languages have to give up or just become to much of a hassle (for various reasons)
You don't really start a project in C unless you target limited hardware or some low-level library that can be embedded in other things and interact with other language that can make us of C-style APIs.
C became the "new assembly", meaning it sort of replaces the role assembly had. The chips that are sold are not programmed in assembly, because they're sold with a C compiler target directly.
C is more than a programming language, it's an universal glue, so it often makes sense to use C because it gives access to everything. It's like english: you can't expect to use esperanto just because it's a superior language. Programming languages are the same.
Disclaimer: I mainly use python and C++.
Like it or not, C might still be the most widely used language after 50 years, it will not go away, instead, future AI code review tools, static analyzers, more powerful compilers will evolve fast to make C safe and alive. Why, the price to replace it will be much higher in practice, it might simply be impossible.
Yes, that library does things with pointers the new language can't prove are safe. It's been used for longer than you've been alive and it isn't changing. If a new language can't express what it's doing, well, the library isn't going to move, the language is. Therefore, I either have odd shims and contortions or I have C.
I await a Buzz Language to eventually have "inline C" the way C has inline assembly.
>I used to think "C presents the most honest representation of the low-level mechanisms of the computer", but... even this is shaky. I've been programming for almost 15 years now, and I don't think I've ever seen a computer where memory is actually a continuous array of bits sorted by memory address. The C representation of memory (and all the pointer arithmetic) is not a real representation of your hardware, and this too is an abstraction.
Pointers are an abstraction, but they are less abstract than most languages simply assuming there is just one giant sheet of memory to take from.
It was my first programming language and I still think it's a simple and fun language. Also many things have a native C interface so it's a natural choice in those cases. It's certainly not the only language I use, but for many things my default. What's nice is that I don't have to consciously think much about the language when I use it because I know it well.
As to why work in C - it’s incredibly fast, it feels very powerful as long as we manage memory correctly. We use fsanitize, which is an amazing library that can find memory leaks, buffer overruns, etc etc and run it on all unit tests. I think fsanitize is essential to have in your tool belt if you’re doing any C programming at all.
A pretty direct translation from Go to C resulted in about a 125% speed up (ie the C code was 25% faster) and this was already very optimized Go code with no allocations. From Go to WASM the results were disappointing to say the least - WASM was about 32% the speed of Go and not at all easy to multithread (and a gigantic file). From C to WASM I got a much better 79% of native speed - would have wanted a little bit more, but this is much more doable, and we haven’t begun to optimize some parts of this engine yet. And Emscripten seems to have very good pthread support, which I will try soon.
I think you're confused, because this is internally incoherent.
In single reference implementation languages, all behavior is undefined behavior. Undefined behavior is just behavior for which there are no requirements imposed by the international standard. It's an unbounded form of implementation-defined behavior.
Undefined behavior does not mean that the behavior is completely unpredictable. It does mean you should read your compiler's documentation (including tweaking what happens with certain common UB). For example, if you want signed integer overflow to always wrap, and you read the GCC or Clang documentation, you'll know to use -fwrapv. If overflow could cause catastrophic failure and the program should abort if it happens (e.g., Therac-25), you'll know to use -ftrapv. There's nothing wrong with writing to an arbitrary memory address, either, if you've read your documentation and that's how your environment communicates with a particular I/O port.
C’s type system is lacking, I wish it was more strict, and sometimes I wish it had some features from C++ (operator overloading for mathematical types, templates for generic programming) and features of other languages (multiple return values especially), but overall I’m okay with its limitations and have become used to working around them. Sometimes I compile C code with a C++ compiler just to take advantage of stricter typing, templates, etc. but for a lot of projects this isn’t a necessity.
C code written today will still be runnable 30+ years from now, and likely on whatever platform you're using, unlike code written in some flavor of the month language. C is standardized, has been ported to every architecture, and is easy to port in general, and there's so much code that's already been written in it that the inertia behind it is virtually insurmountable. I've invested significant time in other language ecosystems (like Perl, coincidentally also on the front page) only to see them eventually declared "uncool" (however productive) and killed-off by faddish HN types. But I'm confident they won't have similar success against C.
C is the real Hundred Year Language: http://www.paulgraham.com/hundred.html
I'm a little surprised at folks incredulity that C is used "nowadays". To me, there will always be a place for C. If you look at:
- the bulk of the internet traffic
- the bulk of the base OS systems and libraries it is running on
I'd say 99.9% of that is written in C. Hence my surprise.
I know Linus now embraces Rust in the Kernel or whatever and I'm not disparaging that, just seems obvious that C, old as it may be, is still highly relevant. C is a graybeard. Give it a break, right? :)
Interestingly, some might print the same title about JavaScript.
Or just comparable results?
Edit: oh, look at the chances - the ("the [only]" - 2nd to 1st) neighbouring submission is "How small is the smallest .NET Hello World binary". Suggests non overlappable outputs.
Really wish people would stop having language wars and realize that languages are tools for a job. C is like a flathead screwdriver, C++ is like a philips, Rust like a torx and Go like a hex. You can probably use a flatheat or even a philips for the torx or hex screws but you probably shouldn't, because they're not the right tool for the job.
I love Rust. I use Rust often. I choose it over C or C++ these days for a number of reasons. But I'm not going to write some throwaway little one-off scripts in Rust. I'd probably choose Python if I need to crunch some data, or Node (javascript) to do some quick I/O related tasks.
I'll use a makefile when windows compatibility and graph evaluation speed aren't important, and a shell script when compatibility and graphs aren't important at all.
And so on. The sooner people begin to realize this, collectively, we can stop having these silly "X language is better than Y" discussions.
C has its place. It's simple (quirky, but simple), doesn't take on a philosophy, and has a very, very wide set of compatible toolchains. That is the reality. Perhaps C-like's will take off and replace it (e.g. Drew Devault's Hare[0]) to get us away from most of the quirks, but that probably isn't happening any time soon.
In the same way that "putting ChatGPT in front of a computer-enabled machine gun is irresponsible", so is using unsafe languages in cases where you absolutely cannot afford a security risk without some sort of safeguards, verifiers, etc. and just good ol' fashioned "good engineering". And even in "safe" languages this is often hard to achieve, so it often comes down to the engineers anyway - not the language.
I feel like we have beaten the "C sucks" horse to death so many times that we could extract oil from it at this point. What is the goal with such discussions in 2023?
There are far more languages than types of jobs though. If it wasn't for language wars, how would you ever decide which one to use?? :)
Rust carries a lot of design decisions heavily influenced from ML and OCaml. The type system and exception handling implements things that look a lot like Monads, but with a bunch of boilerplate code thrown in due to the imperative execution. That sort of syntax turned me away from the language, and probably would for a bunch of other people who don't care for that kind of language design.
I struggle to understand a use case for Hare compared to the other C-sequel type languages.
Except, we do complain about Rust (e.g. I think narrowing conversions should require TryInto or a specific call, not just 'as', and I don't think String impl Add<&str> is a good idea) it's just that we think the other options are far worse.
"Programming language like C, which is quirky, flawed, and should NOT be an enormous success" instead.
you need it when exploring the performance ceiling of a workload. without an understanding of performance ceiling, you can’t design a system well.
if you want to approach that ceiling[1], you need to implement c/c++. if not, it’s better to have chosen not than been forced not.
between ccls, clangd, and clion, tooling is fantastic now. it’s a great time to start.
But look at how simple the language was in the beginning. A handful of concepts, strung together. Incremental changes; not all were deemed best in hindsight.
You can fit what is going on in 16K in your head.
> Thus the core C language escaped nearly unscathed from the standardization process, and the Standard emerged more as a better, careful codification than a new invention.
which made me grin.
If anyone had realized what UB would turn out to mean, it would not have been invented that way. Dennis Ritchie's 1988 comments on the `noalias` proposal match exactly: “the committee is planting timebombs that are sure to explode in people's faces”; “a license for the compiler to undertake aggressive optimizations that are completely legal by the committee's rules, but make hash of apparently safe programs”, and consequently: “[It] must go. This is non-negotiable. [...] The concept is wrong from start to finish. It negates every brave promise X3J11 ever made about codifying existing practices, preserving the existing body of code, and keeping (dare I say it?) ‘the spirit of C.’”
The standard describes the old interpretation of "if you do that, you get the consequences". The fact that somebody came up with a reinterpretation that bring a completely unreasonable meaning that just happens to be compatible with the text is all up to the people reinterpreting it. Nobody preemptively disavows unreasonable interpretations when writing something.
[0] because their machines worked very differently from the others' machines
The answer is different for different kinds of undefined behavior, but spatial memory safety violations are basically always possible in a language you can write an OS in, since you need to convert from hardware buffers to higher level types. temporal memory safety wasn't possible to enforce at the time on a low level language, it's taken decades for it to be implemented in a mainstream non-garbage-collected language. Integer overerflow is still not caught by default even in rust for efficiency reasons (it would take all the processor vendors to implement an efficient way of catching it)
Honestly, I don't think that was the right default, but it is configurable at the project level for release builds. If I were deploying tools, I would certainly enable them, just like the Android team does.
Errors produced by current mainstream compilers are terrible.
"Unresolved symbol" being the best they can do is some joke
/usr/bin/ld: main.o: in function `main':
<path>/cptutils/src/xycpt/main.c:148: undefined reference to `xycpt'
You get the file, the line, the name of the missing symbol ...Yet when reality kicks in, then you can spend 15mins trying to figure what the hells is going on
I run compilation and expect sane error messages - saying that "oh, it's because of linker yada yada" doesn't solve my problems nor is valid excuse
Other language (compilers) do better.
well, you may do that, but it doesn't make it so. the linker has much less information to go on than the compiler - basically (depending on how you compiled) machine code. actually, the GNU linker does quite a good job, given what it has to work with.
> Other language (compilers) do better.
a language is not a compiler
but it was their decision to split the tools like that, wasn't it?
nothing technically prevents you to model it in such a way that you can have access to the data you need, right?
You probably don't even need to have linker at all.
>> Other language (compilers) do better.
>a language is not a compiler
I wrote compiler in parenthesis.
Also, since "language" has two definitions
1. syntax
2. whole ecosystem
Then it is valid either way