Skip to content
C++
Language
since C++98
Basic

goto

Unconditional jump statement that transfers control to a labeled statement within the same function, respecting RAII destructor semantics.

gotosince C++98

Unconditionally transfers control to a labeled statement within the same function, calling destructors for any automatic variables whose scope is exited in the process.

Overview

goto is C++'s unconditional jump statement, inherited directly from C. Its reputation precedes it β€” Edsger Dijkstra's argument against unstructured jumps shaped how structured programming developed β€” but that reputation is somewhat deserved and somewhat overblown. In modern C++, goto has a narrow legitimate role.

The two patterns where goto is genuinely defensible:

  • Nested loop exit: breaking out of multiple levels of nested loops without auxiliary flags, lambdas, or function calls.
  • Generated code and state machines: compiler output, parser generators (re2c, Bison), and hand-written state machines frequently emit goto-heavy C++, because goto maps directly to edges in a control flow graph.

In new handwritten code, RAII and exceptions replace the classic C pattern of jumping to a cleanup: label. But the nested-loop case has no equally clean alternative in the core language.

Syntax

cpp
goto identifier;        // transfer control to label
identifier: statement   // label definition

A label is any identifier followed by a colon, attached to any statement. Labels have function scope: a label defined inside a block is visible throughout the entire enclosing function β€” before and after the block in which it appears. Duplicate label names in the same function are ill-formed.

cpp
void f() {
    goto end;    // forward jump β€” valid
    // ...
end:             // visible from anywhere in f()
    return;
}

When a label must appear at the end of a block or function with no real statement following it, attach it to a null statement:

cpp
end:
    ;   // null statement

Scope Rules and Variable Declarations

The critical constraint: jumping forward over a variable initialization is ill-formed unless the variable's type permits it.

cpp
goto skip;
int x = 42;        // ERROR: has initializer, jump over it is ill-formed
skip:
    use(x);

A forward jump is well-formed only if every variable whose scope is entered satisfies all of these conditions:

  • Declared without an initializer
  • Has a scalar type, a class type with trivial default constructor and trivial destructor, a cv-qualified version of either, or an array thereof
cpp
goto label2;

[[maybe_unused]] int n;       // OK: scalar, no initializer
[[maybe_unused]] double d;    // OK: scalar, no initializer
// int x = 1;                 // ERROR: has initializer
// std::string s;             // ERROR: non-trivial constructor/destructor

label2:
    ;

Note that jumping over an uninitialized scalar is syntactically legal but semantically dangerous β€” reading that variable afterward is undefined behavior, because its value is indeterminate.

The same rules apply to all forms of control transfer, including switch fallthrough into a case after a declaration.

goto cannot transfer control into a try block. Jumping out of a try block is legal (automatic variables in the try body are properly destroyed).

cpp
goto into_try;     // ill-formed
try {
into_try:          // cannot enter here via goto
    ;
} catch (...) {}

Destructor Invocation

When goto exits the scope of any automatic variables, their destructors are called in reverse construction order. RAII interacts correctly with goto.

cpp
struct Guard {
    ~Guard() { release_lock(); }
};

void critical_section(bool abort_early) {
    {
        Guard g;
        if (abort_early)
            goto after;    // g.~Guard() is called before the jump
        do_work();
    }
after:
    ;
}

Jumping backward to re-enter a scope is valid when the variables in that scope satisfy the conditions above. Each pass through the declarations constructs fresh objects and each exit destroys them:

cpp
int n = 3;
top:
    Guard g;              // constructed on every pass
    do_something();
    if (--n > 0)
        goto top;         // g.~Guard() called, then re-enters loop

Examples

Breaking Out of Nested Loops

The canonical legitimate use case. The alternatives β€” a bool found flag checked at each level, a wrapping lambda with return, or a helper function β€” each introduce indirection or noise. goto is the most direct expression of the intent.

cpp
bool find_in_grid(const int grid[][COLS], int rows, int target,
                  int& out_r, int& out_c) {
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < COLS; ++c) {
            if (grid[r][c] == target) {
                out_r = r;
                out_c = c;
                goto found;
            }
        }
    }
    return false;

found:
    return true;
}

Generated State Machines

Tools that emit C++ often produce goto-based state machines. This is intentional β€” goto maps cleanly onto transition edges without function-call overhead:

cpp
// Typical structure from a lexer generator
int lex(const char* p) {
s0:
    if (*p == '\0') goto accept_end;
    if (*p >= 'a' && *p <= 'z') { ++p; goto s1; }
    goto reject;
s1:
    if (*p >= 'a' && *p <= 'z') { ++p; goto s1; }
    if (*p == '\0') goto accept_ident;
    goto reject;
accept_ident:
    return TOKEN_IDENT;
accept_end:
    return TOKEN_EOF;
reject:
    return TOKEN_ERROR;
}

Attempting to "refactor" generated goto code into structured form often produces slower or harder-to-maintain output than leaving it as emitted.

C++23: goto in constexpr Functions

Prior to C++23, goto was prohibited inside constexpr functions. C++23 lifts this restriction β€” goto and non-case labels are now permitted, provided the function remains evaluable at compile time when called in a constant expression context.

cpp
// C++23
constexpr int triangular(int n) {
    int sum = 0;
loop:
    if (n > 0) {
        sum += n--;
        goto loop;
    }
    return sum;
}

static_assert(triangular(5) == 15);  // C++23

Best Practices

Restrict goto to nested loop exit and generated code. These are the two cases where the alternatives are objectively worse or where the pattern is idiomatic.

Keep label and goto close together. A jump that crosses more than a screenful of code is a maintenance hazard. If the distance is large, restructure.

Use descriptive label names. found, done, end_loops, error communicate intent. L1, lbl, x do not.

Prefer RAII over goto-to-cleanup. The C idiom of jumping to a cleanup: block to release resources has no place in modern C++ where destructors exist. Wrap resources in RAII types instead.

Never read an uninitialized variable entered by forward jump. The compiler allows jumping over uninitialized scalars; reading them afterward is undefined behavior.

Common Pitfalls

Indeterminate values after jumping over uninitialized scalars. The compiler rejects jumps over initialized or non-trivially-typed variables, but silently allows jumping over int n;. Using n after the jump is undefined behavior:

cpp
goto skip;
int n;         // no initializer β€” jump is syntactically permitted
skip:
use(n);        // undefined behavior: n holds indeterminate value

Confusing function-scoped labels with block-scoped names. A label defined inside an if body is visible to code preceding the if. This surprises developers who assume labels are block-scoped like variables.

Forgetting the null statement at end of scope. A label at the end of a block must be followed by at least a null statement; a bare label before } is a syntax error:

cpp
if (cond) {
    goto end;
    do_work();
end:    // ERROR: label at end of compound statement requires a statement
}

Fix:

cpp
end:
    ;

Re-entering scopes with backward jumps. Jumping backward past declarations means those declarations execute again on each pass, which is usually the intent when emulating a loop. If the variables have non-trivial types, confirm this matches expectations.

See Also

  • reference/language/constant-expressions β€” C++23 permits goto and labels in constexpr functions
  • reference/language/const-correctness β€” cv-qualified variables are among the types that may be jumped over without initialization