The author has a unique perspective since he - somehow - skipped learning C until now. He programmed assembly before, and from his other work he clearly is familiar with dynamic and more modern languages.
Consequently, his perspective on C is that of somehow who is new to the language, yet also understands both the fundamentals of what his code will compile down to, and the higher level facilities that later languages and programming models abstracted away.
that was my experience when I went from ASM - to functional and then C when wrangling hardware. I am still surprised at how reliable C apps can be (linux kernel) - hats off to the genii involved in making things stable.
I use Linux, but I am still surprised it is really stable. At low level it's quite messed up and shows the cons of being a pseudo-bazaar ecosystem. Lately it's changing to a cathedral with a handful of core developers acting quite dictatorial and perhaps that will clean up things a bit. I'm not advocating any of both camps, just bringing up it is far from perfect.
In particular I've spent last week trying to figure out TCP/IP port management and it was quite frustrating. Lack of docs and plenty of hardcore operations unexplained, not explicitly marking all macros and inline functions, no clear distinguishing of scope for objects, and I could go on for a few dozen more lines of this.
Only a fanboy or an egomaniacal author can argue that. Don't get me wrong, I like it overall and I prefer it to most other OSs.
It should be, but usually it is just user base. Microsoft has the advantage of everybody sweating hard trying to make their things work. Their beta users are in the millions, and mostly are experienced users or even developers.
I see Linux developers talk about regression tests but couldn't find where is the central repository of this (is there one?) Regression tests IMHO should be distributed with the source code so everybody can run them. (I understand testing a kernel is not like testing any other user space program.) The BSDs do it, for example.
I remember reading about a study about a very different style of development from years ago, where automated testing was NEVER done, but instead formalized models of the program were created and mathematically verified.
If I remember correctly, the final result was a similar level of quality to an extensively tested system developed today, and the code tended to be of somewhat higher quality thanks to the extra thought put into it. (But I'm only going off vague memories here... I might be misremembering)
That's not an exclusive or though. If you have a proof of your code's correctness, I'd still like to see it being regressed against any changes to the code. Ideally the more different methods you have for validating that your code is doing what it's supposed to be doing, the better.
I'm actually quite disappointed to see my original comment with a negative score. I'm going to assume it was just badly phrased, as I don't seriously think that anyone believes that more testing of software results in a decrease in quality.
Of course it doesn't make software worse, but you make it sound like it's the only factor in software quality. Far from it, and IMO the degree to which it has become a religion is disturbing. I suspect that might be the source of the downmods.
I've seen projects with more tests than code, and I think that shows a problem with development methodologies. Simply reasoning about the flow through the code (and possibly using a powerful type system [think Ocaml] to help catch errors at compile time) would remove the need for many of these tests (not all tests should go, of course) and would result in tighter code.
Yeah, if you have more tests than actual code, that would seem to indicate something's gone wrong somewhere. And I'd agree that no amount of testing is going to turn an O(n) algorithm into an O(log n) one. There's no substitute for sitting down with a pen and paper and doing some old fashioned design sometimes.
I do think having a suite of repeatable test cases you can run against developing software is a useful thing to have, though. Not only can you test for correctness, but you can also run benchmarks against each modification to see if your performance or memory usage is going up or down.
It probably depends on what you're trying to do...
In contrast, I consider the Linux kernel code clear and clean. I have mostly looked through the scheduling and file system code; I can't speak for TCP/IP port management.
First, I'll grant that there's very little internal documentation. This hurts. But I find I can still figure out what's going on by reading the code, and that the really obtuse things have comments.
The coding style is consistent - I don't think I've ever seen a single deviation. Linux kernel code is fit for print. This helps enormously with readability. It's rare to find such a large codebase written by so many people that uniform.
The directory structure and file names are straightforward. Want to know where the main scheduling code is? kernel/sched.c. What about the data structures for scheduling? include/linux/sched.h. How is fork implemented? kernel/fork.c. This is important because it means the source is discoverable.
Functions are just the right length. A single function conceptually does one thing, as it should. I've never read through a function and thought "This should be factored out into several different functions."
Consistent and intuitive names. Even if I've never read a function, or seen the definition of a variable, I have a good idea of what it does just by the name. This is should be true for all code, but considering the size and complexity of the code, I find it impressive I've never thought "That's a stupid name." It helps that special functions follow certain naming conventions. For example, if I encounter a function named do_foo(), I know that it is the function called for a foo system call.
And most importantly, any time I've wanted to know how something works in the Linux kernel, I've always been able to figure it out with focused study and tracing. Focused study, not a heroic effort. If it takes longer than I thought it would, it's always because I had to learn a few new concepts along the way, not because the code was obtuse.
Reading the Linux kernel code helps if you do it using LXR, http://lxr.linux.no/, which does a cross-reference of the code. Most variable names, functions and structures are links to their definitions and occurrences.
There is an "aesthetic" to the kernel code I now realise (after having had it explained) - whilst some of it is no doubt odd looking, most of it is done that way for a reason, and reasonably consistently. But not to everyones taste.
I'm not a style-nazi, any of the basic styles is fine for me (e.g. indent and keep the same style in all source.)
My dislikes are more pragmatic, no good comments, packing multiple complex operations in a single line, and not making clear what is what (OK this last bit is style but every single coding style for C out there defines this!)
[Again, I'm playing a bit Devil's Advocate as I'm a Linux user myself. There are many things I love from this team.]
I'm still not convinced that there's anything particularly magical about C here. Reliable programs are written by people who:
* understand the problem domain
* know the implementation language and its supporting library
* pay attention to detail
Admittedly, some languages fit some problem domains better than others, but 90% of the time, picking the language you're personally most familiar with will be as good a choice as any.
The thing about C is that you are generally very aware of the side effects. Aside from the libraries you use, the (data) structures are yours. When you set something to NULL, you know damn well that that means to you.
As the author alludes to as well - in C you're made more aware of the error conditions you can handle and the ones you can't. So you can code to a level of robustness... Exceptions in Java are all well and good, but I haven't seen many implementations that do anything with IOException except for cascade it.
This works really well for programs/modules that can fit in the head of a single programmer. When you go beyond that it gets pretty messy - which is where some of the advantages of metaphors like OO start to help... Course, there is the argument that modules should never get that big, but that's another debate.
"As the author alludes to as well - in C you're made more aware of the error conditions you can handle and the ones you can't."
You mean like when a function silently returns -1 to indicate failure and then you wonder why your program returned a wrong result (if you're lucky enough to even notice)? In the bigger part of most of my programs I want a big, flashy, loud, total failure by default if anything goes wrong.
The only time a function would "silently" return a -1 would be if you're not checking your returns for errors! Functions have return codes for a reason.
What you're saying is that you want your programs to blow up when a function returns a known error (such as "disk full").
One of the points of the article is that C makes you think about all the possible, well-documented, errors that each function can encounter. You know when you write C precisely what can go wrong at each stage, and decide the right way to handle it.
Seeing a dialog box pop up with some exception backtrace is one of the things that annoys me about 'modern' Java-esque languages: they seem to encourage developers to default to ignoring most exceptions.
Interestingly, I noticed earlier this week that the new GCC gives warnings when the return values of POSIX / libc functions are ignored but shouldn't be.
Which versions of gcc and libc? Any special flags passed to gcc? I ask because I just tried it with a test program here (openSUSE Factory, gcc 4.3.3 and glibc 2.8.90) and it didn't print any warnings, but it would be nice if it did.
The difference between letting an exception bubble up and seeing a system call fail and aborting is pretty small. Either way, you need a higher level way of dealing with it.
One of the most interesting papers I read was "Why computers fail and what we can do about it" by Jim Gray while he was at Tandem. Transactions are essential, and most failures are transient. This can be applied at many levels, and can deal with many failures without the developer of the mainline code having to decide what to do for each given failure.
Exactly. In properly written C, about half your code is checking for errors and handling them explicitly. If you don't do that, at some point it will blow your foot off; you have to account for those errors. C programmers have generally accepted this and thus spend an excessive amount of coding time thinking about and writing recovery mechanisms for external errors -- and that's why so many C programs are incredibly stable.
That won't scale. I'm sure you'll have lots of fun grepping through your source files looking for "!$!$&^!$!!!!" when it appears on your stderr. The point is that with an exception system you don't have to write stupid shit like that everywhere just to crash and burn correctly.
Failing loudly is the only sensible behavior when an unexpected error occurs and there's absolutely no indication of what to do in such a case.
From what breif comments I've read on Lisp's condition system, it sounds much better. Containing and isolating the error, as soon as possible, then passing information about the error higher up like an exception might propagate, until a level high enough to know the goal of the operation and what to do in case of errors, then passing a response back down, and being able to continue in a useful manner or tidy up gracefully.
A great big codesplosion spraying you with a several hundred lines of stack trace and a "Your program has encountered an error an needs to close" is not "sensible behaviour".
"A great big codesplosion spraying you with a several hundred lines of stack trace and a "Your program has encountered an error an needs to close" is not "sensible behaviour"."
Yes it is, if it's a program in development where I haven't yet decided what behavior an error condition should produce.
You guys don't seem to understand I don't necessarily want to decide how to handle all errors now, not that I never want to have to think about how to handle them.
setjmp/longjmp is essentially what exception handlers do for you. I love C for its simplicity, but I'm not certain that leaving exception handling out of the language was a good idea. There are a lot of people who would agree with me there, including (I believe) a few Bell labs veterans who wrote the language in the first place.
The nice thing about exceptions beyond setjmp/lngjmp (in C++ at least) is the destructor semantics which you can use to guarantee that resources are cleaned up on error. There are better ways to handle such things, such as scoped resource allocation, but exceptions do an ok job.
You mean like when a function silently returns -1 to indicate failure and then you wonder why your program returned a wrong result (if you're lucky enough to even notice)?
This is the fault of the programmer who wrote the function, not of the language.
What? What can a programmer do besides returning a "special" value such as -1 in a language like C with strict types and that doesn't support exceptions?
Sure you can ignore it. You don't want to know the number of times I've seen people using the "try, catch, toss away the exception" structure in Java code.
In OCaml, you're required to do something with the return from any non-void function. You can just say "ignore (f x)" to discard the result, so it's not hugely inconvenient, but causes suspect code to accumulate warning signs.
Well I don't know your experience, but most Java developers I've seen either throw the exception, or suppress it... If they throw it, it eventually ends up as ABCGenericException and you've probably got no chance to reasonably recover. If you suppress it you end up with a nasty side effect (a null somewhere perhaps) that causes the app to bomb in another unexpected piece of code.
Exceptions are a language semantic - in itself it doesn't make your code robust. You still need to apply it correctly.
But! The reality is the same applies to C too. What the original author was alluding to what that the mentality of C encourages coders to look deeper into their errors.
False. In languages with better type systems (e.g. ML and Haskell), you generally use the "Option" (OCaml) or "Maybe" (Haskell) type in situations like these.
(Example code is OCaml)
type 'a checked = Pass of 'a | Fail
(* very much a toy example *)
let even x = (x mod 2 == 0)
let can_fail x = match x with
| 0 -> Fail
| x when (even x) -> Pass (x * 10)
| x -> Pass (x)
# can_fail 2;;
- : int checked = Pass 20
# can_fail 3;;
- : int checked = Pass 3
# can_fail 0;;
- : int checked = Fail
#
The languages infers where you're passing around option types (such as "int checked" above) and forces the calling function to account for any potential failures. Rather than just Pass 'a and Fail, it could be Pass, File_Not_Found, Access_Denied, etc., and (Unknown_Error string). OCaml also has exceptions, but the combination of union types and required exhaustive matching covers most cases.
If, as in the case of extsmail, one wants to be robust against errors, one has to handle all possible error paths oneself.
That's not really a bad thing, as the author points out.
One thing I've always felt a sense of dread about in C# and Java (last Java I did was back in 2001) is that I never truly knew what errors were lurking with their exception handling. It would be really nice if all error possibilities were listed in the documentation so I could pick precisely what to handle.
It's even worse. For top level functions it can throw the UNION of the exceptions thrown by the functions it calls. Therefore, by changing a low level function's exception signature the exception signature of all higher level functions changes too. That always worried me, but it's doesn't seem to be a big deal in practice.
It's a huge deal with checked exceptions in Java. You have to change the signature of every caller, and so on, when you change a low-level function. Abstraction leakage galore; it's why many libraries just throw a single generic exception type whenever anything goes wrong (which defeats the purpose of exceptions) or subclass RuntimeException (which defeats the purpose of checked exceptions).
Unchecked exceptions don't seem to be a problem, because oftentimes you don't care what specifically went wrong, you just need to know that something went wrong and abort appropriately.
> It would be really nice if all error possibilities were listed in the documentation so I could pick precisely what to handle.
Yeah, that's a problem - it's a problem of style and implementation rather than a "Java" problem... but it's a problem none-the-less.
Many exceptions in Java are simply == "it didn't work". Often there is no distinguishing between: (a) didn't work because you supplied something invalid (e.g. Integer.parseInt("notanint") and (b) it didn't work, but you can probably recover if you want and (c) it didn't work, there is nothing you can do about it.
People often write in higher level languages because they want lots of bad code fast. Almost all business applications are CRUD apps (create/retrieve/update/destroy) with some business logic, and they're generally written in C#. The app may crash when you click the wrong button, but the app is cheap to develop and the programmers are easily replaceable.
Of course I'm generalizing, and a lot of C# programmers write great and reliable code. The point is that they often don't -need- to write great code, because lousy code is good enough for all business purposes (except when the software is your product, which is rarely the case).
The second issue is that people who choose C for their projects tend to (a) understand low-level concepts, (b) care about speed / memory usage / reliability / dependencies, (c) don't consider development time that important. That you create more reliable software that way is obvious - the same programmers would create reliable software in C# (or similar language). But there aren't many situations in which development speed doesn't matter, speed and memory usage don't matter, dependencies don't matter but, for some reason, reliability is very important.
I come from the systems research world. I've seen people choose C not for the reasons you mention, but just because it's what they know best. This is not always the best thing to do.
One instance is a colleague who needed to do data post-processing, and just did it in C because it was most familiar. A language like Perl or Python would have been a better choice, and saved him time in the long run, since string manipulation in C is tedious and error prone.
I am not sure why are you bringing enterprise software into the discussion. Despite being the most popular form of employment for "programmers" it has always been the absolutely lowest form of life in a software ecosystem and, therefore, should be ignored and left out of any intelligent discussion about programming.
In the 90s it was Visual Basic, now it's Java and C#, but it has absolutely nothing to do with what most of us consider to be discussion worthy.
So? Despite being useful, what's so interesting about CRUD programming for the enterprise? I've been there, I've had my share of dealing with coworkers that openly admit that they haven't touched a single book since graduation 10 years ago and they see no reason why would they want to.
That's the kind of programmers this industry attracts, and that's the kind of software it builds. What's so interesting about it? Why even bother mentioning these numerous java/C# jobs? My ex-wife with zero programming experience has trained herself in less than a month to run a simple SQL queries in Visual basic and blast results in a grid control on a form, so did thousands of ex-taxi drivers in late 90s. So?
I think how interesting it is depends on your interest.
If you are heavily into programming, as it seems you are, I can understand that SQL queries seem trivial and boring. If your interest is elsewhere programming is often just a tool used to create the interesting stuff. Whether that is usability, design, getting people to interact in new ways, launching rockets or automatically turning off your garden lights when your computer powers down.
Believe it or not there are interesting discussions to be had about other things than programming.
> My ex-wife with zero programming experience has trained herself in less than a month to run a simple SQL queries in Visual basic and blast results in a grid control on a form, so did thousands of ex-taxi drivers in late 90s. So?
To be honest, I'm still not sure what point you're trying to make is. At best it's somewhat of an Ad Hominem attack.
"People often write in higher level languages because they want lots of bad code fast."
Even if this is true, it's true only in enterprise software environment where code quality has never been terribly important. Therefore it can't really be an argument against higher level languages in the context of a typical HN discussion.
I'm in no way trolling here, but if you think about it, most of the guys working on startups are also producing lots of bad code fast. The only difference is that most of them are aware of it, and will improve the code based on the requirements from the market.
Interesting perspective. I was programming a long time in C, C++ and C#, and I don't feel that programming in C# is easier in some fundamental way, or that C# programmers are easily replaceable. Yes, the mode of thinking is different in each language, but the issue that you have to think carefully is basically the same. ;-)
People often write in higher level languages because they want lots of bad code fast.
I assume you simply meant programmers who want to churn out something quick prefer high level languages. (Majority of them would never like to write 'bad' code intentionally)
Your second assertion is spot on. If one needs to develop a high performing solution optimized for speed and memory consumption, plus high reliability and if it also needs to be reasonably cross platform, one would gravitate towards C. Add to this the established heritage/ecosystem and the tools, and availability of a (relatively) big pool of programmers, C tends to be a good choice.
The only con is a relatively longer development cycle. (True only for mortals like me though, some people stream out C code faster than i write an email. :-) )
Just do what the gaming industry has been doing for awhile (in various ways).... core libraries / engine in C/C++ with higher-level logic in a higher-level scripting language.
This gives you the advantages of a high level language where the extra overhead and consequences for coding a little poorly aren't as detrimental.
Yes, we are doing something similar currently. A combination of C & Ruby is working out quite well. (The product is for the Ruby domain so the high level language choice was very straightforward.)
Yes, lots of bad code fast is half the reason I use Scheme almost exclusively now after 12 years of C. The other half is converting bad code to good code fast.
Considering it's the first (alpha) release of the first significant C program he's written which is for a new (and therefore little used) programming language, and the page of Converge tools has no mention of testing tools, and the page itself has error messages on it, the claim that it's "not riddled with bugs" seems a touch, erm, cheerful.
Programs compiled from C don't change much when run, while dynamic applications vary significantly on every run due to the large environment (e.g. GC.) This is a bliss for debugging. I've never seen anything like gdb on any other language. You can backtrack, set and change things, watch for expressions, and even run one line of compiled code at a time to see what's breaking.
Also C programs usually are made to run many times and stay alive, so the attitude of the developer tends to be more careful.
The problem with C is they left too many things completely on the wild. Strings and memory management are all laissez-faire and everybody does whatever they think is right.
A typical performance issue is calling malloc/free all the time, it can be avoided but there are no standard ways.
I hope some great features of C++ get some day backported to C. But don't bet money on that :(
C believes that programmer doesn't make mistakes. About memory management, I don't think it should be a language feature because there are some good implemented libraries out there.
I used to do research with multithreaded memory allocators. The best of class is, I think, TCMalloc, which is a part of Google's perftools: http://code.google.com/p/google-perftools/.
Then there's my work, which is frankly not suitable for using in a real application since it's tested as heavily as the other two I mentioned: http://people.cs.vt.edu/~scschnei/streamflow/
The later C standards add additional features. However, I'd argue that there is no such thing as a "great feature" in C++: almost everything in the language is a mistake, either intrinsically or in combination with other 'feature's.
It's a blind pig with fifteen legs, trying to put on its own lipstick while riding a unicycle.
Spoken like someone who truly doesn't understand C. You even managed to confuse it with C++ (you might as well have said "Java" in the place of C++, it's about as close to C as C++ is)
Well done.
edit: Ignore this post. I misread. I'm an idiot. Sorry about that
The language is quite overcomplicated, yes. But there are many mitigations making the whole thing quite appealing.
C++ template libraries are cleaner, easier to improve and adapt, and often faster than the C counterparts (when there are counterparts!) A good example was how OpenMP was added to the basic types in the STL (on GNU/gcc/libc++) by a quite small group of people last year.
Another example are the regular expression libraries.
I'm not an Stroustroup/C++ apologist but this language is the absolute best in several traits. I like to code in Python the most but C and C++ are still fun and make sense in their own way. I hate languages that make programming hard just because they follow an overcomplicated paradigm and don't give you anything useful back from that (like Java, IMNSHO.) In particular all languages with multi-million dollar advertisement campaign just piss me off (the language, not the tools.)
C is, as the author points out, a portable assembly language. If a portable assembly language is not what you need - for example, you want better string support and memory management - then don't use C.
Then why the authors did a standard library from the beginning? (including strings) The problem is it's quite dated (30 years). There isn't anything wrong with the language itself and most languages evolve with time.
Because back when C first was designed and implemented, the alternative was to do string manipulation in assembly.
C has evolved over time, but it has not left its niche: a systems programming language which is a thin abstraction of hardware. If you want a higher abstraction of the hardware, then C is not your best choice.
> What I realised is that neither exception-based approach is appropriate when one wishes to make software as robust as possible.
The author misses the main point of exceptions: they let us separate data processing code from error handling code. This is why we are more productive in languages that have exceptions and our code is easier to maintain.
C requires us to handle errors throughout our program, tightly coupling the data processing code with the error handling code. With apologies to Mr. Spencer, I would declare that:
"Those who don't code in languages that lack exceptions are doomed to reimplement them, poorly."
I have to disagree. From my perspective it is an error to think that error handling can be handled separately from the main code path. The classic example that I use to demonstrate this is to point to all of those tedious discussions that programmers have as to whether something is an exception or just normal behaviour of a system. Straight away for me that's a red flag that a non-real distinction is being made.
I honestly don't see any advantage to exceptions over C-style return codes, with one important ...euh... exception: the boiler plate for exception handlers can be well handled by modern IDEs. All the other supposed advantages seem to be just waffling to me. Take the whole 'Oh, but exceptions make handling errors the default!' kind of argument (several examples on this thread already). Yes, sure, you do have to write exception handlers for all errors in a language such as Java. But my experience is that if I'm writing use-once-and-throw-away in C, I'll just not use the return code. In Java I'll just stick a whopping great big try/catch around the whole app, and be done with it. If I'm trying to write stable code that's going to be around for a while in C, I check the error codes returned by a function every single time, which gives me around about as much work as when I am using Java, and actually handling different exceptions correctly.
All of which means, for me at least, that exceptions don't add anything to a language, but they do make the language just a little bit harder to learn (remembering exactly how any given language has implemented exceptions, and which resources can still be safely used when is a pain, as each language tends to have subtle differences that can bite you).
- A whole lot of C code is old. Old, actively maintained code, tends to be more reliable than new code.
- C tends to be employed in relatively predictable sub-systems. Something like a device driver has a relatively predictable set of states relative to a GUI application. There aren't that many paths. C code for GUIs, in my experience, tends to be at least a buggy as code in other languages.
You seem to forget that in embedded systems, C is still the most used language. So most of the new systems developed these days have new C code. And embedded systems are everywhere.
Also a big part of embedded systems have to be reliable for years in hostile environement without external interventions. so I wouldn't say that this is easily predictable subsystems.
I'll qualify this with the fact that I've never done any embedded programming, but my feeling is, that relative to a GUI application, that things are still fairly predictable.
Unpredictability seems to arise mostly from interuptability. GUI applications have the ability to jump from a large bank of states to another large bank of states with almost endless permutations and maintain several, not one state simultaneously.
At least in systems programming and I'd assume embedded programming (and like I said, I'm guessing -- I'd be interested in your take there) the chain of interruptability is much more structured and the logic flow inside of components is much more atomic.
Hmmm, I've worked in the embedded world for the last 10 years, and I can tell you that the bulk of embedded code is far from hardened. It appalls me the number of times that mobile phones, set top boxes, etc crash. Typically, reliability is low because it is hard to run decent unit tests in an embedded environment. You're obliged to write a type of simulator that you can run on a PC, but the simulator never has the exact same behaviour as the target system, so you can never truly verify correct code behaviour.
Yes, this is true: A lot of embedded code is poorly tested. Even more is poorly designed and written by poor programmers (like most software.)
Some people have to deal with the machine, and C is a good model of the machine. Even reliable systems have some core in C (or an equivalent) that provides an abstraction where higher level abstractions can be expressed.
Testing isn't the ultimate solution; some people are just able to use the machine better and produce better code. Testing crappy code doesn't really help. A subset of developers produce tools where others can produce systems with less risk by providing appropriate abstractions.
C has been gaining traction in embedded avionics software, which is purposefully very simple, understandable, maintainable code, and tested & verified beyond nearly any other type of software.
C is also used on mobile phones, where it's more hit & miss, as I'm sure many of us have experienced first-hand...
In some cases old, actively maintained code is more reliable. However, I have seen many cases where old, actively maintained code (depending highly on the quality of the maintainers) has lost reliability because the original principles of the codebase have been lost.
Device drivers (especially in multiprocessor systems) have very many unpredictable paths. In my experiences in writing devices drivers and GUI code code (as well as lots of code in the middle), device drivers are more likely to have the unforeseen codepath. With GUI code, you can constrain concurrency so that possible codepaths are also reduced.
After having read a lot of these types of language debates, the only conclusions I can safely arrive at are 1) that every language has its advocates and 2) some people are highly productive in their preferred language, much more so than the average programmer would be in their preferred language. But the real question, imho, is how do average programmers compare? How will the same app written in C++ and Java compare, when written by non-superstars? The question merits some empirical research.
As a person who spent few years doing C programming, and then got trapped into the Ruby realm, I must say that the main difference is focus. If you have to focus about memory management, strings, you think much more about the code you write.
Theoretically, high level programming should enable you to focus on the abstractions much more, but somehow it doesn't work this way.
104 comments
[ 5.7 ms ] story [ 185 ms ] threadConsequently, his perspective on C is that of somehow who is new to the language, yet also understands both the fundamentals of what his code will compile down to, and the higher level facilities that later languages and programming models abstracted away.
In particular I've spent last week trying to figure out TCP/IP port management and it was quite frustrating. Lack of docs and plenty of hardcore operations unexplained, not explicitly marking all macros and inline functions, no clear distinguishing of scope for objects, and I could go on for a few dozen more lines of this.
Only a fanboy or an egomaniacal author can argue that. Don't get me wrong, I like it overall and I prefer it to most other OSs.
I see Linux developers talk about regression tests but couldn't find where is the central repository of this (is there one?) Regression tests IMHO should be distributed with the source code so everybody can run them. (I understand testing a kernel is not like testing any other user space program.) The BSDs do it, for example.
I remember reading about a study about a very different style of development from years ago, where automated testing was NEVER done, but instead formalized models of the program were created and mathematically verified.
If I remember correctly, the final result was a similar level of quality to an extensively tested system developed today, and the code tended to be of somewhat higher quality thanks to the extra thought put into it. (But I'm only going off vague memories here... I might be misremembering)
I'm actually quite disappointed to see my original comment with a negative score. I'm going to assume it was just badly phrased, as I don't seriously think that anyone believes that more testing of software results in a decrease in quality.
I've seen projects with more tests than code, and I think that shows a problem with development methodologies. Simply reasoning about the flow through the code (and possibly using a powerful type system [think Ocaml] to help catch errors at compile time) would remove the need for many of these tests (not all tests should go, of course) and would result in tighter code.
I do think having a suite of repeatable test cases you can run against developing software is a useful thing to have, though. Not only can you test for correctness, but you can also run benchmarks against each modification to see if your performance or memory usage is going up or down.
It probably depends on what you're trying to do...
The coding style is consistent - I don't think I've ever seen a single deviation. Linux kernel code is fit for print. This helps enormously with readability. It's rare to find such a large codebase written by so many people that uniform.
The directory structure and file names are straightforward. Want to know where the main scheduling code is? kernel/sched.c. What about the data structures for scheduling? include/linux/sched.h. How is fork implemented? kernel/fork.c. This is important because it means the source is discoverable.
Functions are just the right length. A single function conceptually does one thing, as it should. I've never read through a function and thought "This should be factored out into several different functions."
Consistent and intuitive names. Even if I've never read a function, or seen the definition of a variable, I have a good idea of what it does just by the name. This is should be true for all code, but considering the size and complexity of the code, I find it impressive I've never thought "That's a stupid name." It helps that special functions follow certain naming conventions. For example, if I encounter a function named do_foo(), I know that it is the function called for a foo system call.
And most importantly, any time I've wanted to know how something works in the Linux kernel, I've always been able to figure it out with focused study and tracing. Focused study, not a heroic effort. If it takes longer than I thought it would, it's always because I had to learn a few new concepts along the way, not because the code was obtuse.
Reading the Linux kernel code helps if you do it using LXR, http://lxr.linux.no/, which does a cross-reference of the code. Most variable names, functions and structures are links to their definitions and occurrences.
My dislikes are more pragmatic, no good comments, packing multiple complex operations in a single line, and not making clear what is what (OK this last bit is style but every single coding style for C out there defines this!)
[Again, I'm playing a bit Devil's Advocate as I'm a Linux user myself. There are many things I love from this team.]
* understand the problem domain
* know the implementation language and its supporting library
* pay attention to detail
Admittedly, some languages fit some problem domains better than others, but 90% of the time, picking the language you're personally most familiar with will be as good a choice as any.
As the author alludes to as well - in C you're made more aware of the error conditions you can handle and the ones you can't. So you can code to a level of robustness... Exceptions in Java are all well and good, but I haven't seen many implementations that do anything with IOException except for cascade it.
This works really well for programs/modules that can fit in the head of a single programmer. When you go beyond that it gets pretty messy - which is where some of the advantages of metaphors like OO start to help... Course, there is the argument that modules should never get that big, but that's another debate.
You mean like when a function silently returns -1 to indicate failure and then you wonder why your program returned a wrong result (if you're lucky enough to even notice)? In the bigger part of most of my programs I want a big, flashy, loud, total failure by default if anything goes wrong.
What you're saying is that you want your programs to blow up when a function returns a known error (such as "disk full").
One of the points of the article is that C makes you think about all the possible, well-documented, errors that each function can encounter. You know when you write C precisely what can go wrong at each stage, and decide the right way to handle it.
Seeing a dialog box pop up with some exception backtrace is one of the things that annoys me about 'modern' Java-esque languages: they seem to encourage developers to default to ignoring most exceptions.
I think the space between to two is more uncomfortable than either extreme.
One of the most interesting papers I read was "Why computers fail and what we can do about it" by Jim Gray while he was at Tandem. Transactions are essential, and most failures are transient. This can be applied at many levels, and can deal with many failures without the developer of the mainline code having to decide what to do for each given failure.
Failing loudly is the only sensible behavior when an unexpected error occurs and there's absolutely no indication of what to do in such a case.
tyger(~)% cat << EOF > main.c
? #include <assert.h>
?
? void main( void )
? {
? assert( 0 == 1 ) ;
? }
? EOF
tyger(~)% gcc main.c
main.c: In function ‘main’:
main.c:4: warning: return type of ‘main’ is not ‘int’
tyger(~)% ./a.out
Assertion failed: (0 == 1), function main, file main.c, line 5.
Abort
tyger(~)%
A great big codesplosion spraying you with a several hundred lines of stack trace and a "Your program has encountered an error an needs to close" is not "sensible behaviour".
Yes it is, if it's a program in development where I haven't yet decided what behavior an error condition should produce.
You guys don't seem to understand I don't necessarily want to decide how to handle all errors now, not that I never want to have to think about how to handle them.
http://www.on-time.com/ddj0011.htm
I'm almost tempted to make an analogy with scheme's (call-with-current-continuation) here, but I think that might be pushing it.
This is the fault of the programmer who wrote the function, not of the language.
By the same token, dozens of Java libraries only throw ABCException, with no other information... Might as well be -1.
Whereas when you're dealing with return codes, the default is to ignore.
It seems like a good compromise.
Exceptions are a language semantic - in itself it doesn't make your code robust. You still need to apply it correctly.
But! The reality is the same applies to C too. What the original author was alluding to what that the mentality of C encourages coders to look deeper into their errors.
(Example code is OCaml)
The languages infers where you're passing around option types (such as "int checked" above) and forces the calling function to account for any potential failures. Rather than just Pass 'a and Fail, it could be Pass, File_Not_Found, Access_Denied, etc., and (Unknown_Error string). OCaml also has exceptions, but the combination of union types and required exhaustive matching covers most cases.That's not really a bad thing, as the author points out.
One thing I've always felt a sense of dread about in C# and Java (last Java I did was back in 2001) is that I never truly knew what errors were lurking with their exception handling. It would be really nice if all error possibilities were listed in the documentation so I could pick precisely what to handle.
Unchecked exceptions don't seem to be a problem, because oftentimes you don't care what specifically went wrong, you just need to know that something went wrong and abort appropriately.
Yeah, that's a problem - it's a problem of style and implementation rather than a "Java" problem... but it's a problem none-the-less.
Many exceptions in Java are simply == "it didn't work". Often there is no distinguishing between: (a) didn't work because you supplied something invalid (e.g. Integer.parseInt("notanint") and (b) it didn't work, but you can probably recover if you want and (c) it didn't work, there is nothing you can do about it.
Of course I'm generalizing, and a lot of C# programmers write great and reliable code. The point is that they often don't -need- to write great code, because lousy code is good enough for all business purposes (except when the software is your product, which is rarely the case).
The second issue is that people who choose C for their projects tend to (a) understand low-level concepts, (b) care about speed / memory usage / reliability / dependencies, (c) don't consider development time that important. That you create more reliable software that way is obvious - the same programmers would create reliable software in C# (or similar language). But there aren't many situations in which development speed doesn't matter, speed and memory usage don't matter, dependencies don't matter but, for some reason, reliability is very important.
One instance is a colleague who needed to do data post-processing, and just did it in C because it was most familiar. A language like Perl or Python would have been a better choice, and saved him time in the long run, since string manipulation in C is tedious and error prone.
But I agree, C isn't the best choice for that task.
In the 90s it was Visual Basic, now it's Java and C#, but it has absolutely nothing to do with what most of us consider to be discussion worthy.
It just runs our banks, hospitals, governments... Much better to solely focus on those that write kernels, device drivers and filesystems?
That's the kind of programmers this industry attracts, and that's the kind of software it builds. What's so interesting about it? Why even bother mentioning these numerous java/C# jobs? My ex-wife with zero programming experience has trained herself in less than a month to run a simple SQL queries in Visual basic and blast results in a grid control on a form, so did thousands of ex-taxi drivers in late 90s. So?
If you are heavily into programming, as it seems you are, I can understand that SQL queries seem trivial and boring. If your interest is elsewhere programming is often just a tool used to create the interesting stuff. Whether that is usability, design, getting people to interact in new ways, launching rockets or automatically turning off your garden lights when your computer powers down.
Believe it or not there are interesting discussions to be had about other things than programming.
To be honest, I'm still not sure what point you're trying to make is. At best it's somewhat of an Ad Hominem attack.
"People often write in higher level languages because they want lots of bad code fast."
Even if this is true, it's true only in enterprise software environment where code quality has never been terribly important. Therefore it can't really be an argument against higher level languages in the context of a typical HN discussion.
Makes sense?
I'm in no way trolling here, but if you think about it, most of the guys working on startups are also producing lots of bad code fast. The only difference is that most of them are aware of it, and will improve the code based on the requirements from the market.
Your second assertion is spot on. If one needs to develop a high performing solution optimized for speed and memory consumption, plus high reliability and if it also needs to be reasonably cross platform, one would gravitate towards C. Add to this the established heritage/ecosystem and the tools, and availability of a (relatively) big pool of programmers, C tends to be a good choice.
The only con is a relatively longer development cycle. (True only for mortals like me though, some people stream out C code faster than i write an email. :-) )
This gives you the advantages of a high level language where the extra overhead and consequences for coding a little poorly aren't as detrimental.
Also C programs usually are made to run many times and stay alive, so the attitude of the developer tends to be more careful.
A typical performance issue is calling malloc/free all the time, it can be avoided but there are no standard ways.
I hope some great features of C++ get some day backported to C. But don't bet money on that :(
I tend to see all major projects end up doing things their way: OpenSSL, Apache HTTPd, GCC, Linux, BSD, for example.
Hoard is also suitable for use in real applications: http://www.hoard.org/
Then there's my work, which is frankly not suitable for using in a real application since it's tested as heavily as the other two I mentioned: http://people.cs.vt.edu/~scschnei/streamflow/
But my original point was there is no stantard way. It should be part of CXX, libc, or POSIX, IMHO. C is just too atomized.
Also, I can't edit my post, but that should say "it's not tested as heavily."
[yeah, I guessed you meant that from the sentence structure]
It's a blind pig with fifteen legs, trying to put on its own lipstick while riding a unicycle.
Well done.
edit: Ignore this post. I misread. I'm an idiot. Sorry about that
C++ template libraries are cleaner, easier to improve and adapt, and often faster than the C counterparts (when there are counterparts!) A good example was how OpenMP was added to the basic types in the STL (on GNU/gcc/libc++) by a quite small group of people last year.
Another example are the regular expression libraries.
I'm not an Stroustroup/C++ apologist but this language is the absolute best in several traits. I like to code in Python the most but C and C++ are still fun and make sense in their own way. I hate languages that make programming hard just because they follow an overcomplicated paradigm and don't give you anything useful back from that (like Java, IMNSHO.) In particular all languages with multi-million dollar advertisement campaign just piss me off (the language, not the tools.)
C has evolved over time, but it has not left its niche: a systems programming language which is a thin abstraction of hardware. If you want a higher abstraction of the hardware, then C is not your best choice.
The author misses the main point of exceptions: they let us separate data processing code from error handling code. This is why we are more productive in languages that have exceptions and our code is easier to maintain.
C requires us to handle errors throughout our program, tightly coupling the data processing code with the error handling code. With apologies to Mr. Spencer, I would declare that:
"Those who don't code in languages that lack exceptions are doomed to reimplement them, poorly."
I honestly don't see any advantage to exceptions over C-style return codes, with one important ...euh... exception: the boiler plate for exception handlers can be well handled by modern IDEs. All the other supposed advantages seem to be just waffling to me. Take the whole 'Oh, but exceptions make handling errors the default!' kind of argument (several examples on this thread already). Yes, sure, you do have to write exception handlers for all errors in a language such as Java. But my experience is that if I'm writing use-once-and-throw-away in C, I'll just not use the return code. In Java I'll just stick a whopping great big try/catch around the whole app, and be done with it. If I'm trying to write stable code that's going to be around for a while in C, I check the error codes returned by a function every single time, which gives me around about as much work as when I am using Java, and actually handling different exceptions correctly.
All of which means, for me at least, that exceptions don't add anything to a language, but they do make the language just a little bit harder to learn (remembering exactly how any given language has implemented exceptions, and which resources can still be safely used when is a pain, as each language tends to have subtle differences that can bite you).
- A whole lot of C code is old. Old, actively maintained code, tends to be more reliable than new code.
- C tends to be employed in relatively predictable sub-systems. Something like a device driver has a relatively predictable set of states relative to a GUI application. There aren't that many paths. C code for GUIs, in my experience, tends to be at least a buggy as code in other languages.
Also a big part of embedded systems have to be reliable for years in hostile environement without external interventions. so I wouldn't say that this is easily predictable subsystems.
Unpredictability seems to arise mostly from interuptability. GUI applications have the ability to jump from a large bank of states to another large bank of states with almost endless permutations and maintain several, not one state simultaneously.
At least in systems programming and I'd assume embedded programming (and like I said, I'm guessing -- I'd be interested in your take there) the chain of interruptability is much more structured and the logic flow inside of components is much more atomic.
Some people have to deal with the machine, and C is a good model of the machine. Even reliable systems have some core in C (or an equivalent) that provides an abstraction where higher level abstractions can be expressed.
Testing isn't the ultimate solution; some people are just able to use the machine better and produce better code. Testing crappy code doesn't really help. A subset of developers produce tools where others can produce systems with less risk by providing appropriate abstractions.
C has been gaining traction in embedded avionics software, which is purposefully very simple, understandable, maintainable code, and tested & verified beyond nearly any other type of software.
C is also used on mobile phones, where it's more hit & miss, as I'm sure many of us have experienced first-hand...
In some cases old, actively maintained code is more reliable. However, I have seen many cases where old, actively maintained code (depending highly on the quality of the maintainers) has lost reliability because the original principles of the codebase have been lost.
Device drivers (especially in multiprocessor systems) have very many unpredictable paths. In my experiences in writing devices drivers and GUI code code (as well as lots of code in the middle), device drivers are more likely to have the unforeseen codepath. With GUI code, you can constrain concurrency so that possible codepaths are also reduced.
Theoretically, high level programming should enable you to focus on the abstractions much more, but somehow it doesn't work this way.