This may not be as nice as the compiler yelling at me even before the program is run, but it certainly makes for a much simpler language.
This may not be as nice as the compiler yelling at me even before the program is run, but it certainly makes for a much simpler language.
I've had this code yesterday on an embedded arm cortex m0:
bool tx_byte(char msg) { for (int i = 0; i < 8; i++) { tx_bit((msg >> i) & 1); } }
This function wouldn't return. What's even stranger, the variable i would increment past 7. The for-loop just continues.
I got a warning about the missing return statement in that function, but didn't think there could be a connection to my bug. It turned out that adding that return after the for loop fixed it.
The warning was one among about 100 others, as I was refactoring quite some code at the time. I only started working on the warnings after I had decided that me an two of my colleagues were just overlooking something and would fix the bug another day.
All that I wanted to say is that, no, C doesn't make you aware of what is happening. It has this giant pit of UB that does weird things when you fall into it. I was lucky that in my instance, I got a warning. I know other UB just works most of the time and goes unwarned.
Wouldn't that even be a compiler bug allowing no return statements, but then incorrectly handling scope?
Here the compiler assumes the function never returns (because it would be UB since there's no return statement), so it looks like it just didn't bother to add a loop exit at all.
This is standard compliant.
> If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.
So if the return value is never used then there is no undefined behavior and a standards-compliant compiler isn't allowed to optimize away the loop exit condition.
This is the whole story, really.
https://stackoverflow.com/questions/7280877/why-and-how-does...
With a enough printfs() I was able to whittle the problem down to: A local variable "b" was changing after some function call that looked like: "call(&a)". Turns out because our program looked like "{ int a[3]; int b; ... call(&a); }" an off by one error accessing "a[4]" in the function was overwriting variables never passed to it!
Felt like the champ figuring out. (This is a 15 year old story from freshman year, so I hope I got the details right.)
But still: If you passed the array and referenced it wrong it would change local variables in some remote stack frame.
Not necessarily. Many C programming errors invoke Undefined Behaviour, and then "appearing to work correctly" is a permissible option. So you will miss the programming error, until a change seemingly unrelated to that part of the system will trigger some segfaults or wrong results, or allow a determined attacker to use your program to launch calc.exe.
Unfortunately it isn’t true, C let’s you write plenty of broken programs that don't fail at run time, despite making a complete dogs breakfast of your systems memory space, instead we get crap like Heartbleed instead.
var ch chan bool
for i := 0; i < 10; i++ {
go func() {
// do something amazing
ch<-true
}()
}
Oops, I just leaked 10 goroutines, because I forgot to `make` the channel instead of declaring it, so its nil and will block indefinitely. Unless the program encounters an complete deadlock of all gorutines, this will not crash anything, and the caller of this code has no way of knowing it happened.Broken code can be written in any language, no matter how much checks & convenience it offers.
Suppose I screw up writing the GIF decoder for avatars on my forum web site and you are a bad guy who can create accounts and upload an avatar.
If I write the decoder in C it is very possible that your bad files can seize control of the web server, spill user credentials, post nonsense, mine crypto-currency on my servers, anything.
If I write the decoder in safe Rust, some of these things are much harder to pull off, and I need to be really incompetent to cause the worst harm - but you can likely cause a lot of mayhem still if I screwed up.
But if I write the decoder in WUFFS, the best you can achieve is to have the decoder chew CPU in an infinite loop or something. There are no boundary misses, integer overflows, or anything like that in WUFFS. My decoder might render your weird input as a giant orange splodge, or, as I said, spin forever wasting CPU, but it can't accidentally become a reverse shell server, or send you credentials from my password database, such things are entirely impossible.
This is because WUFFS is a special purpose language. There doesn't need to be a way to write these nonsense programs in WUFFS, whereas it must be possible in a language like C to achieve the "general purpose" designator. But this should encourage us to write as little as possible with these unnecessarily powerful languages, like the way you don't use a chainsaw to sharpen pencils.
Some languages are easier to write broken code in than others.
C is a great language, but it is absurdly common to accidentally write code that writes garbage to nearby memory, or can be exploited to do so. Such bugs are the cause of a vast number of security problems. This characteristic of C is simply not true of most other languages.
Sure, valgrind exists, but that is not native execution.
So in many situations the Sanitizer gives you a false sense of security that your code is correct because in the test environment, with the Sanitizer, it works, and then in Production, without a Sanitizer, under some circumstances it blows up because those circumstances never arose in your test environment.
Compare (safe) Rust which doesn't have uninitialized variables, and even WUFFS which doesn't have array bounds errors. In both cases the entire problem doesn't exist, programs which exhibit this problem are not valid programs in those languages, which is great because we did not mean to write these programs anyway. Hooray.
The cause of the error is.
The errors are not the problem, but the result of the underlying problem.
I assert it’s possible to have a simple language where it’s difficult to shoot yourself in the foot. C just happens to be a simple language where it’s exceedingly easy to shoot yourself in the foot.
Zig would be one such example.
Zig just provides different guarantees.
That only in matter of complexity. If rust were actually comparable to C++ it will a major mess.
Rust is actually a ML + C + Haskell-ish lang..
Rust is not "theoretical", but have some theory applied to great effect. The comparison to Haskell is more in the use functional idioms, algebraic types, pattern matching and bits here and there...
Pay no attention to how the language was derived. Just dive into it.
Anyway it’s simultaneously a very simplified cousin of Haskell (similar syntax but most of the advanced syntactic and type stuff dropped), and where the current movement to make compiler error message more readable, conversational, and helpful (e.g. decoration, explaining the “reasoning”, providing possible solutions) originates.
Those who aren't careful enough to free up allocated memory are the same types of people who leak memory in Java.
You can't fix lack of knowledge with a compiler. You can only catch accidental mistakes, but that assumes that the norm is for the person to know what they are doing and a lot of people have only superficial knowledge of the tools they are using.
We already have accident catching tools for C. What C doesn't have is a helmet, training wheels and padded walls, which is by design.
Those who aren't careful enough to only flip the bits they intend to when programming with a magnetized needle are the same types of people who overflow buffers in C.
You can't fix lack of knowledge with a compiler. You can only catch accidental mistakes, but that assumes that the norm is for the person to know what they are doing and a lot of people have only superficial knowledge of the tools they are using.
We already have accident catching tools for magnetized needles. What a magnetic needle doesn't have is a helmet, training wheels and padded walls, which is by design.
> AS IT TURNS OUT, good craftspeople don't shy away from questioning, criticizing, and trying to improve their tools (or even switching to other tools entirely!)
C is the best tool for a lot of use cases, but no tool can be a perfect fit for every use case. This is the reason that multiple programming languages exist and the world has not converged on using only C everywhere. Also, there are properties not intrinsic to the language that also affect the fit.
If the Rust fanboy brigade could articulate actual criticism for C, such as the lack of imports, or eskewing the type system with void*, or the signedness of char, or any of the other topics beaten to death in online "C gotchas" power point presentations, then we could have a constructive conversation amongst craftspeople.
If you feel that C is a good language for your needs, then by all means, use it.
I know that Rust brings me a very efficient safety net that lets me concentrate on the algorithm [1] without having to also spend quite as much time auditing, fuzzing, valgrinding and {thread, memory, ...} sanitizing my code. In addition to the safety added by the type-system, I also enjoy the convenience of tools such as `Drop`, or the ability to derive (de)serializers for most formats, etc.
This is not about criticizing C. This is about picking the best tool for the job. Sometimes, it is Rust. Sometimes, it is TypeScript. I haven't encountered a job for which C was the best tool for me in a while. I'm sure that there are cases, mind you, but I don't encounter them anymore.
[1] With caveats, of course. As all Rust users, I need to please the borrow-checker gods.
That’s a fair point. Unfortunately I think that in order to really talk about the pros and cons, we’d need a relatively specific situation. Toy examples are just that… toy examples. FWIW most of the time I will make language choices based on the expected lifetime of a particular project, the relative expertise of team members working on a project, and the ease of delivering bug fixes.
The original article and the Rust language would beg to differ. The compiler generates detailed, didactic error messages with actionable suggestions.
Seeing how contrived the examples are makes me more interested in rust.
The error messages are very good, cargo is awesome, rust analyzer is already good but still needs some time to flourish, and the performance in production is really predictable. It's for sure not perfect, but in many aspects it feels like a step forward, so much that sometimes I'm wary on how much I'm relying on the compiler to check my decisions for me.
That isn't true at all. The majority of my and other's bugs I hit working with C (I did C for several years at a large company) were because they explicitly did not fail when people forgot things. If you iteratively learn C you learn tons of bad behaviors as C does not tell you when things are broken.