14
$\begingroup$

Goto is often "considered harmful". But are there good reasons to have goto? If not, what are some of the cons associated with it that imply it should be avoided?

$\endgroup$
5
  • 2
    $\begingroup$ Related: stackoverflow.com/questions/245742/… $\endgroup$
    – naffetS
    Commented May 18, 2023 at 20:21
  • 1
    $\begingroup$ It's notable that the harmful paper was referring to goto between functions, as is native in assembly. The version in C and derivatives is restricted to within one function and therefore drastically less expressive / hazardous, the conclusion from the assembly properties does not necessarily apply. $\endgroup$ Commented May 18, 2023 at 21:28
  • 9
    $\begingroup$ Pro: It can be convenient sometimes. Con: You get mauled by raptors. $\endgroup$
    – user
    Commented May 19, 2023 at 22:34
  • $\begingroup$ I always felt that "goto" was unfairly singled out as bad as I have knowledge of assembly. But most moder code-reviewers wouldn't want it, and their justifications usually make sense. $\endgroup$ Commented Jul 19, 2023 at 22:31
  • $\begingroup$ A fun challenge for C# programmers: can you write a program with a goto where the goto statement is reachable but the corresponding label is not? There are a lot of weird cases you have to consider when designing a modern language with a feature as low-level as goto. $\endgroup$ Commented Jan 10 at 3:11

13 Answers 13

18
$\begingroup$

There are famous problems caused by jumping into the middle a block from outside it.

Unrestricted goto may violate variable scope rules

This code, in C++, is an error. Your compiler must reject it.

int main() 
{ 
    if (std::rand() & 1) { 
        goto target; 
    } 
 
    { 
        int x = 42; // Pay attention to this line. 
    target: 
        std::cout << "x = " << x << '\n'; 
    } 
}

This, however, is not an error, but will result in undefined behaviour or implementation-defined behaviour (I can't remember which off the top of my head).

int main() 
{ 
    if (std::rand() & 1) { 
        goto target; 
    } 
 
    { 
        int x; // The only thing that's different... 
        x = 42; // ...is these two lines. 
    target: 
        std::cout << "x = " << x << '\n'; 
    } 
}

If you're not a C++ language lawyer, you're probably quite confused right now. You may have thought that the two are semantically identical, but they are not.

In the first, x is initialised at the point where it is declared, and the program tries to jump into the middle of its scope. If that jump is ever taken, x could be either initialised or not initialised, which is an inconsistent state.

In the second, x is not initialised where it is declared. So unlike the first example, x is not in an inconsistent state. This code still has undefined/implementation-defined behaviour, but only because the value of x is read.

In the case of int, the distinction doesn't matter, but it does matter when it comes to objects that have destructors. The first example, if allowed, would mean jumping to a point between where an object is constructed and where it is destroyed, and this would result in an unconstructed object having its destructor executed, which would be a serious bug. But presumably for convenience, for the purpose of this rule C++ doesn't care that it's a built-in type.

Unrestricted goto may reduce opportunities for optimisation

Consider this loop:

int i = 0;

for (i = 0; i < N; ++i) {
    some_code();
    some_loop_invariant_code();
    some_other_code();
}

some_more_code();

And let's suppose that the compiler discovers the loop-invariant code and decides that it would be good to move it outside the loop. It can transform it to this:

int i = 0;

some_loop_invariant_code();
for (i = 0; i < N; ++i) {
    some_code();
    some_other_code();
}

some_more_code();

So far, so good. But what about this?

int i = 0;
if (some_condition()) {
    goto target;
}

for (i = 0; i < N; ++i) {
    some_code();
target:
    some_loop_invariant_code();
    some_other_code();
}

some_more_code();

Where, precisely, should that code be moved to?

The problem is that the control flow graph is not reducible. Reducibility has a technical definition, but you can think of it, intuitively, as meaning that every loop has a single header and, therefore, a single place to move loop-invariant code to.

We will assume for the moment that moving some_loop_invariant_code before some_condition is not a valid transformation. So the only solution is to either not perform the optimisation, or duplicate code blocks (which, incidentally, is also what partially redundant code optimisation would do).

int i = 0;
if (some_condition()) {
    /* I guess? */
    some_loop_invariant_code();
    goto target;
}

some_loop_invariant_code();
for (i = 0; i < N; ++i) {
    some_code();
target:
    some_other_code();
}

some_more_code();
$\endgroup$
4
  • 1
    $\begingroup$ The first example (if compiled as C with printf instead of cout) and the second example are actually completely fine and have no implementation defined or undefined behaviour, because rand() % 1 always evaluates to 0 and the goto branch is never taken :) $\endgroup$
    – chrysante
    Commented Dec 14, 2023 at 12:04
  • 1
    $\begingroup$ @chrysante That's the thing with random numbers. You can never tell! $\endgroup$
    – Pseudonym
    Commented Dec 14, 2023 at 22:45
  • $\begingroup$ Are you being serious? You can tell in this case. I'm just wondering if you write rand() % 1 deliberately or meant rand() % 2? $\endgroup$
    – chrysante
    Commented Dec 14, 2023 at 23:47
  • 1
    $\begingroup$ Duh! I meant to write rand() & 1. Thank you for the code review! I will edit. $\endgroup$
    – Pseudonym
    Commented Dec 15, 2023 at 0:29
13
$\begingroup$

There is one significant advantage of goto that was not yet mentioned. If your language can be potentially a target for compilation, or if it has powerful metaprogramming capabilities (which, in turn, implies that it's a target for compilation), not having goto makes it much harder to compile a wider class of languages into it, and denying a wide range of optimisations on higher level of translation.

The difference between having and not having goto is in the ability to express irreducible control flow. If for whatever reason your compiler generate an irreducible CFG at the end, you'll have to then do a lot of very inefficient and damaging trasnformations to get out of it - see WebAssembly compiler backends (and some OpenCL implementations) for example of such calamity.

$\endgroup$
4
  • 2
    $\begingroup$ Many people who favor adding runtime constraints to C for the purposes of facilitating optimizations ignore the fact that it undermines the language's usefulness as a back-end for any language that would let programmers do things not provided for by such constraints. $\endgroup$
    – supercat
    Commented Jul 14, 2023 at 15:04
  • $\begingroup$ That may (or may not) be an argument for C; but most other languages aren't the target of other compilers, are they?  (Are the needs of human-writable programming languages necessarily sufficiently similar to those of compiler-targeted intermediate languages that it's worth compromising one or the other to squeeze them into the same language?) $\endgroup$
    – gidds
    Commented Dec 15, 2023 at 23:04
  • $\begingroup$ @gidds any meta-language (any language with procedural macros) is a target of compilation, for any macro is a compiler. And even if your language is handicapped and does not have macros, you still may need to use external preprocessors (things such as flex and yacc, for example). $\endgroup$
    – SK-logic
    Commented Dec 15, 2023 at 23:17
  • $\begingroup$ @gidds: The historical popularity of C as a back-end langauge isn't a result of C being well designed for the purpose, but rather the lack of anything better. If someone were developing a back-end language, it would be useful for it to recognize different optimization implications of constructs using goto versus structured looping constructs, such as allowing some operations to be hoisted ahead of a structured loop without regard for whether that would affect program behavior, but not allowing such latitude with structures using goto. $\endgroup$
    – supercat
    Commented Dec 20, 2023 at 20:14
12
$\begingroup$

Exiting from nested loops

If your language doesn't allow labeling loops and then doing break label;, goto also works for this:

for ...
  for ...
    if ...
      goto a

a:
  ...

This is a common pattern in C.

$\endgroup$
2
  • $\begingroup$ What languages support loop labels? $\endgroup$
    – Someone
    Commented Dec 21, 2023 at 21:02
  • $\begingroup$ Isn't this a better argument for including loop labels, or multi-level breaks, than it is for including goto? $\endgroup$ Commented Dec 24, 2023 at 12:08
11
$\begingroup$

Implementing goto is easy for compiler writers because it neatly maps to assembly language: Unconditional goto is a “jump” instruction, and if...goto is a “branch” instruction.

The reason that goto got such a bad rap is that in early programming languages, it was pretty much the only control-flow statement you had.

For example, if you don't have do...while loops, you emulate them with if...goto.

START_LOOP:
    // do lots of stuff
    if (condition) goto START_LOOP;

If you don't have block statements (like C's {...} or Pascal's begin...end), then here's how you handle an if...else block:

    if (not condition) goto ELSE;
    // stuff to do if condition is true
    goto END_IF;
ELSE:
    // stuff to do if condition is false
END_IF:

Do this enough, and your program becomes a “spaghetti code” mess of goto statements. This complaint was frequently voiced circa 1980, when line-numbered BASIC was the lingua franca of home computers (Apple II, Commodore, Atari, etc.). If you've never worked with BASIC, I recommend that you go find some vintage BASIC game, study its code, and convert it to your modern language of choice. Then you will understand the anti-goto movement.

The point of “structured programming” wasn't really to get rid of goto. It's still there behind the scenes, in the machine code generated by your compiler. Rather, it's that by replacing each goto with a more specific keyword — the block if...else statement; the do, while, or for loop; the early-exit break, continue, or return; and the throw and catch (and sometimes finally) for exceptions — the intent of the code becomes more clear. And you stop thinking in terms of assembly-style “jumps” and “branches”, replacing them with the higher-level constructs of “blocks”, “loops”, and “functions”.

There are still occasions where a goto is convenient. Like exiting from nested loops (but the loop can be refactored into a function with an early return). Or goto ERROR; to handle error reporting and cleanup (but this can be replaced with exception handling).

But in modern languages, a goto is never necessary, and there are popular languages that get along just fine without it, like Python or Java.

$\endgroup$
3
  • 1
    $\begingroup$ I’ve seen the proof for some language that goto is never necessary by transforming any function and destroying all code flow structure. $\endgroup$
    – gnasher729
    Commented Dec 18, 2023 at 21:16
  • $\begingroup$ @gnasher729: It's trivial to turn any combination of looping control structures into a single while which brackets a combination of if and flag assignment statements, without any reverse branches aside from the while, but that doesn't mean the latter form is "better". $\endgroup$
    – supercat
    Commented Dec 18, 2023 at 21:28
  • $\begingroup$ @gnasher729: In many cases it may be difficult to optimize code containing goto without making overly pessimistic allowances for possible program flows, but optimizing code that jumps through hoops to avoid goto would be even worse. $\endgroup$
    – supercat
    Commented Dec 18, 2023 at 21:33
7
$\begingroup$

Computed gotos make virtual machines faster at dispatching instructions.

Consider the "normal" way of dispatching via a looped switch statement:

while (true) {
  switch (code[pc++]) {  // dispatch next opcode
    case OP_ADD:
      add();
      break;

    case OP_SUB:
      sub();
      break;

    case OP_MUL:
      mul();
      break;

    case OP_DIV:
      div();
      break;
  }
}

This iterates over a program counter within the user's bytecode and invokes arithmetic functions based on the observed opcode.

The switch statement is, ultimately, the single point of dispatch for all opcodes. That means that a CPU's branch predictor can't really determine a pattern to the next opcode. Consider that over the past few handled instructions within a user's bytecode, the branch predictor will have seen multiple different opcodes. Thus, the prediction for the next switch target cannot be focused onto a single opcode, which means that there will be no (correct) branch prediction!

Fortunately there is a non-standard extension to C/C++ that some compilers implement. Computed gotos allow programs to reference labels directly:

// opcodes must be in consecutive order for this array to be valid
void* table[] = {&&op_add, &&op_sub, &&op_mul, &&op_div};

// start the first dispatch
goto *table[code[pc++]];

op_add:
  add();
  goto *table[code[pc++]];  // dispatch next opcode

op_sub:
  sub();
  goto *table[code[pc++]];  // dispatch next opcode

op_mul:
  mul();
  goto *table[code[pc++]];  // dispatch next opcode

op_div:
  div();
  goto *table[code[pc++]];  // dispatch next opcode

Here there are multiple points of dispatch because each opcode handler dispatches to the next opcode in the user's bytecode. If the user's bytecode is part of a tight loop, then the branch predictor will see a pattern of the current opcode based solely on the previous opcode. That means prediction is more focused (the current opcode determines the next opcode), which means there will be a (likely correct) branch prediction!

This technique is sometimes referred to as indirect threading (not to be confused with multithreading).

CPython's virtual machine will use computed gotos if possible, noting:

At the time of this writing, the "threaded code" version is up to 15-20% faster than the normal "switch" version, depending on the compiler and the CPU architecture.

$\endgroup$
11
  • 2
    $\begingroup$ The reason why this gives a performance improvement is not because "there is only one branch". In fact, the exact opposite is true: there is a "branch" at the end of every opcode. This helps a modern CPU's branch predictor, which is a cache indexed by the program counter. If there is one big switch statement, there is only one place to branch from, so the predictor never gets a chance to predict. $\endgroup$
    – Pseudonym
    Commented May 19, 2023 at 2:08
  • 1
    $\begingroup$ @Pseudonym There is one branch per opcode dispatch. $\endgroup$ Commented May 19, 2023 at 2:32
  • 3
    $\begingroup$ Most compilers would implement the "big switch statement" with one or two branches at the most, assuming that the set of opcodes is dense: one to check the range and the other through a jump table. The range check would be perfectly predicted almost all the time and therefore cost nothing. So the number of branches per dispatch is essentially the same. $\endgroup$
    – Pseudonym
    Commented May 19, 2023 at 2:50
  • 2
    $\begingroup$ @Pseudonym You are correct. I have amended my answer. $\endgroup$ Commented May 19, 2023 at 3:40
  • 1
    $\begingroup$ It's an indirect threading in your example. Direct threading would have been replacing your bytecode array with label addresses first (e.g., see how OCaml bytecode interpreter is doing it). $\endgroup$
    – SK-logic
    Commented Jun 30, 2023 at 9:40
7
$\begingroup$

Pro: supporting goto can make it easier to target your language

For example:

  • Parser generators often generate gotos, because the transitions of the produced state machine are natural to express using goto.

  • Transpilers can generate gotos to simplify translating control flow into your language.

More generally it could make life easier for anyone generating code in your language.

$\endgroup$
2
  • 1
    $\begingroup$ Welcome! I think both of these points are already made in SK-logic's answer. $\endgroup$
    – kaya3
    Commented Jul 2, 2023 at 18:08
  • 1
    $\begingroup$ Oh no... I missed it, because I was initially searching if someone mentioned parser generators. $\endgroup$ Commented Jul 2, 2023 at 18:26
7
$\begingroup$

Cleanup tasks / errors

If you have a complex function that does tasks that can fail, goto can help. This is more useful in C-like languages, where you might need to deallocate memory or something. Maybe you open a file, do some tasks, and for each task, you need to check if it failed. If it failed, then goto to cleanup to close the file and return an error. Otherwise it would just error and not close the file.

  open(file);

  doA;
  if (doA failed) {
    status = E_DOA;
    goto cleanup;
  }

  doB;
  if (doB failed) {
    status = E_DOB;
    goto cleanup;
  }

  do stuff to file;

cleanup:

  close(file);

  return status;

(idea from https://stackoverflow.com/a/245781)

$\endgroup$
3
  • 5
    $\begingroup$ The main issue here isn't that you have manual memory management, but rather you don't have other constructs like try/catch that would allow you to do the same thing. $\endgroup$ Commented May 19, 2023 at 17:24
  • 2
    $\begingroup$ @AmanGrewal - right. goto is a very good replacement for missing language constructs -- but there are other alternatives that can also achieve similar results that are a little more structured (e.g. various constructs involving first class continuations), but best of all is ensuring you have adequate language constructs for expressing things programmers are likely to want to do. $\endgroup$
    – occipita
    Commented Jul 12, 2023 at 19:54
  • 1
    $\begingroup$ As a structured way to do this, many languages offer a defer keyword. $\endgroup$
    – xigoi
    Commented Jul 16, 2023 at 21:08
6
$\begingroup$

Two pros:

  • goto is more or less the equivalent of a JMP instruction in assembly, so it's definitely easier for the compiler.
  • Whenever I have two loops, and I want to break out of the innermost one, the only option I have (in Java, C#, JavaScript) is return, and that only works (most of the times) if the loops are the only thing in a method:
for (int i = 0; i < maxI; i++) {
    for (int j = 0; j < maxJ; j++) {
        if (match(i, j)) {
             // I'd really like to quit both loops here, but `break` only quits the `j` loop ...
        }
    }
}

Cons:

  • Yes, the ability to jump to anywhere in the current method or program increases the complexity for humans to comprehend the code. The code above can be improved if the language had something like a break all; instruction; at least that keeps the instruction pointer close to the last instruction, as opposed to an unlimited goto.
$\endgroup$
4
  • 3
    $\begingroup$ If your language permits variables with scopes smaller than "function", a goto may considerably more complicated than a JMP: you now need to handle whatever happens when variables go out of scope. $\endgroup$
    – Mark
    Commented May 18, 2023 at 23:49
  • 3
    $\begingroup$ Java has labels, you can break out of an outer loop by wrapping it in a label $\endgroup$
    – Seggan
    Commented May 19, 2023 at 13:47
  • 2
    $\begingroup$ > goto is more or less the equivalent of a JMP instruction in assembly, so it's definitely easier for the compiler. Definitely not if you are targetting WebAssembly. WebAssembly has no jmp. $\endgroup$ Commented Jul 2, 2023 at 22:22
  • $\begingroup$ As an alternative to labels (which are a kind of limited-use goto) or break all;, in PHP both break and continue take an optional integer to jump out of that number of loops, so this example would simply be break 2; On the other hand, PHP added goto in version 5.3. $\endgroup$
    – IMSoP
    Commented Dec 15, 2023 at 15:35
3
$\begingroup$

Pros

One pro of having GOTO is that jumps are a fundamental building block of computer programming, and the GOTO expresses the unconditional jump. There is also a counterpart concept of the 'line label', which is how the target of a jump is expressed.

Discussion of how jumps work is still important when trying to teach people for the first time how computers and programs really work, and how structured blocks are composed from more basic elements.

Even a very modern language cannot be hurt by having the potential to be useful in the classroom. Or put another way, it cannot be bad that languages which are taught in the classroom because they have the facility to express basic concepts, are also fully useful industrial languages.

A second pro is that there are certain algorithms and programming patterns where the use of GOTO remains either the clearest implementation of that algorithm, or the fastest implementation.

Finite state machines are the main example of this residual area, as the transitions between states can occur in a way that doesn't easily reconcile with the hierarchical nature of structured blocks. The structured solution essentially just implements a small processing loop and a variable that controls the movement from one arbitrary branch inside the loop to another - achieving by boilerplate and contrivance what GOTO achieves directly.

Cons

The cons of GOTO really relate to its manner of use rather than its mere existence in the language.

One con is that the use of GOTO can make it difficult for an optimising compiler to analyse and transform the code, because the unstructured jumps can't be easily fitted into patterns which the compiler knows can be optimised in a particular way.

Another con is that it is said to promote the creation of so-called spaghetti code, where too much of the structure of the code becomes latent, or analysing possible paths through the code (and verifying them all as legitimate paths) becomes unreasonably challenging to a programmer.

History

It's worth saying that some of the worst offences that GOTO was capable of enabling are nowadays no longer possible anyway.

For reasons of speed and simplicity (in some regards), callable procedures in old languages were not always re-entrant - that is, they could not be called recursively. The memory for local variables was allocated statically. As a by-product, you could jump directly into and out of these procedures.

In modern languages, callable procedures have a more complicated and re-entrant implementation, which means GOTO is not compatible with jumping in and out of a procedure body arbitrarily.

There was also a strong culture amongst programmers many decades ago, of relentlessly optimising performance and memory use, without any regard whatsoever for maintainability (that is, the ability to read code to recover an understanding of it, and the ability to modify a program later without upsetting an unclear set of assumptions and dependencies between parts).

Counselling practitioners to forgo to the use of GOTO was often tacitly a counsel to avoid the most horrific optimisations and to employ (what were at that time...) "high-level" constructs that leave the code in a more maintainable condition, and containing fewer tricks.

Another early objection to block-structured programming - and therefore justifying the continued use of GOTO - was that it wasn't capable of expressing all algorithms. But that was proven absolutely false, albeit sometimes that the alternative to GOTO would be at the cost of some additional memory or slightly reduced performance.

$\endgroup$
2
$\begingroup$

I'd summarize the answers so far as:

Cons:

Jumping into the middle of some code is almost always bad:

  • It can prevent optimization.
  • It can make the flow of control non-obvious to those reading it (there's no indication that something might jump into this spot).
  • It can introduce bugs when someone modifies the code not realizing that something could jump into the middle of it.
  • It makes debugging more complicated, as there's no easy way of finding all the code that could have executed the goto, much less determining which one did.

Pros:

Jumping out of the middle of some code to the end of a section, where the flow would normally have ended up had the goto not happened, is good:

  • It makes it easy to get out of nested loops.
  • It makes it easy to get to a clean-up and return section.

Example:

{
    ...
} label:
...

One should be able to know with confidence that the only way to jump to that label is from within the immediately preceding {...}.

$\endgroup$
2
$\begingroup$

Some algorithms in scientific or mathematical literature are described including the goto construct, the way

  • 1 initialize
  • 2 if abc then go to 5 (skip redundant action or if)
  • 3
  • 4 if cde then go to 7 (break out or error handling)
  • 5
  • 6 if ghi then go to 2 (loop)
  • 7 done

The steps are often described in plain human language, or some pseudocode is used that provides goto. It is not our choice, the choice has been made by somebody else who is a mathematician and not a software engineer. A good looking solution is to implement each step as a call to function, keeping this list at high level so easily observable.

When implementing such an algorithm in real language, it having goto allows to stay closer to the original description, providing commented sections of the code that match the sections in the article. Otherwise you need a different code that is more apart from the scientific paper used to write it. That makes more difficult to make the matching changes later if the scientific author publishes improvements, derived work or errata.

$\endgroup$
1
$\begingroup$

It has been mentioned several times that implementing goto is easy, but I would not say so. I don't mean it's impossibly difficult either, I mean there are non-trivial implementation considerations. You can deal with them, but you would have to deal with them.

What goto does unlike typical (semi-) structured programming primitives, is enable the expression of irreducible CFGs at the source level, meaning that your compiler will have to deal with them. Irreducible CFGs may also be formed by your back-end anyway, depending on whether you implement optimizations that do so, so you may not entirely avoid them.. but you can avoid them in the front-end. You can avoid them in the back-end as well if you so choose, which Graal for example does.

That is not a consideration in simple compilers that translate a statement at the time into machine code, but most compilers will convert their AST to some graph of basic blocks, and then goto will enable that CFG to be irreducible, which then tends to complicate almost anything that you may want to do with your CFG. You can make irreducible CFGs reducible with node splitting, but you may not like the worst-case expansion factor.

As one example, there is simpler algorithm for into-SSA-translation when dealing with structured programming specifically, single-pass generation of static single-assignment form for structured languages (https://dl.acm.org/doi/pdf/10.1145/197320.197331). Otherwise, even in the presence of goto you can still translate into SSA of course, and that is done a lot, it's just more complex.

WASM only supports structured control flow (or used to anyway, I have not kept up to date), requiring some workarounds to deal with irreducible CFGs, such as Emscriptens "relooper". Possibly part of the motivation for that was to make it easy to convert WASM into SSA form, I don't have a real source for that, just one anonymous person replying that in a github issue that asked for goto. As mentioned earlier, it's certainly possible to convert to SSA in the presence of goto, but it's more complex.

Graal apparently also uses a structured representation, requiring workarounds only to deal with unusual bytecode. I couldn't find any reference for why it does that, but I imagine it is done that way because it's just easier to deal with IR that has only structured control flow, and Java does not compile into irreducible CFGs anyway. Irreducible CFGs make many things more complicated than they are for reducible CFGs.

$\endgroup$
0
$\begingroup$

goto was used in Fortran IV which didn’t even have if / then / else forcing you to use two gotos. This was both awful and unavoidable. So a lot of objections against goto come from this time. In today’s languages, situations where goto is useful are very, very rare, assuming you don’t count “break” and “return” as gotos.

It is getting even more rare with compilers that optimise code that in the past could be made faster with goto: Take

Bool done = false;
While (! done) .., 

The variable could be removed by using goto in the right places, but today a compiler can do that automatically.

And in C++, goto runs into the problem that you are not allowed to goto around a variable declaration. Like

int I = 0;
If (condition) goto xyz;
int j = 0;

is not allowed.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .