A problem with control structures is the side-effect of their imperative nature. Control structures mostly work quite well given a very isolated (closed system) state, just as C initially permitted functions to be (merely with a few input parameters, and one output). I think problems arise when you don't have the simplicity of maintaining such a low-dimensional program state, or your design goal requires a highly dynamic control flow. This may severely complicate the programmer's task of determining such structures. Also, I'm definitely not convinced that control structures are the best solution to goto's problem.
Using goto is always very short sighted. Taking Nexius example.
When placing a line after the while loop, you have to decide whether it has to be before or after the label. This is hard enough because it depends on exact reason why and when that goto is called. If I am the one to modify the code that alone would give me the creeps. But what if someone has to modify the loop itself? ...god awful...
Good use of goto is seldom demonstrated with a simple example, as someone can always say "you can do it like this...". Simple examples have simple alternatives.
The short sightedness is threads like this is the knee jerk reaction, "never use X".
As for the whole, "I've had an error throw an exception", that is just as bad. Talk about using goto causes spaghetti code; at least you can see the label that to goto goes to, throw an exception and who knows where it will end up. Plus there may be times when you are not allowed to use exceptions, so knowing alternatives is always good.
Here's an example when goto is OK. And even this can be tweaked so the goto isn't necessary.
This is only because C++ lacks labeled break / continue. Anyway, I'm sceptic about using break and continue at all. You should try to write your loops in such a way that there is only one exit point - the loop condition. A long loop littered with breaks / continues is really not any more clear than a bunch of gotos.
The few times I've been tempted to use a goto, I ended up scratching the entire context and reworking it. I'm sure this isn't always the case, but in my work, if the flow isn't natural than it isn't right. So far, I've always ended up with better code after avoiding the goto-trap.
The one case (snicker) where I find "goto" to be extremely useful is in handling switch statements where multiple cases share the a majority of code and differ only in the initial setup:
switch ( inst_operation )
{
case I_ADD_I8: result = REG(dreg) + IMM8(iword); goto alu_inst;
case I_ADD_I16: result = REG(dreg) + IMM16(iword); goto alu_inst;
case I_ADD: result = REG(dreg) + REG(sreg); goto alu_inst;
/* And many more ... */
case I_SUB_I8: result = REG(dreg) - IMM8(iword); goto alu_inst;
case I_SUB_I16: result = REG(dreg) - IMM16(iword); goto alu_inst;
case I_SUB: result = REG(dreg) - REG(sreg); goto alu_inst;
case I_SUBR: result = REG(sreg) - REG(dreg); goto alu_inst;
alu_inst:
cc.zero = (result == 0);
cc.neg = (result & NEG32) ? TRUE : FALSE;
/* save result ... */
/* update pipeline for a simple ALU instruction ... */
break;
case I_BRA: iaddr = iaddr + SEXT12(iword << 1); goto branch_inst;
case I_BSR: SetReg(A6,iaddr); iaddr = iaddr + SEXT12(iword << 1); goto branch_inst;
case I_JMP: iaddr = REG(dreg) & INST_MASK; goto branch_inst;
case I_JSR: SetReg(A6,iaddr); iaddr = REG(dreg) & INST_MASK; goto branch_inst;
branch_inst:
/* invalidate the instruction pipeline ... */
/* start fetching the new instruction ... */
break;
/* Many more instruction operations */
}
I have seen code which avoids "goto" by nesting multiple switch statements, but I have also seen code where one of the cases is missing and such an error can only be caught at run-time:
switch ( inst_operation )
{
case I_ADD_I8:
case I_ADD_I16:
case I_ADD:
case I_SUB_I8:
case I_SUB_I16:
case I_SUB:
case I_SUBR:
switch ( inst_operation )
{
case I_ADD_I8: result = REG(dreg) + IMM8(iword); break;
case I_ADD_I16: result = REG(dreg) + IMM16(iword); break;
case I_ADD: result = REG(dreg) + REG(sreg); break;
case I_SUB_I8: result = REG(dreg) - IMM8(iword); break;
case I_SUB_I16: result = REG(dreg) - IMM16(iword); break;
case I_SUBR: result = REG(sreg) - REG(dreg); break;
}
cc.zero = (result == 0);
cc.neg = (result & NEG32) ? TRUE : FALSE;
/* save result ... */
/* update pipeline for a simple ALU instruction ... */
break;
/* Many more instruction operations */
}
Other solutions which I have seen involve functions for the shared code or subclasses for the case-specific code.
They only seem to be controversial in C++. In pretty much every other language I've used that supports them, they are the norm.
I'm also sure throwing an exception is a hell of a lot slower than using goto or setting up an appropriate control structure.
That's what everyone says. But I have yet to experience any significant performance issues in any of my personal programs that I was able to attribute to exceptions.
The one case (snicker) where I find "goto" to be extremely useful is in handling switch statements where multiple cases share the a majority of code and differ only in the initial setup:
I disagree: You can as easily just create a function with the common code. This is to me a classic example of misuse because a function call will look almost identical. I see no gains in using goto here. You can even inline the function if you must.
I think I'll even take the embedded switch approach here.
Switch statements are often just gotos. Something like
1 2 3
switch (variable) {
// Some cases (at least 5)
}
will usually get compiled into something like this:
1 2 3 4
mov eax, variable // eax = result of switch expression
mov ebx, jump_table // Basically an array of addresses, each address being a different case
add ebx, eax
jmp [ebx] // Jump (goto) to the correct case
which is why switches are often faster than if/else trees.
They only seem to be controversial in C++. In pretty much every other language I've used that supports them, they are the norm.
But it was C++ we were talking about, right?
Also, exceptions are rather unique in their flexibility in C++, and they have a bigger adverse effect on performance in C++ than most other languages too, so that would be why.
webJose wrote:
I disagree: You can as easily just create a function with the common code. This is to me a classic example of misuse because a function call will look almost identical. I see no gains in using goto here. You can even inline the function if you must.
Context makes the difference, and that is where programming becomes an "art". What are the design goals? How many arguments are required for the function? Is it appropriate to seperate the functional blocks into seperate functions or are they more easily understood when grouped together? Is a list or a grid more appropriate for grouping the "data" embedded within the code. Is the code in the critical path for optimization?
The above code is (as everyone can probably guess) a pseudo-fragment of a CPU simulator. In this case, the ALU and branch instructions were on the critical optimization path and function calls would actually introduce a substantial performance penalty (the critical path was about 80 SPARC instructions and about a 100 IA32 instructions executed). In this case, inlining would be nice if the common code permitted inlining and if the optimizer was smart enough to tail-merge the case statements, neither of which was the case.
"goto" was a valuable tool when used in what we considered to be the right situtation. All of the cases were "table-ized" (with at most 3 or 4 variants per row) and the goto label immediately followed the cases.
Anyways, keep "goto" in the toolbox. In almost every case it may be the wrong tool but there are times when it is useful and appropriate.
webJose wrote:
I think I'll even take the embedded switch approach here.
Did you notice that the embedded switch was missing the I_SUB case? That was a simple example as I have seen both CPU simulation and message handling code where there are frequently over a dozen nested cases. I can't even count the number of times I have seen errors late in a project where code gets reorganized or new messages are added and a corresponding case statement is missed in one or the other switch statement. All of these cases have gotten past the developer's checkin and code reviews. Most are fortunately caught by testing, but even there they can be missed (especially if there is no default case to issue an error or fatal error, or if the error logs aren't analyzed for failures). Repetition of code is usually evil, but so it the repetition of values.