That's true when you write a goto that goes around scopes, and doesn't jump into them. However, you can still jump into the middle of an inner scope, where sometimes a variable might be uninitialized.
void c89_fn(int arg) {
if (arg > 0)
goto after;
{
int x = 42; /* or "int x; x = 42;" */
after:
/* x is in scope, but x could be uninitialized */
printf("%d\n", x);
}
}
For this particular example, GCC and Clang inline the definition of x as 42 unconditionally with any amount of optimizations on, but with -O0, both generate code that reads from a sometimes-uninitialized stack address. There's probably a more practical example that one could find.Also, my original example is invalid C++, as is yours.
C++ does not allow goto to skip variable initializations.
In C++, if you want to goto past some variable declarations, you have to do the right thing and encapsulate them into a block.
GCC's diagnostics look like:
goto.cc: In function ‘void f()’:
goto.cc:7:1: error: jump to label ‘end’ [-fpermissive]
end:;
^~~
goto.cc:3:8: note: from here
goto end;
^~~
goto.cc:5:7: note: crosses initialization of ‘int x’
int x = 42;
^I see. I think I get what you're saying: if you only put variable declarations at the top of the scope, then if you only jump to labels at the same statement depth as the goto statement, you won't skip initialization or assignment of uninitialized variables. Whereas, if you mix declarations and statements, you might end up with uninitialized variables even when you only jump to labels at the same statement depth as the goto statement. Is that what you're saying, or did I grasp something orthogonal to your point?
> C++ does not allow goto to skip variable initializations.
I didn't know that until now. However, if you change the function so that x is assigned after being declared, it still compiles and produces the same result.
void c89_fn(int arg) {
if (arg > 0)
goto after;
{
int x;
x = 42;
after:
/* x is in scope, but x could be uninitialized */
printf("%d\n", x);
}
}
Other Algol-like languages including Algol itself also separate declarations and statements: Pascal, Modula, Ada, ...
In C90, you can have variables anywhere, but they must be wrapped in a compound statement. This has a number of advantages:
1. You can repeat variables:
2. You can move code around more easily, and move these sub-blocks into their own functions more easily, since they follow "declaration close to use".3. You control the end of the scope of the variables, not only the beginning. You know that after each of the above two closing braces, the yes variable is no longer in scope. (If there is a yes variable in scope, it must be coming from a parent scope; it is not "leaking" sideways.)
4. If you write a goto which goes around these encapsulated scopes, that goto does not jump into a region where the variable is uninitialized. Compare:
For these reasons, I avoid mixing declarations and statements in C programming; the separation is a very good idea. It's enforced in Ada, where it is rationalized with safety arguments. Mixing declarations and statements encourages bugs. It lets you hack dubious solutions into the program without having to think about how its structure could be improved to do it cleanly.In Common Lisp and similar dialects, you can mix imperative code with variable definitions by using progn.
On a small number of occasions, I've done the above in C, using the comma operator, to avoid mixed declarations and statements: It's a little dirty but not as dirty as mixed declarations and statements.Also note that there is no need for mixed declarations and statements, if your code is functional! In any of your code that emphasizes functional programming, you should not run into a need for this, because the entire purpose of the feature is to be able to stick a side effect between variable definitions, which has to be sequenced there.
The Scheme language has the feature of defining variables anywhere. You can use the define form in any scope, like inside a function:
How this works is that Scheme implementations perform a code walk which transforms these defines into nested let forms. Each sequential body of expressions has to be scanned for the presence of define and treated this way.It's pretty ugly; you end up with what looks like a self-contained form (define x 0) that is frobbing the surrounding lexical scope, such that another form not enclosed in it depends on its definition of x.
In Common Lisp and related dialects, anything that starts with "(def" is understood to be for top-level definitions only, which work by performing a run-time side effect when they are evaluated. (And so their effect is visible to later forms not due to lexical scope but due to the chronological order of execution.)
There is a strong metaprogramming advantage in having rigid variable binding forms like let. Because let has all the variables in one place, it is easy to interpret and compile. This is one of the things that helps a Lisp-in-Lisp metacircular evaluator be very short. If you have variables defined anywhere, it means that every form that contains statement-like forms must be scanned for those definitions. They are not in a fixed place in the AST: (let vars form ...). That is one big reason why if you implement Scheme's define, you want that to be expanded early, so that everything downstream just sees nice nested lets.