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.
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.
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.
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.
An 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 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. 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 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.
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 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 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.
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.
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?
C makes no assumptions about the size of a byte
C does not support bit-addressing.
https://developer.arm.com/documentation/100165/0201/Programm...
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?
I considered rewriting it in Go a couple of times but just didn’t see the point.
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*)0x12345678 *(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."
I can see the argument, but there's a much better way to communicate to the compiler 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 well defined compiler could do what you wanted.I've seen other people suggest that UB is a mechanism to have these magical backdoor conversations 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.
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 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.
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...
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.
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.
This is not a hack, it is the way it should be in C.
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.
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).
Are you talking about the ability to hover over an operator to find out which function it's calling? I think most IDEs can do that.
Total opposite. C is like poetry and icebergs. You have to look beneath the code to understand its meaning. I like writing software that appears simple to others. The C language gives me lots of tools for doing it. I'm a fan of promotion, narrowing, implicit casting, and I've even used implicit coercion. The best thing about C is how implicit it is. That's the grand trick which attracted everyone to the language. All the programs written in it all looked effortless to create, which was much more encouraging for beginners than something like COBOL or PL/1 which dumps a truckload of complexity on you all at once. Back in the K&R C days, even more stuff was implicit than there is today. For example, you didn't use to need to use types at all in your programs. You could also freely cast between integers and pointers.
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 register allocation/instruction selection questions, which are always gonna be *added* 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.
The vast, vast majority of C undefined behavior just isn't a real problem with common compilers and/or can be trivially addressed. If you're afraid of integer overflow, for example, you can simply call GCC or Clang with -fwrapv, or use one of the builtins.
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
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?