Hacker News new | past | comments | ask | show | jobs | submit login

I have seen this argument numerous times before.

It doesn't hold water, because potentially initializing variables to wrong values is just as much as consequence of using that static tool, as it is a consequence of an "always initialize" coding habit.

The static tool produces a report of uninitialized variables or potentially uninitialized ones. The developers react to this by fixing all those instances, so that the tool is "shut up". Once that happens, they have a code base for which the tool doesn't report anything, just like the other developers who always initialize.

There is no reason to believe that either team has a lower or higher rate of incorrect-initial-value bugs. Or is there?

I would argue that the developer who is writing the code, at that moment, quite possibly has a better idea of what the correct initial value should be, than someone who is running a tool across a large code base and following up (by him or herself).

Really, the coding convention should be "always initialize, with the correct value which doesn't need to be overwritten later."

Even in imperative languages, we should program as "functionally" as is reasonably possible.

Imperative code that mutates local variables should only do so if it implements iteration. If the code has no loops and is setting local variables conditionally, that shows that it's trying to influence the behavior of some common code where the control flow paths merge --- instead of putting it into a function!

In the specific program we are looking at, for instance, the function is trying to use the head processing block at the end, but over different input streams. This can be done by putting it into a function, so then there is no assignment to the fp variable:

   if (!*argv) {
     head_processing(stdin);
   } else {
     FILE *fp = fopen(*argv, "r");
     /* check for fopen failure ... */
     head_processing(fp);
     fclose(fp);
   }
We can eliminate assignments out of loops, too, but that requires tail recursion:

   /* pseudo code */
   head(int argc, char **argv, int first_time)
   {
     if (argc == 0 && first_time) {
       return head_processing(stdin);
     } else if (argc == 0) {
       return 0;
     } else {
       FILE *fp = fopen(*argv, "r");
       /* head_processing checks for null */
       int status = head_processing(fp);
       fclose(fp);
       
       /* tail call, if status was good */
       return (status != 0) 
              ? status : head(argc - 1, argv + 1, 0); 
     }
   }
C programmers will tend not to be comfortable with this style, and it requires tail call support in the compiler to not generate O(N) stack. So for pragmatic reasons, you can't make a coding convention out of this.



No tool will help you much if you don't know how to use it. In this case, using the tool wrong, one failure mode puts you in the same situation as not using the tool and defensively initializing. That is a very poor argument that the tool does no good.


That argument isn't that the tool does no good. The tool is useful even if you follow the practice of always initializing: it helps catch accidental deviations from that practice.


Well sure. My point is that it basically devolves into checking that if you treat it poorly, and can be more useful if you don't. If you make a habit of initializing variables with the wrong value, it's less useful.


My other comment was in response to the first half of your comment, before an edit added more about structure of code. I'd say I partly agree, but I think another consideration is that in C there is a limit to how much you can reasonably fit in an expression. What in, say, Haskell might look like...

    let f = case something of
                Foo -> 10
                Bar -> 22
cannot reasonably take the same form in C, despite there not being a "real" update going on.

This means in C you wind up writing things like,

    int f;

    switch(something) {
        case Foo:
            f = 10;
            break;
        case Bar:
            f = 22;
            break;
        default:
            /* ... handle error ... */
    }
And the flow is basically the same, but with an additional degree of freedom to make a mistake, and there some static checking can have your back.

Incidentally, on GCC and Clang I recommend building with -Wswitch-enum, which will help point you at the above when you add something to the enum that contains Foo and Bar.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: