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.
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