back
147 comments
I think the main weakness of this paper is that it doesn't really address why C is used in lieu of other "better-C" non-managed languages, such as C++, D, Nim, Rust, or Zig--it's almost entirely focused on the idea that the competitors to C are inherently managed languages, framing the question essentially as a "why C instead of Java?" which is a frustrating approach when you're asking "why not <language which is not Java in the same way C is not Java>?"
C is better because it doesn't try to program for you, it isn't clever, convenient, or have any ideas about how you structure or solve a particular problem. It gives you basic functionality and then gets out of your hair and lets you ge on with programming without making a grand statement about computer science. For people who actually like to program and don't want the language to do it for you that is pretty attractive.

It also turns out that if you remove all the fancy abstraction and write everything out, the code becomes very maintainable and easy to manage. The fact that its about as portable as a program can be helps. I find that most people don't want to write C, but almost everyone is pretty happy interacting with C code written by others, because its simple, fast, portable, easy to integrate, and bind to other languages, and most non-C programmers can read it and have a rough idea of what is going on.

I do think you’ve identified a significant rift between those that prefer C and those who do not. I think you’re being downvoted for the value judgement you’re injecting, though.

> gets out of your hair and lets you ge on with programming

This is the divergence, I think. For me, the method in which c “gets out of my hair” means that more burden is placed on me, the developer. Which means I have to work more slowly, cautiously, and remember things that a good type system could have been checking for me. And the lack of features means I spend time working around that lack rather than getting real work done.

I suspect these preferences are irreconcilable.

> C is better because it doesn't try to program for you, it isn't clever, convenient, or have any ideas about how you structure or solve a particular problem

Why is that better though? I personally appreciate it when cleverness and convenience make my life easier, my programs less buggy and less likely to end up in a CVE, or point me toward better structure.

> It also turns out that if you remove all the fancy abstraction and write everything out, the code becomes very maintainable and easy to manage

History shows that this isn't true though, not in all cases.

> C is better because it doesn't try to program for you, it isn't clever, convenient, or have any ideas about how you structure or solve a particular problem.

Oh, C very much does have ideas. One of the big problems I have with C is that it has a surprisingly limited feature set that makes it difficult or impossible to express certain programming styles. Want to describe a function that returns multiple values? Well, you can't really do that (especially if you want this to mean "use two registers"). How about using unwind to express exceptional control flow? Can't do that either. Or if you want a function with multiple entry points? Or coroutines? C's fundamental inability to express certain kinds of control flow [1] greatly limit your ability to structure your code in ways that might make the code clearer.

> It also turns out that if you remove all the fancy abstraction and write everything out, the code becomes very maintainable and easy to manage.

I think experience has shown that this is not the case. The closest kernel of truth you might find is that if writing code requires a certain amount of tedium, that tends to limit the scale you can grow a codebase to, and size is one of the largest drivers of complexity. While it's definitely the case that abstraction can decrease maintainability, it is a mistake to think that it necessarily does so. Indeed, abstraction is often necessary to make code more maintainable. For example, when I design APIs, I think very hard about how to design it in such a way that it's impossible to misuse the API. So I make my APIs distinguish between UTF-8 strings and Latin-1 strings, so you can't be confused as to what's in them, or even distinguish between a row number and a column number.

The other thing to bring up is that we know quite well that humans are pretty bad at doing tedious, repetitive tasks with high accuracy. And computers are pretty good at that! What this suggests is that languages should be designed so that things that are tedious and repetitive--such as propagating errors up a stack, or cleaning up resources on failure--can be done by the compiler and not the programmer. As we've seen with things like gotofail, C is a language which lacks features that enable work to be pushed onto the compiler and not the programmer, and code written in C suffers as a result of that.

[1] And there's even more exotic kinds that I could get into--this is merely the list of features available in languages you've probably heard of, maybe even used!

> It also turns out that if you remove all the fancy abstraction and write everything out, the code becomes very maintainable and easy to manage.

I was with you on everything until this part. I just don't see how reducing soundness via a weaker type system, and needing to explicitly manage memory vs e.g. Rust, uniq_ptr, shared_ptr, and so on makes things easier to maintain. I also don't see how absence of generics helps here either.

I think it's high time to bury the myth of the "bondage and discipline language".

Study after study has shown that, comparing development of the same application in C vs. a language with a very strong type system such as Ada, the application in the language with the very strong type system costs less to develop and has far fewer errors.

The fact that C "gets out of your hair", i.e., doesn't provide checking for certain kinds of correctness, is an anti-feature.

i haven't come across a single language that would limit me in how i structure my code. that is, i didn't feel limited. whether it was python, ruby, smalltalk or even lisp. heck, once i translated a program from pike to common lisp. the structure of both programs was identical despite everyone claiming that lisp is so much different. it can be, but it doesn't force you to be.

most non-C programmers can read it and have a rough idea of what is going on

that's because most programmers can read most other languages and have an idea what's going on. that's not at all a special feature of C.

> C is better because it doesn't try to program for you, it isn't clever, convenient, or have any ideas about how you structure or solve a particular problem.

This is much of why I prefer to use C (well, I actually prefer to use C++-as-a-better-C, but since I'll omit almost all of of what C++ brings, it's still C for all intents and purposes).

Other reasons I prefer C include producing small executables (and, more importantly, being able to effectively control executable size), that it is the most portable language (in the sense that there are C compilers for just about every computer platform in existence), and it is both readable and concise.

The historical answer was fairly simple:

1. C was slightly more understandable than existing options at the time

2. The compiled objects could be manually verified/debugged with relative ease

3. There was no mystery/interpretation between the runtime and the hardware, but this isn't technically true anymore for many platforms.

4. The simplified environment demands simplified structural design. Try anything fancy, and C can be very unforgiving to the uninitiated (easier to write bad code). Do a one liner in high level languages instead =)

5. Due to the well-defined simpler syntax of standard C89, the GNU groups were able to bring opensource/free compilers to many new platforms. ARM popularity, Linux, android, and most IT infrastructure owe their existence to these compilers... even if it was just bootstrapping the new environment.

6. All languages have use-case specific tradeoffs. I have worked with over 54 different languages, and have observed the following: if your code still builds and runs on a 5 year old OS, than it will likely not need refactored for another 5 years.

As Intel begins to deprecate x86 legacy features, there will be a lot of drivers that will need rewritten.

Have a wonderful day =)

I like Rust, but I wouldn't categorize it as a "better-C". It is a whole different world, and some things work well in Rust but not in C, and vice versa. Many things are flat out uncomfortable to express in Rust, and the number of times you need to bail out and just Rc<RefCell<T>> or Arc<Mutex<T>> random things is a bit high.

Interacting with anything but basic C is also horrible, requiring significant engineering efforts to hide the glue in a binding crate when possible, and sometimes it just doesn't work out. Many C libraries are heavy on linked lists of linked lists with their own lifetimes, and that's Rust's kryptonite.

Zig is definitely a "better-C", but also young and not nearly as widely adopted.

I would like to attempt an explanation. It boils down to learning complexity.

> why C is used in lieu of other "better-C" non-managed languages, such as C++, D, Nim, Rust, or Zig

I am a system admin. I do not earn my income writing code and therefore spend at most a few hours a week programming. I've spent about 1000 hours writing C code in my life. About 200 hours of Golang. Years of Posix Shell. Years of Perl 5. I've had a little exposure to Java and C++ and Haskell. I have read a few examples of Rust.

C++ has a higher complexity than C. C++'s syntax is more powerful, it has an additional paradigm (the C++ template system), and if you mix in QT you have one more paradigm (QT preprocessor), it has a very wide ranging standard lib (which data structure do you use for lists? vector, dequeue, ...) augmented by QT and augmented by boost. C++ is huge. There is no way I will learn that in my professional life, I simply do not have enough hours of training left. C++ is not a valid successor to C, because its complexity hinders acquiring the language. Rust suffers the same complexity as C++. I will not have enough hours on my learning schedule to acquire a proficient level of Rust.

Golang is nice for me personally. The book "The Golang programming language" is only double the size of "The C programming language", which makes them comparable in complexity. I get stuff in Golang done faster than in C since I find debugging easier.

I have neither used NIM, nor D, nor Zig. All I can tell you is that C is sexy because the language is small and therefore one can acquire it in a life time -- without being a full time programmer.

2017, so zig was very much a baby. Rust was still young.
(2017)

This always gets 0 comments or hundreds of comments. Some previous ones with more than a handful of comments:

* https://news.ycombinator.com/item?id=34640233

* https://news.ycombinator.com/item?id=26300199

* https://news.ycombinator.com/item?id=19736214

* https://news.ycombinator.com/item?id=15179188

Some of those comments are pure hilarity: "C got popular because it turned out to be ideally suited for DOS. C could deal with near/far pointers. No other language could". Yes, C was very good at having two (or even three, "huge" being fully-normalized far pointers) completely differently sized kinds of pointers, without any proprietary extensions, absolutely, and it caused no problems whatsoever because C memory model indeed presupposes a segmented address space.
Escaped the dupe detector because four months ago there was an extra slash in the url.
I appreciate this kind of discussion. It's important to note C programmers these days effectively avoid the inherent risks of unsafety by deploying solid memory strategies.

For example (self-plug incoming) we're hosting a workshop this summer [0] teaching people to replace a rat's nest of mallocs and frees with an arena-based memory allocator.

These sorts of techniques eliminate the common memory bugs. We don't do C programming the way we did back in the 80's and 90's.

[0] https://handmadecities.com/boston

Arenas were pervasively used in the 80s, 90s, 2000s, ... and are still widely used

Using Malloc and free in the naive way was the exception, not the rule

They can be the cause of memory safety problems in some cases, as well as a partial solution in others

Or better yet, don’t use dynamic allocation at all! Very happy when I’m on a firmware/embedded project with no dynamic allocation, it makes C halfway enjoyable… though extremely boring.
Wow! I'm really interested in the Boston event. I'm a little afraid I may not be experienced enough though. How master are we talking with these masterclasses?
I'd be curious if there were any studies done on how much effect this kind of approach has on the number or severity of bugs or security issues in a code base.
What is "solid memory"? Is that what I call "contiguous memory"? (Allocating a big block and doing all your memory stuff in that one block, using metadata to know how much of the block is "free"?)
Nice plug! Just signed up
Very interested to hear more, will be registering virtually!
The crux of the argument of this article seems to be that C allows working directly with raw memory, which is necessary for some low-level tasks, such as communication with memory-mapped devices. This is not possible in pure managed language, due to the additional level of abstraction.

However, the author themselves state that this is not something that implies that undefined behavior are unavoidable when working close to the metal:

    In fact, the very “unsafety” of C is based on an unfortunate conflation of the language itself with how it is implemented. Working from first principles, it is not hard to imagine a safe C.13 As Krishnamurthi and Felleisen [1999] elaborated, safety is about catching errors immediately and cleanly rather than gradually and corruptingly
I would say that, what they are describing is essentially what Rust has achieved, or at least what it is meant to do.
While I don't fully disagree with you, I think the author would disagree. This line of argument is also repeated in recent C++ standards papers around "safety," that is, this argument is explicitly saying "memory safety != safety, and safety is more important, and therefore focus on memory safety is bad for safety."

I have two responses to this that aren't "back on script."

> it is not hard to imagine

(from your quote)

Sure, we can imagine this language, but it does not exist. Yes in theory maybe if we all did what the author said, C could be that language, but it is simply not today. When people say "C" they mean the C that actually exists, not an alternative implementation that would technically conform to the standard. Pushes have been made in this direction a number of times, from a number of different people, yet the market has spoken.

Second,

> Consider unchecked array accesses. Nowhere does C define that array accesses are unchecked. It just happens that implementations don’t check them. This is an implementation norm, not a fact of the language.

This is just factually incorrect. Let's examine both C89: https://www.open-std.org/JTC1/sc22/wg14/www/docs/n1256.pdf and C99: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf

> J.2 Undefined behavior

> An array subscript is out of range, even if an object is apparently accessible with the given subscript (as in the lvalue expression a[1][7] given the declaration int a[4][5]) (6.5.6).

It is literally defined as undefined behavior. While sure, that does mean a particular compiler could implement and document semantics for this, it's by definition not portable, because it is stated as such by the standard.

C, is high level language that happens to map really well to hardware. Much of UB is UB simply because it difficult to effectively detect when something goes in to a state of UB. Writing to a memory address can be done in one instruction, but figuring out if that address is legal to write to is a slow and complicated process. The C standard let an implementation ignore that problem, but it is also does not stopping the implementation from making sure the address is valid before writing to it. Almost no one tries to make such an implementation, because the users have decided they would rather have speed, but anyone can write a ISO compliant implementation without any UB if that's what they want.
Communication with memory-mapped devices actually works pretty well in other languages. And surprisingly badly in C: when trying to directly use memory mapped space as backing store for C objects, compilers have freedoms about the memory accesses, which doesn't mix well with finicky mmio devices. (There are also platform differences which make this pretty nonportable). Reads and writes to memory mapped devices can be represented as eg function calls in other languages (and commonly in C too).
Any good docs on memory and data structures for c?
The article that introduced me to "Priest with Balloons", by Tiny Ruins.

Based on a true story: https://en.wikipedia.org/wiki/Adelir_Ant%C3%B4nio_de_Carli

I moved away from C to Java, then I returned to C. I tried Python several times, but always returned to C.

C is small, with very few hidden 'gotchas', and it works. No wonder I always come back to C. You could call it the WYSIWYG programming language.

What are the 'gotchas' that Java has but C does not?
This "communicativity" is a feature only enabled by fixing a certain execution model (roughly "everything is addressable bytes in memory"). Thus any languages that would support "communicativity" would necessarily need to adopt this model explicitly - which would deprive them of some abstractness, and, I think, even probably make them C-like. In other words, there is simply nothing other to do than "creating new worlds" (or creating a C).
I have a trivial, superficial gripe with modern languages. I hate this syntax:

    let variable_name : type = value;
I find it extremely ugly.

I know that people here have disdain for discussions about syntax [1], and I know that once compiled, the specific syntax doesn't matter that much.

But I think that language aesthetics do matter.

People say that languages are "just tools", and the output quality is all that matters. But, personally, I would want the tool that I'm going to be spending a significant amount of time working with to be beautiful and feel good.

And that's one point where I consider C to be superior: aesthetics. If I start a hobby project, I may start in C, because programming in it makes me feel good, even if it's unsafe.

I'm not a language expert, so I don't know if there are good technical reasons for this modern syntax to have replaced C-style initialization, but I think there must be some way to avoid using this modern syntax without losing whatever feature is made possible by it.

So, my point is: We shouldn't discard aesthetic considerations so lightly. Languages are not "just [ugly] tools", they can be a medium of artistic expression to some people.

[1] https://wiki.c2.com/?WadlersLaw

I agree that syntax matters, but I like that syntax so much better. Here are some reasons:

- The variable name comes before the type. Thus if the type name is large it makes it difficult to scan for the variable. This isn't too bad for `let`s in a language like C, but my day job is C++ right now and the fact that the return type for functions comes first is terrible. It's inevitably enormous, so it makes it bloody impossible to quickly see what the damn function is called.

- The keyword `let` is first, so with syntax highlighting you can easily visually see the structure of the code, and which lines declare variables vs. call functions.

Are there reasons to like the C style

    type variable = value;
besides familiarity, and it being slightly shorter?
There are good technical reasons. That said, I don’t expect them to be persuasive, but the main reason for doing so is that:

1. It doesn’t require as much context to parse, making tooling easier.

2. It plays nicer with type inference or deduction.

These reasons generally trump aesthetics for new language developers, and so I wouldn’t expect to be seeing the C style in newer languages. Also, not everyone agrees aesthetically; I am the polar opposite of you.

I don't think that more modern variable declaration syntax is used because it allows for other things to be expressed syntactically (although maybe it does, like for type inference), but I think a lot of us just think it _does_ look better. I don't mind the C and friends

    type variable_name = value;
style, but I do like the type coming afterwards. I don't feel like there's a tradeoff, I think it's just better across the board, and I assume a lot of people are in that boat as well due to its popularity.
This is not a modern syntax, it predates C, and this whole debate goes a-a-all the way back to ALGOL and PL/I, which were late 60-es. And it doesn't really matter if the type is before (ALGOL) or after (PL/I, Pascal) the variable name; what matters is if it is actually before/after (ALGOL, PL/I, Pascal) the variable name, or around of it (C): compare "X ^ARRAY[10] OF ^INT" with "int (x)[10]".
I'm with you until type inference comes into play. For example, I find the proliferation of "auto" in C++ code far uglier than Go's "foo := expression" syntax where most of the time you don't even need to specify a type. Since most modern languages include type inference, I can see why they are moving away from C-like syntax, even though I still prefer it in most cases.
I generally think that C is the right level of abstraction and provides the right model for thinking about programming. However, I don't actually use C because I can work at the same level in other programming languages.

Right now, C++ is my language of choice, but I'm far from happy with it, for obvious reasons. I think my ideal language would be as close to C as possible while adding some quality of life modifications. Off the top of my head, those would include: namespaces, modules, generics, removal of the -> operator, and removal of forward declarations (which would imply order-independent declarations).

Of course, it's easy to imagine such a language, but hard to implement it and be successful in practice. Most attempts probably never get out of the hobby project phase. Those that achieve some success will feel pressure to continue to add features to differentiate themselves from the competition. Maybe I am in too small a niche and there simply isn't a market for what I would like to use.

C23 technically has namespaces for the attribute syntax. Hopefully it will be extended into the main syntax in the future. It's also easy to implement basic generics using either _Generic() or m4 macros.
You may enjoy Hare.
Man, I write all my programs in pretty much C. I'll use a class or two, but that's as far as it goes. I call myself a C++ programmer, but really I don't touch 95% of it with a barge-pole. It just seems like a load of unnecessary, complicated crap that will surely go wrong and confuse me.
I've always thought of assembly language as the arithmetic of programming. You can do anything programming-wise with assembly (ignoring micro-instructions). I don't know what the C equivalent in math would be but can't see it going away for the same reason eliminating the math equivalent wouldn't make sense.
I'm working on some bare metal ARM32 firmware, and I can't find a language that comes close to C. I can get it to generate pretty much any assembly I can think of. Other languages make it a pain to write memory unsafe code.
c and cpp are great. definitely worth learning, sometimes worth using.

probably best avoid for the first few years of serious engineering as a beginner.

Sentimental.