>To me seemed like C was such a tight, perfect little design. Only thirty keywords, and simple consistent semantics.
Except that clearly history showed that it wasn't enough, and we ended up with about 50 millions (and counting) different meaning for "static" for instance. I like C but its simplicity is almost by accident more than by design. It's pretty far from "perfect" in my book.
There are so many weird features about the language that can bite you ass for no good reason. Why don't switch() break by default since it's what you want it to do the overwhelming majority of time (answer: if you generate the corresponding assembly jump table by hand "fall through" is the easiest and simplest case, so they probably kept it that way).
Why do we have this weird incestuous relationship between pointers and arrays? It might seem elegant at first (an array is a pointer to the first element or something like that) but actually it breaks down all over the place and can create some nasty unexpected behavior.
Why do we need both . and -> ? The compiler is always able to know which one makes sense from the type of the variable anyway.
String handling is a nightmare due to the choice of using NUL-terminated strings and string.h being so barebones that you could reimplement most of it under an hour.
Some of the operator precedences make little sense.
Writing hygienic macros is an art more than a science which usually requires compiler extensions for anything non-trivial (lest you end up with a macro that evaluates its parameters more than once).
Aliasing was very poorly handled in earlier standards and they attempted to correct that in more modern revisions while still striving to let old code build correctly and run fast. So you have some weird rules like "char can alias with everything" for instance. Good luck explaining why that makes sense to a newbie without going through 30+ years of history.
The comma operator.
Undefined function parameter evaluation order.
I suspect that with modern PL theory concepts you could make a language roughly the size of C with much better ergonomics. I'm also sure that nobody would use it.
> (answer: if you generate the corresponding assembly jump table by hand "fall through" is the easiest and simplest case, so they probably kept it that way)
If this is true, then that makes perfect sense. C was attempting to look after its target market: assembly programmers.
> Why do we need both . and -> ? The compiler is always able to know which one makes sense from the type of the variable anyway.
I've often wondered this myself. The best I can come up with is that the underlying code generation includes an additional dereferencing step with ->, so having both . and -> makes the compiler a little more transparent.
Of course, in C++ you really need both because overloading -> is nice for e.g. objects that want to look like pointers.
This was solved in rust by approaching it from the other side. You need to be explicit everywhere that a value is "borrowed", so while you never need a `->`, you'll still know it's dereferencing in the more natural place - before the variable, not as an implied step after.
> Why do we have this weird incestuous relationship between pointers and arrays? It might seem elegant at first (an array is a pointer to the first element or something like that) but actually it breaks down all over the place and can create some nasty unexpected behavior.
> Why do we need both . and -> ? The compiler is always able to know which one makes sense from the type of the variable anyway.
You're contradicting yourself in a way.
Maybe K & R thought: "Why can't arrays decay to pointers? The compiler is always able to know which one makes sense from context anyway."
I agree with the your opinion about arrays/pointers, but disagree about ./->. A programmer reading the code might confuse a pointer for a non-pointer if you conflate . and ->
Except that clearly history showed that it wasn't enough, and we ended up with about 50 millions (and counting) different meaning for "static" for instance. I like C but its simplicity is almost by accident more than by design. It's pretty far from "perfect" in my book.
There are so many weird features about the language that can bite you ass for no good reason. Why don't switch() break by default since it's what you want it to do the overwhelming majority of time (answer: if you generate the corresponding assembly jump table by hand "fall through" is the easiest and simplest case, so they probably kept it that way).
Why do we have this weird incestuous relationship between pointers and arrays? It might seem elegant at first (an array is a pointer to the first element or something like that) but actually it breaks down all over the place and can create some nasty unexpected behavior.
Why do we need both . and -> ? The compiler is always able to know which one makes sense from the type of the variable anyway.
String handling is a nightmare due to the choice of using NUL-terminated strings and string.h being so barebones that you could reimplement most of it under an hour.
Some of the operator precedences make little sense.
Writing hygienic macros is an art more than a science which usually requires compiler extensions for anything non-trivial (lest you end up with a macro that evaluates its parameters more than once).
Aliasing was very poorly handled in earlier standards and they attempted to correct that in more modern revisions while still striving to let old code build correctly and run fast. So you have some weird rules like "char can alias with everything" for instance. Good luck explaining why that makes sense to a newbie without going through 30+ years of history.
The comma operator.
Undefined function parameter evaluation order.
I suspect that with modern PL theory concepts you could make a language roughly the size of C with much better ergonomics. I'm also sure that nobody would use it.