goto transfers control to a label in the same function. It cannot jump into another function, and a jump cannot bypass initialization when entering a scope would violate C++ lifetime rules.
Most application code is easier to verify with loops, guard clauses, helper functions, and return values. RAII objects make cleanup happen when scope exits, removing a historical reason for cleanup labels.
A retry loop states its condition and progress in one place. The equivalent backward goto separates those facts and makes later edits riskier.
#include <iostream>
int main() {
for (int attempt = 1; attempt <= 3; ++attempt) {
std::cout << "attempt " << attempt << '\n';
const bool succeeded = attempt == 2;
if (succeeded) return 0;
}
return 1;
}
attempt 1
attempt 2
Low-level generated code or tightly measured state machines sometimes use goto, but that is an engineering exception requiring local scope, documented invariants, tests, and evidence that clearer constructs are inadequate.
Structured return and object lifetime remove the cleanup label commonly used in legacy code.
#include <fstream>
#include <string>
bool save(const std::string& path, const std::string& text) {
std::ofstream file(path);
if (!file) return false;
file << text;
return static_cast<bool>(file);
}
The stream closes on every return path.
A jump into a scope could bypass construction and leave an object apparently alive even though its constructor never ran.
Place the resource in an RAII object whose destructor performs cleanup automatically. Then each error path can return normally without duplicating release code.
It is occasionally used for a tightly contained exit from deeply nested low-level logic, but it should never bypass object initialization or replace ordinary structured flow. Before keeping it, try a helper function with early return, an algorithm, RAII cleanup, or a small state machine; these alternatives usually make invariants easier to verify.
Explore 500+ free tutorials across 20+ languages and frameworks.