Monday, April 21, 2014

Old C Coding Habits Die Hard

Old habits die hard.

In their 1978 book "The C Programming Language", Brian Kernighan and Dennis Ritchie described a version of C which has since become known as "K&R" C.  For those of you who aren't older than dirt, K&R C differed in many ways from modern C.  For example:

    foo(c, v)
    int c;
    char **v;
    {

Whoa!  What's that?  Original K&R C didn't let you declare the formal parameter types in-line with the function definition, you had to declare them between the ")" and the "{".  Also, functions defaulted to being of type "int".

Lots of fossilized programmers like me were forced into habits in the old days which have outlived their need.  Now, a lot of younger programmers getting out of school have as their first real-world experience the job of maintaining that old code, learning those same obsolete habits through osmosis.


LOCAL VARIABLES

In K&R C, local variables *had* to be declared at the top of the function.  With C89, locals could be declared at the start of any compound statement (i.e. after any "{").  Finally, as of C99 locals could be declared basically anywhere.

In my opinion, it makes sense for a variable to be declared and initialized immediately before (or very close to) its first usage.  Doing this conveys valuable information for a future code maintainer: this variable isn't used prior to this line.  I can't tell you how often I spent time doing reverse searches for a variable to see where else it is being used.

Here's some "old habits" code:

    foo_find(...)
    {
        int found = 0;
        ...tens of lines...
        while (! found) {

With this, you might go straight to the top of "foo()" to see how "found" is declared and initialized, but then you still have to search the tens of intervening lines to see how "found" might changed.

I prefer this:

    foo_find(...)
    {
        ...tens of lines...
        int found = 0;
        while (! found) {

Sure, there might be a 20-year-old compiler which can't handle it, but anybody using a 20-year-old compiler has bigger problems than this.


GLOBAL VARIABLES

Another habit which I think might be a K&Rism: declaration of global variables at the top of the file. I might be wrong, but I suspect that K&R C disallows global variables to be declared after some functions are defined.  As with local variables, this restriction is no longer in force.  So, if you have a group of functions which implement an abstraction that uses globals, but the abstraction is not significant enough to justify putting them in their own file, I think it makes sense to put the globals associated with the abstraction in front of the first implementation function.  For example:

    #define FOO_MAX_NUM 67
    typedef foo_t struct {...};
    foo_t foo_storage[FOO_MAX_NUM];
    static int foo_num_stored = 0;

    foo_t *foo_create(...)
    {

This code, including the #define, the typedef, and the global variables, may well be positioned after other functions are already defined.  Once again, it helps a code reader know that the definitions are not used in the preceding code.


INCLUDE FILES

In the above example, "FOO_MAX_NUM" and "foo_t" are defined close to the code, instead of at the top of the file.  But many programmers wouldn't even put them at the top of the file, they would put them in an include file "foo.h".

I don't think this is a K&Rism.  It's just a habit to put all typedefs and #defines in the include file.  But again, I advocate for keeping things as localized as possible.  Definitions and declarations which are internal implementation details to an abstraction should be hidden from general view.  C++ may do a better job of managing the external and internal details of an abstraction, but the guidelines should be followed in C as well.

No comments: