goto
Unconditional jump statement that transfers control to a labeled statement within the same function, respecting RAII destructor semantics.
gotosince C++98Unconditionally 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++, becausegotomaps 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
goto identifier; // transfer control to label
identifier: statement // label definitionA 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.
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:
end:
; // null statementScope Rules and Variable Declarations
The critical constraint: jumping forward over a variable initialization is ill-formed unless the variable's type permits it.
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
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).
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.
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:
int n = 3;
top:
Guard g; // constructed on every pass
do_something();
if (--n > 0)
goto top; // g.~Guard() called, then re-enters loopExamples
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.
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:
// 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.
// 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++23Best 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:
goto skip;
int n; // no initializer β jump is syntactically permitted
skip:
use(n); // undefined behavior: n holds indeterminate valueConfusing 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:
if (cond) {
goto end;
do_work();
end: // ERROR: label at end of compound statement requires a statement
}Fix:
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 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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:
// 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 β 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.
// 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:
// 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.
| Mechanism | Exits | Destructor calls | Overhead | Notes |
|---|---|---|---|---|
break | Innermost loop or switch only | Yes (scope exit) | None | Cannot exit multiple levels |
continue | Skips to next iteration | Yes (scope exit) | None | Innermost loop only |
return | Current function (all loops) | Yes (full unwind) | None | Requires extracting to a function |
throw | Any scope, across frames | Yes (full unwind) | Non-zero at throw site | Reserve for truly exceptional conditions |
goto | Within function, to named label | Yes (exited scopes) | None | Cannot cross variable initializations |
Immediately-invoked lambda + return | Exits the lambda (local to call site) | Yes | Minor code size | Captures context; no naming burden |
std::expected / error return value | Caller propagates manually | Yes | None (data copy) | Structured propagation without exceptions |
Scenario-based selection:
- One level of loops, simple condition:
breakorcontinue. - Multiple nested loops, no complex state:
gotowith 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; usestd::expectedif exceptions are disabled or performance-critical. - C API interop with multiple resources: RAII wrappers; avoid
goto cleanupentirely. - Generated code (re2c, bison, coroutine frames): leave
gotoas emitted; it is semantically load-bearing.
See Also
reference/language/constant-expressionsβ C++23 permitsgotoand labels inconstexprfunctionsreference/language/const-correctnessβ cv-qualified variables are among the types that may be jumped over without initialization