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.

setjmp/longjmp Comparison

setjmp/longjmp are the C non-local jump mechanism β€” fundamentally different from goto. Where goto transfers control within a single function, longjmp transfers control across call frames, rewinding the call stack to a previously saved checkpoint.

setjmp saves the CPU register state (instruction pointer, stack pointer, callee-saved registers) into a jmp_buf. A subsequent longjmp restores that state unconditionally, causing execution to resume as if setjmp had returned a second time.

c
// C error handler using setjmp/longjmp
#include <setjmp.h>

static jmp_buf error_env;

void risky_operation() {
    // on failure, unwind to the setjmp site
    longjmp(error_env, 1);
}

int do_work() {
    if (setjmp(error_env) != 0) {
        // arrived here via longjmp
        return -1;
    }
    risky_operation();
    return 0;
}

longjmp does NOT call C++ destructors for objects on the unwound stack. If any non-trivial RAII objects exist in stack frames between the setjmp and the longjmp, their destructors are bypassed. This is undefined behavior in C++. The C++ equivalent is try/catch, which unwinds the stack properly and calls every destructor along the way.

cpp
// C++98 β€” correct C++ replacement for setjmp/longjmp error handling
void risky_operation_cpp() {
    throw std::runtime_error("operation failed");
}

int do_work_cpp() {
    try {
        risky_operation_cpp();
        return 0;
    } catch (const std::exception& e) {
        return -1;
    }
}

longjmp remains present in two contexts: C signal handlers (where C++ exceptions cannot be thrown safely) and C code compiled as part of a C++ project. In both cases, document explicitly that no non-trivial destructors may exist on the unwound path, and enforce this with [[noreturn]] annotations and static analysis.

Structured Alternatives to Nested Loop Exit

goto is the simplest mechanism for exiting multiple nested loops, but it is not the only one. Each alternative has a different cost profile.

Option 1: Boolean flag. Works in all standards; adds a variable and an extra condition test per loop level.

cpp
// C++98
bool found = false;
for (int r = 0; r < rows && !found; ++r) {
    for (int c = 0; c < cols && !found; ++c) {
        if (grid[r][c] == target) {
            out_r = r; out_c = c;
            found = true;
        }
    }
}

Option 2: Extract into a helper function and return. Cleanest semantic split; forces naming the extracted function, which often improves readability. The cost is that every variable needed inside must be passed as a parameter or captured β€” in generic code this is straightforward, in legacy code it can require significant refactoring.

cpp
// C++11
std::optional<Position> find_in_grid(const Grid& g, int target) {
    for (int r = 0; r < g.rows(); ++r)
        for (int c = 0; c < g.cols(); ++c)
            if (g[r][c] == target)
                return Position{r, c};
    return std::nullopt;
}

Option 3: Immediately-invoked lambda. Local to the call site, captures everything by reference, requires no separate function name. The return inside the lambda exits the lambda only, not the enclosing function.

cpp
// C++11
auto result = [&]() -> std::optional<Position> {
    for (int r = 0; r < rows; ++r)
        for (int c = 0; c < cols; ++c)
            if (grid[r][c] == target)
                return Position{r, c};
    return std::nullopt;
}();

Option 4: std::views::cartesian_product (C++23). Flattens the nested loop into a single range, eliminating the nesting problem entirely.

cpp
// C++23
#include <ranges>

auto indices = std::views::cartesian_product(
    std::views::iota(0, rows),
    std::views::iota(0, cols)
);
auto it = std::ranges::find_if(indices, [&](auto idx) {
    auto [r, c] = idx;
    return grid[r][c] == target;
});

Decision heuristic: for a simple two-level search, goto remains readable and direct. For logic that needs a name for documentation or reuse purposes, extract to a function. For generic iteration pipelines in modern code, prefer the ranges approach.

Coroutine Compiler Output β€” goto as Machine Code

C++ coroutines (C++20) compile down to a state machine. The compiler transforms every co_yield and co_await into a numbered state, then emits a resume() function that switches on the current state and jumps to the appropriate label. The resulting output is a goto-heavy switch statement β€” the same pattern human authors avoid, used freely by the compiler.

cpp
// C++20 β€” source coroutine
#include <coroutine>

struct Task { struct promise_type; using handle = std::coroutine_handle<promise_type>; /* ... */ };

Task gen() {
    co_yield 1;
    co_yield 2;
}

The compiler generates approximately:

cpp
// Approximate compiler output (simplified) β€” not standard C++
void gen_resume(GenFrame* frame) {
    switch (frame->state) {
        case 0: goto state_0;
        case 1: goto state_1;
        case 2: goto state_2;
    }
state_0:
    frame->value = 1;
    frame->state = 1;
    return;   // suspend
state_1:
    frame->value = 2;
    frame->state = 2;
    return;   // suspend
state_2:
    frame->done = true;
    return;
}

Duff's Device is a related pattern β€” a switch statement with case labels inside a loop body β€” used to unroll copy loops in C. The compiler naturally emits similar constructs when vectorizing or unrolling loops.

goto carries stigma in handwritten business logic, not at machine level. Every structured loop compiles to conditional branches and jumps. The objection to goto is about readability and maintainability of source code, not about what CPUs execute.

C Error Handling Pattern vs RAII

The classic C goto cleanup pattern exists because C has no destructors. When a function acquires multiple resources, each acquisition failure must jump to a label that releases only the resources acquired so far.

c
// C β€” goto cleanup pattern
int do_work(const char* path) {
    int result = -1;
    FILE* f = fopen(path, "r");
    if (!f) goto end;

    int* buf = malloc(1024 * sizeof(int));
    if (!buf) goto close_file;

    // ... work with f and buf ...
    result = 0;

    free(buf);
close_file:
    fclose(f);
end:
    return result;
}

This pattern is structured and disciplined β€” a meaningful improvement over ad-hoc if-chains β€” but it requires the programmer to order labels correctly and match every goto to the right cleanup point manually.

C++ RAII eliminates the pattern entirely. Each resource is wrapped in an RAII type whose destructor releases the resource. All exit paths β€” early return, throw, normal fall-through β€” invoke destructors automatically.

cpp
// C++11 β€” RAII replacement
#include <cstdio>
#include <memory>

struct FileCloser { void operator()(FILE* f) const { if (f) fclose(f); } };
using FilePtr = std::unique_ptr<FILE, FileCloser>;

int do_work(const char* path) {
    FilePtr f{fopen(path, "r")};
    if (!f) return -1;

    auto buf = std::make_unique<int[]>(1024);
    // ... work with f and buf ...
    return 0;
    // destructors run here on every exit path
}

When calling C APIs that allocate multiple resources, RAII wrappers using std::unique_ptr with a custom deleter, or a scope_guard type, replace goto cleanup patterns entirely. The C goto-cleanup idiom should be treated as a code smell when encountered in new C++ code.

goto in Parser and Lexer Generated Code

Parser and lexer generators emit goto as a first-class tool, not as a workaround. re2c generates C/C++ scanners where goto implements transitions between DFA states. bison and flex use similar mechanisms for LALR(1) automaton state transitions.

A simplified two-state lexer produced by re2c looks like:

cpp
// Generated by re2c (simplified) β€” do not hand-edit
int lex(const char* p, const char* pe) {
    const char* ts;
    goto state_start;

state_start:
    ts = p;
    if (p >= pe)  goto state_eof;
    if (*p >= 'a' && *p <= 'z') { ++p; goto state_ident; }
    if (*p >= '0' && *p <= '9') { ++p; goto state_digit; }
    goto state_error;

state_ident:
    if (p < pe && (*p >= 'a' && *p <= 'z' || *p >= '0' && *p <= '9')) { ++p; goto state_ident; }
    return TOKEN_IDENT;

state_digit:
    if (p < pe && *p >= '0' && *p <= '9') { ++p; goto state_digit; }
    return TOKEN_NUM;

state_eof:   return TOKEN_EOF;
state_error: return TOKEN_ERROR;
}

Do not refactor goto out of generated parser or lexer code. The tool owns that file and will regenerate it on the next build, discarding any manual restructuring. More importantly, the goto in generated code is semantically intentional: each jump corresponds to one edge in the automaton, and the structure encodes the grammar directly.

The performance benefit over a switch-per-character loop is measurable in hot lexer paths: goto compiles to a single unconditional branch instruction with no comparison, while a switch requires at minimum one comparison to select the next state. Modern CPUs with branch predictors handle dense goto tables well, and the practical difference is small for most workloads β€” but for a lexer processing gigabytes of source text, the directness of the jump matters.

Alternative Control Flow Tools: Comparison Table

The right tool depends on what scope boundary needs to be crossed and what cleanup semantics are required.

MechanismExitsDestructor callsOverheadNotes
breakInnermost loop or switch onlyYes (scope exit)NoneCannot exit multiple levels
continueSkips to next iterationYes (scope exit)NoneInnermost loop only
returnCurrent function (all loops)Yes (full unwind)NoneRequires extracting to a function
throwAny scope, across framesYes (full unwind)Non-zero at throw siteReserve for truly exceptional conditions
gotoWithin function, to named labelYes (exited scopes)NoneCannot cross variable initializations
Immediately-invoked lambda + returnExits the lambda (local to call site)YesMinor code sizeCaptures context; no naming burden
std::expected / error return valueCaller propagates manuallyYesNone (data copy)Structured propagation without exceptions

Scenario-based selection:

  • One level of loops, simple condition: break or continue.
  • Multiple nested loops, no complex state: goto with a descriptive label.
  • Multiple nested loops, logic worth naming: extract to a helper function, use return.
  • Multiple nested loops, modern codebase: immediately-invoked lambda or ranges pipeline.
  • Error that cannot be handled locally: throw; use std::expected if exceptions are disabled or performance-critical.
  • C API interop with multiple resources: RAII wrappers; avoid goto cleanup entirely.
  • Generated code (re2c, bison, coroutine frames): leave goto as emitted; it is semantically load-bearing.

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