Skip to content

Comment on What is C in practice?parent

Comments

"First, there is reading an uninitialized variable (i.e. something which does not necessarily have a memory location); that should always be a compile error. Period."

You cannot implement that efficiently without rejecting valid programs. Consider code like this:

  int x;
  if( f()) x = 1;
  if( g()) h(x);
For sufficiently complex functions f and g, there's no reasonable way to decide at compile time whether x will be set whenever g() returns true. For example, f() might always return false because Fermat's last theorem is true.

And that 'reasonable' likely isn't a necessary part of that statement.

Yes, this is exactly the reason for undefined behavior. It is often overlooked in the "well, the compiler should just throw an error instead of invoking undefined behavior" debate: the compiler doesn't just "choose to invoke" undefined behavior: it relies on it to do program analysis.

For example, your example could legally be rewritten to:

    int x;
    f();
    x = 1;
    if (g()) { h(x); }
The only difference is if f() were false, and someone would then access x and see 1; but that's undefined behavior, so you can ignore it. In fact, assuming x is not accessible outside this block:
    f();
    if (g()) { h(1); }
These optimizations happen all over the place, it's not the compiler invoking or causing undefined behavior, but assuming that it won't ever happen.

EDIT: Note the further optimization that looms: if f() can be proven pure (no side effects), then it can be removed. This makes little sense for a function with no arguments (in which case it would just be a constant). If, however, f(y, ...) is some expensive but pure function, it can just be removed completely.

"Note the further optimization that looms: if f() can be proven pure (no side effects), then it can be removed. This makes little sense for a function with no arguments (in which case it would just be a constant)."

It could apply if f computes a value based on global state but doesn't change anything, which is slightly weaker than "pure".

Why does this have to be a valid program? There is obviously a chance that x is undefined at line 3 so why not allow the compiler to throw an error?

Not necessarily. Just because the compiler can't prove that g only returns true iff f also returned true doesn't mean the programmer doesn't know it to actually be true.

So changing that behavior means that the compiler now rejects 40 years worth of correctly working legacy code (and some buggy code, as well). Newer languages (e.g. Java, C#) that don't have to support existing code can afford to do what you want and reject programs where a simple heuristic isn't enough to tell whether a variable is initialized or not.

You can already break a lot of programs with -Wall -Werror (I generally compile my code with both, but turn them off for libraries). Just hide the amazing optimisation behind an -foptimise-undefined-behaviour which you can only turn on if you've specified at least a significant portion of -Wall.

So don't raise an error, instead publish a warning.

I think the idea is to give an error when there is absolutely no way that variable could have been initialised when it's read, i.e. cases like this:

    int x;
    h(x);
The point is not to solve the Halting Problem, but to catch code that is so obviously wrong that no programmer would deliberately write it (unless they were testing the compiler's reaction.)

If so, "that should always be a compile error. Period.” is IMO a poor way to express that idea.

On top of that, the C compilers I know more or less have that, as they give warnings for basically cases where Java (with its stricter rules) would refuse to compile equivalent code, and have a flag that turns warnings into errors.

The Java compiler WILL reject this program, because x is not initialised on all code-paths. This is not a valid program.

That should be rejected as an invalid program, you'd fix it by initializing x to a sane default.

Or by putting the second if inside the body of the first.

Often there is no sane default value.

Putting the second if inside the body of the first is not necessarily equivalent.

g() might have side-effects, and those side-effects have to happen regardless of whether or not f() returned true.

Then the correct code was something like:

    if (f()) {
        int x = 1;

        if (g()) {
            h(x);
        }

    } else {
        g();
    }
If g() can be called before f() then it could also be written as:
    int g_flag = g();

    if (f()) {
        int x = 1;

        if (g_flag) {
            h(x);
        }
    |
If you really only had one assignment to x, and both f() and g() have side effects that must be run in-order, this can be more clearly written as:
    int f_flag = f(); // force f() to run for ${reasons}
    int g_flag = g(); // force g() to run for ${reasons}

    if (f_flag && g_flag) {
        h(1);
    }
This is a lot clearer about what the code is doing, and the compiler is probably going to optimize away the two int flag variables anyway.

You should never rely on uinitialized values not just because of the problems relating to undefined behavior, but also because you're adding in assumptions about the runtime state. The code depended on the return value of f() but did not fully express that dependency in the code.

It's true that if the only change you make is wrapping braces from "x = 1;" through the end of the quoted code, you would need to be sure that g does not have desired side effects. Otherwise, you could lift both the f() and g() calls above the branching (which still leaves the second if inside the body of the first, as I described).

Valid as well.

AboutSource Built by g1lg1l

Hackerly is an independent reader for Hacker News, built on the public HN API. Not affiliated with Y Combinator.