Advanced Control Flow
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
C offers four ways to transfer control beyond the ordinary statements: a short jump (goto) within a
function, a function call and its return, a long jump (longjmp) across frames, and asynchronous
interruption by a signal. The last two come with restrictions strict enough that most code should avoid them — knowing exactly what those restrictions are is the point of this page.
Sequencing Recap
Before jumping anywhere, recall from Operators and Expressions what C guarantees about order within a statement:
-
Full expressions are sequenced: everything in one statement completes before the next statement starts.
-
&&,||,?:and the comma operator impose an order on their operands; nothing else does. -
Function arguments are indeterminately sequenced — evaluated in some unspecified order, but not interleaved.
-
Two unsequenced writes to the same object, or a write and a read, are undefined behavior.
Everything below inherits those rules: a longjmp or a signal can only land between sequenced points, and
whatever was mid-expression is lost.
Short Jumps — goto for Cleanup
goto transfers control to a label in the same function. Its legitimate uses are cleanup and escaping
nested loops, both shown in Control Flow. Two constraints
worth restating:
-
You may not jump into the scope of a variable-length array, and jumping over an ordinary declaration leaves that object uninitialized (its lifetime began, its initializer did not run).
-
gotocannot cross function boundaries — that is whatlongjmpis for.
#include <stdio.h>
int main(void)
{
int status = 0;
// Jumping FORWARD over a declaration: value's lifetime has begun but its
// initializer never ran, so reading it would be undefined.
goto skip;
int value = 42; // this initialization is skipped
status = value;
skip:
printf("status = %d\n", status); // 0, and value must not be read here
return 0;
}
Function Calls as Control Transfer
A call is the ordinary structured jump: it saves a return address, allocates a frame for the callee’s automatic objects, and returns control after the call expression. Two properties matter for the rest of this page:
-
Automatic objects are destroyed at return — so a pointer to one is dangling afterwards.
-
The call and return are sequenced, which is why they compose safely and
longjmpdoes not.
Recursion, [[noreturn]] functions and function pointers are covered in
Functions.
Long Jumps — setjmp and longjmp
setjmp records the machine context in a jmp_buf; longjmp restores it, abandoning every frame in between.
It is the closest thing C has to an exception — and it is not one.
context saved M->>P: parse(input) P->>T: tokenize(text) Note over T: unrecoverable error T-->>M: longjmp(env, 2) Note over P,T: frames of parse() and tokenize()
are abandoned - no cleanup runs,
nothing is freed or closed Note over M: setjmp(env) returns 2 again
execution resumes here
#include <limits.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
static jmp_buf recovery;
// Values passed to longjmp: never 0, because 0 is what setjmp returns initially.
enum { ERROR_SYNTAX = 1, ERROR_OVERFLOW = 2 };
[[noreturn]] static void fail(int reason)
{
longjmp(recovery, reason); // abandons every frame back to setjmp
}
static long parse_digits(const char *text)
{
long value = 0;
for (const char *p = text; *p != '\0'; ++p) {
if (*p < '0' || *p > '9') {
fail(ERROR_SYNTAX);
}
if (value > (LONG_MAX - (*p - '0')) / 10) {
fail(ERROR_OVERFLOW);
}
value = value * 10 + (*p - '0');
}
return value;
}
int main(void)
{
// Anything modified between setjmp and longjmp and read afterwards MUST be
// volatile -- otherwise the value it holds after the jump is indeterminate.
volatile int attempts = 0;
const char *inputs[] = { "123", "12x", "99999999999999999999999" };
for (size_t i = 0; i < sizeof inputs / sizeof inputs[0]; ++i) {
int reason = setjmp(recovery); // 0 on the direct call
if (reason == 0) {
++attempts;
printf("%s -> %ld\n", inputs[i], parse_digits(inputs[i]));
} else {
printf("%s -> failed with reason %d\n", inputs[i], reason);
}
}
printf("attempts = %d\n", attempts);
return 0;
}
The restrictions, which are unusually strict:
-
setjmpmay only appear in a handful of contexts: as a whole expression statement, or as the operand of a comparison against an integer constant in anif/switch/loop condition.int r = setjmp(env);is technically outside what the standard guarantees, though every real implementation accepts it. -
Local variables that are not
volatilehave indeterminate values after alongjmpif they were modified since thesetjmp. This is thevolatile int attemptsabove, and it is the most commonly missed rule. -
longjmpinto a function that has already returned is undefined — thejmp_bufis dead. -
Nothing is cleaned up. No
free, nofclose, no unlocking of mutexes. Every resource acquired in an abandoned frame leaks. -
Never pass 0 to
longjmp; it is converted to 1, which conflates a jump with the initial call. -
Combining
longjmpwith variable-length arrays, or jumping out of a signal handler, is undefined.
Because of the fourth point, setjmp/longjmp is the wrong tool for ordinary error handling. Its
defensible uses are narrow: aborting deep recursion in an interpreter or parser that owns no unmanaged
resources, and recovering in a language runtime. Everywhere else, return an error code and clean up with
goto — see
Error Handling and Program Failure.
Signals
A signal interrupts the program asynchronously and runs a handler on whatever stack was executing. The
standard defines six: SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM.
#include <signal.h>
#include <stdio.h>
// The ONLY object type a handler may portably touch: volatile sig_atomic_t.
static volatile sig_atomic_t interrupted = 0;
static void handle_interrupt(int signal_number)
{
(void)signal_number;
interrupted = 1; // set a flag -- that is all a handler should do
}
int main(void)
{
if (signal(SIGINT, handle_interrupt) == SIG_ERR) {
fputs("cannot install handler\n", stderr);
return 1;
}
// The main loop polls the flag and does the real work outside the handler.
for (int i = 0; i < 3 && !interrupted; ++i) {
printf("working (%d)\n", i);
}
if (interrupted) {
puts("interrupted -- shutting down cleanly");
}
signal(SIGINT, SIG_DFL); // restore the default disposition
return 0;
}
Async-Signal Safety
This is the part that makes signals hazardous. Inside a handler, standard C permits only:
-
Assigning to (and reading) an object of type
volatile sig_atomic_t, or a lock-free atomic. -
Calling
signalfor the signal currently being handled,abort,_Exit,quick_exit, andraisefor the same signal. -
Returning.
Everything else is undefined — including printf, malloc, free, and any function that touches errno or
the streams. The reason is reentrancy: the signal may have arrived inside malloc, so calling malloc
again corrupts its state. POSIX defines a longer list of async-signal-safe functions (write, _exit,
signalfd machinery), which is what real programs use.
Two further C-specific rules:
-
A handler for
SIGFPE,SIGILLorSIGSEGV— signals raised by the program’s own undefined behavior — may not return normally. The behavior is undefined if it does, because there is nothing sensible to resume. -
Whether a handler stays installed after firing is implementation-defined in standard C (historic SysV semantics reset it to
SIG_DFL). POSIXsigactionfixes this, and is what portable code should use.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
static volatile sig_atomic_t termination_requested = 0;
static void request_termination(int signal_number)
{
(void)signal_number;
termination_requested = 1;
// NOT allowed here: printf, malloc, free, fopen, exit, longjmp.
}
int main(void)
{
signal(SIGTERM, request_termination);
// raise() sends a signal to the running program -- useful for testing a handler.
if (raise(SIGTERM) != 0) {
fputs("raise failed\n", stderr);
return EXIT_FAILURE;
}
printf("termination requested: %d\n", (int)termination_requested);
// abort() raises SIGABRT and does not return; commented out so the example runs.
// abort();
return 0;
}
The pattern to take away: a handler sets a flag; the main loop acts on it. Anything more ambitious needs
POSIX (sigaction with SA_RESTART, a self-pipe, or signalfd) rather than standard C.
See Also
-
Control Flow —
gotofor cleanup and escaping loops. -
Error Handling and Program Failure — why error codes beat non-local jumps.
-
Atomics and Memory Consistency — lock-free atomics, the other type a handler may touch.
-
Threads — why signal handling and threads interact badly.
-
C++: Error Handling — C++ replaces
setjmp/longjmpwith exceptions — and mixinglongjmpwith destructors is undefined.
References
-
WG14 N3220 — the C23 working draft (§7.13 "Nonlocal jumps `<setjmp.h>`", §7.14 "Signal handling `<signal.h>`", §5.1.2.3 "Program execution").
-
GNU C Library Reference Manual — Defining Signal Handlers (async-signal-safe functions).