Chapter 19: Variations in control flow
Introduces basic blocks and structured control flow, then explains why less predictable changes in control flow make program state harder to reason about.
After this chapter you will be able to
- Basic block boundaries. Describe a basic block as a maximal sequence of statements that executes through to its end once entered at its first statement. Explain that an ordinary function call temporarily suspends the block rather than ending it.
- Structured control flow benefits. Explain why composing basic blocks with conditionals and loops is useful: their syntactic nesting gives control flow a tree-like structure that readers can follow and compilers can optimize.
- Unexpected control flow risks. Explain that jumps across function boundaries, external events, and concurrent execution make program state harder to track, and give examples of the resulting object-use difficulties named here.
- Brace nesting and indentation. Explain what basic_blocks recognizes and how it transforms its input: it reads text containing nested {} constructs from stdin, removes the braces, and marks each nesting level in the output with another > character.
- Character dispatch and recursion depth. Trace the high-level behavior of descend for an ordinary character, an opening brace, and a closing brace, and describe how dp[0] tracks the current recursion depth.
- Relaxed expression evaluation. Explain that C need not evaluate expressions in written order: function argument expressions are unsequenced and may be evaluated in either order or interleaved. Describe why C permits this flexibility.
- Sequence point guarantees. Describe sequence points as boundaries that impose serialization, while recognizing that code between them can still have flexible ordering. Identify representative sequence points named in the section.
- Defined but indeterminate outcomes. Explain how the two side-effecting add calls used as printf arguments can execute in either order, yielding different outcomes despite defined behavior. State that array or structure initialization-list expressions are also indeterminately sequenced.
- Additional sequencing rules. State the sequencing rules for an operator's operation, updates by assignment or increment/decrement, and a function call relative to evaluations of its caller.
- Loop and goto object lifetimes. Distinguish the lifetime of a compound literal in a while-loop body from one in a surrounding block repeated by goto, and explain the consequence for access through a saved pointer.
- Exceptional uses of goto. Justify the section’s restricted use of goto, including the parsing example’s named jump targets.
- Local objects in recursive calls. Explain why simultaneously active recursive calls normally have separate local state.
- Pointer arguments and shared state. Describe how buffer and dp weaken the usual separation between recursive calls, and why the section favors a simple unsigned argument over dp as a pointer.
- Exceptional Parser Exit. Explain why the parser calls longjmp where it detects an exceptional condition, and what that means for the current call to descend.
- Jump Target And Result. Trace how setjmp establishes the parser's jump target and how a later longjmp selects an exceptional case in basic_blocks.
- Valid Jump Target Lifetime. Explain when the saved jmp_buf target remains usable for longjmp.
- Volatile Values Across Jumps. Explain why the parser's depth value needs volatile when it is changed on the normal path and read after a longjmp.
- Signal origins and abstraction. Distinguish synchronous hardware faults from asynchronous software signals, and explain that C signal handlers provide a portable but minimal abstraction for both.
- Interrupted flow and deferred handling. Trace what happens when a handler returns and explain why the example records a signal for later handling in normal execution.
- Safe handler state changes. Explain why shared handler state needs volatile qualification and uninterruptible operations, and distinguish suitable signal storage from a sig_atomic_t counter.
- Asynchronous signal safe calls. Explain why a handler for an asynchronous signal cannot freely call C library functions, and identify the limited kinds of calls the section permits.
What it builds on
- Statements (chapter 2) · Assumes familiarity with C statements.
- Iterations (chapter 3) · Assumes familiarity with loops as control-flow constructs.
- Storage duration, lifetime, and visibility (chapter 13) · Assumes the idea of an object's lifetime.
- Recursion (chapter 7) · The idea that a function can call itself recursively.
- Evaluation order (chapter 4) · Earlier discussion of expression evaluation order.
- Error checking and cleanup (chapter 15) · Earlier use of goto for error checking and cleanup.
- Automatic storage duration (chapter 13) · The lifetime of objects with automatic storage duration.
- Pointer operations (chapter 11) · Pointers that refer to objects
Section by section
Introduction
Introduces basic blocks and structured control flow, then explains why less predictable changes in control flow make program state harder to reason about.
- Basic block boundaries: Describe a basic block as a maximal sequence of statements that executes through to its end once entered at its first statement. Explain that an ordinary function call temporarily suspends the block rather than ending it.
Why it matters: Basic blocks give you a way to describe which statements execute together. - Structured control flow benefits: Explain why composing basic blocks with conditionals and loops is useful: their syntactic nesting gives control flow a tree-like structure that readers can follow and compilers can optimize.
Why it matters: Keeping control flow structured helps both readers and compilers follow the program’s state. - Unexpected control flow risks: Explain that jumps across function boundaries, external events, and concurrent execution make program state harder to track, and give examples of the resulting object-use difficulties named here.
Why it matters: Recognizing these risks explains why unusual control flow needs extra care with program state.
Key terms: control flow, basic block, function calls, structured programs, basic blocks, syntactical nesting, abstract state, longjmp, interrupts, threads
19.1 A detailed example
Introduces a compact recursive descent parser that replaces nested braces with indentation and explains the main control flow of its descend function.
- Brace nesting and indentation: Explain what basic_blocks recognizes and how it transforms its input: it reads text containing nested {} constructs from stdin, removes the braces, and marks each nesting level in the output with another > character.
Why it matters: This gives you the intended result to keep in mind while following the parser’s control flow. - Character dispatch and recursion depth: Trace the high-level behavior of descend for an ordinary character, an opening brace, and a closing brace, and describe how dp[0] tracks the current recursion depth.
Why it matters: These three character cases explain the parser’s main behavior without requiring you to follow its exceptional-case code.
Key terms: basic_blocks, recursive descent parser, nesting, indentation, descend, switch, ASCEND, dp[0], recursion depth
19.2 Sequencing
This section explains which evaluations C orders, which it leaves unordered, and why defined programs can still produce different results.
- Relaxed expression evaluation: Explain that C need not evaluate expressions in written order: function argument expressions are unsequenced and may be evaluated in either order or interleaved. Describe why C permits this flexibility.
Why it matters: Written order alone does not tell you when function arguments are evaluated. - Sequence point guarantees: Describe sequence points as boundaries that impose serialization, while recognizing that code between them can still have flexible ordering. Identify representative sequence points named in the section.
Why it matters: A sequence point provides an ordering guarantee, but not necessarily the particular order you might expect. - Defined but indeterminate outcomes: Explain how the two side-effecting add calls used as printf arguments can execute in either order, yielding different outcomes despite defined behavior. State that array or structure initialization-list expressions are also indeterminately sequenced.
Why it matters: Different results do not, by themselves, mean the program has undefined behavior. - Additional sequencing rules: State the sequencing rules for an operator's operation, updates by assignment or increment/decrement, and a function call relative to evaluations of its caller.
Why it matters: These rules provide ordering guarantees even when written order does not.
Key terms: unsequenced, function arguments, sequence points, sequenced before, indeterminately sequenced, defined behavior, side effects, initialization list expressions, operands, assignment
19.3 Short jumps
This section contrasts local-object lifetimes in loops and goto-based repetition, then explains when a short jump is justified.
- Labels and local jumps: Describe how labels and goto statements implement a short jump within a function.
Why it matters: This identifies the control-flow change behind the section’s examples. - Loop and goto object lifetimes: Distinguish the lifetime of a compound literal in a while-loop body from one in a surrounding block repeated by goto, and explain the consequence for access through a saved pointer.
Why it matters: Similar-looking repetition can leave a saved pointer valid in one example and invalid in the other. - Exceptional uses of goto: Justify the section’s restricted use of goto, including the parsing example’s named jump targets.
Why it matters: It explains why the section discourages routine goto use while allowing a purposeful jump.
Key terms: goto, labels, compound literal, lifetime, while, exceptional changes in control flow, local cleanup, NEW_LINE, ASCEND
19.4 Functions
The section explains how recursive calls to descend have separate local objects while pointer arguments can expose shared state, and why restoring a pointed-to value matters to optimization.
- Local objects in recursive calls: Explain why simultaneously active recursive calls normally have separate local state.
Why it matters: This establishes what state a recursive call normally keeps to itself. - Pointer arguments and shared state: Describe how buffer and dp weaken the usual separation between recursive calls, and why the section favors a simple unsigned argument over dp as a pointer.
Why it matters: It shows why separate recursive calls do not necessarily mean separate data. - Restored depth and optimization: Trace why dp[0] appears unchanged after an ordinary return from descend and what a compiler could infer if the function code is visible.
Why it matters: The apparent lack of a lasting change sets up the optimization caveat examined next.
Key terms: recursive functions, local object, program state, buffer, dp, pointer, unsigned argument, dp[0], restrict-qualified, longjmp
19.5 Long jumps
The section explains how setjmp and longjmp handle exceptional parser conditions by jumping across function calls, along with the validity, syntax, optimization, and type constraints involved.
- Exceptional Parser Exit: Explain why the parser calls longjmp where it detects an exceptional condition, and what that means for the current call to descend.
Why it matters: This explains why an error deep in the parser can be handled without unwinding each recursive call individually. - Jump Target And Result: Trace how setjmp establishes the parser's jump target and how a later longjmp selects an exceptional case in basic_blocks.
Why it matters: The value appearing to come from setjmp determines whether the switch follows its normal path or handles an exception. - Valid Jump Target Lifetime: Explain when the saved jmp_buf target remains usable for longjmp.
Why it matters: A saved target is useful only while there is still a valid execution context to jump back to. - Restricted Setjmp Expression Forms: Describe how the section permits setjmp to be used in controlling expressions and what use it rules out.
Why it matters: The parser's switch uses setjmp in a permitted form rather than treating its result like an ordinary int expression. - Volatile Values Across Jumps: Explain why the parser's depth value needs volatile when it is changed on the normal path and read after a longjmp.
Why it matters: Without the qualifier, the diagnostic after a jump may report an optimized assumption rather than the changed depth. - Opaque Array Jump Buffer: Describe the array-type consequences of jmp_buf for assignment and function parameters.
Why it matters: The hidden array type explains why passing a jump buffer to descend does not make an independent copy.
Key terms: longjmp, descend, basic_blocks, setjmp, jmp_buf, condition argument, jmpTarget, controlling expressions, switch statement, volatile
19.6 Signal handlers
This section explains how C signal handlers respond to externally triggered events, why their work must be limited, and how a program can handle the event safely after normal execution resumes.
- Signal origins and abstraction: Distinguish synchronous hardware faults from asynchronous software signals, and explain that C signal handlers provide a portable but minimal abstraction for both.
Why it matters: The source of a signal helps explain why a handler may encounter program execution in a delicate state. - Signal dispositions and interfaces: Describe how a signal number selects a disposition, what a handler receives, and the roles of signal and raise.
Why it matters: These are the basic pieces needed to follow what the section's example handler does. - Interrupted flow and deferred handling: Trace what happens when a handler returns and explain why the example records a signal for later handling in normal execution.
Why it matters: Recording a signal and acting on it later keeps most program logic out of the handler. - Safe handler state changes: Explain why shared handler state needs volatile qualification and uninterruptible operations, and distinguish suitable signal storage from a sig_atomic_t counter.
Why it matters: An apparently simple C operation is not necessarily safe to interrupt or perform inside a handler. - Asynchronous signal safe calls: Explain why a handler for an asynchronous signal cannot freely call C library functions, and identify the limited kinds of calls the section permits.
Why it matters: A handler that calls an ordinary library function may be unsafe precisely because it can run while other work is interrupted. - Portable signal handling strategy: Justify keeping signal handlers minimal and doing most response logic in normal program flow, including when platform-specific signal names are used.
Why it matters: This approach limits dependence on platform extensions and on what a handler is allowed to do.
Key terms: hardware interrupts, synchronous signals, software interrupts, asynchronous signals, signal handlers, signal number, disposition, SIG_DFL, SIG_IGN, signal
19.7 Summary
Where this is used later
- (chapter intro) (chapter 20) · Introduces threads as another variation of control flow.
- Simple interthread control (chapter 20) · Develops control involving another thread of execution.
- The “happened-before” relation (chapter 21) · Introduces sequencing of evaluations.
- Critical data and critical sections (chapter 20) · The distinction between ordinary and atomic access helps frame later discussion of shared data.
Concepts explained in this chapter
- volatile qualificationThe C volatile qualifier specifies that accesses to an object may have externally observable effects and must not be treated as freely eliminable or cacheable by the compiler.
- signal handlerA signal handler is a function invoked in response to a signal raised by an execution environment or program.
- signalAn asynchronous signal is an externally generated notification delivered to a running program independently of its current instruction sequence.
- sig_atomic_tsig_atomic_t is an integer type whose object evaluation and assignment operations are guaranteed to be indivisible with respect to signal handling.
- setjmp/longjmpA long jump is a nonlocal control transfer that can move execution across function-call boundaries.
- asynchronous signal safe functionAn asynchronous signal-safe function is a function permitted to execute in an asynchronous signal handler without uncontrolled access to program state.
- atomic typeA type specifies the kind of values an entity can represent and the operations applicable to those values.
- interruptA hardware interrupt is an execution event caused synchronously by a processor-detected fault or hardware condition.
- lockless techniqueThe lock-free property indicates that operations on an atomic type do not require a hidden lock and are indivisible with respect to signal interruption.
- signal dispositionA signal disposition is the action configured for a particular signal when that signal is delivered.
- signal numberA signal number is an integer identifier representing a particular signal in the C signal interface.
- signal.hsignal.h is the standard C header that declares the signal-handling functions, types, and constants.
- exceptionAn exception is an abnormal execution condition that can interrupt ordinary computation and potentially terminate a program.
- jmp_bufjmp_buf is an opaque C type for storing the execution environment used by setjmp and longjmp.
- opaque typeAn opaque data type is a type whose representation and implementation details are hidden from code that uses it.
- performance optimizationOptimization is the transformation of a program's implementation to improve execution or compilation while preserving its required observable behavior.
- callA function call is an expression or statement that requests execution of a function with supplied arguments.
- gotogoto is a C jump statement that transfers execution to a labeled statement within the same function.
- labelA label is an identifier followed by a colon that marks a statement as a target for a goto statement.
- lifetimeAn object's lifetime is the interval of program execution during which the object exists and can be accessed according to the language rules.
- indeterminately sequenced evaluationIndeterminately sequenced evaluation is an evaluation relationship in which one evaluation completes before the other, but the language does not specify which occurs first.
- sequence pointA sequence point is a point in C execution at which the language requires specified evaluations and side effects to be completed or ordered relative to other evaluations.
- unsequenced operationUnsequenced evaluation is an evaluation relationship in which the relative order of two evaluations is not specified and they may be interleaved.
- control flowControl flow is the order in which execution proceeds among a program's operations and functions.
- threadA thread of execution is an independently schedulable sequence of program operations within a running program.
- recursionRecursion is a technique in which a computation is defined or performed by calls to the same computation on smaller or simpler inputs.
- compound literalA compound literal is C syntax consisting of a type name in parentheses followed by an initializer that creates an unnamed object with the specified type and value.
- scopeA scope is a region of a C program within which an identifier is visible and its declaration applies.
- secondary blockA secondary block is the block formed by the loop body associated with a for construct in the terminology used here.
- quick_exitquick_exit is a C standard-library function that terminates a program after running functions registered for quick termination.
- loopA for statement is a C iteration statement that controls repeated execution using initialization, a continuation condition, and an iteration expression.
- operatorAn operator is a C symbol or symbol sequence that specifies an operation on one or more operands.
- conditional logicAn if-else statement conditionally selects between alternative statements based on a controlling expression.
- critical regionA critical section is a region of execution that accesses shared state under synchronization to prevent conflicting concurrent operations.
- do statementA do-while loop is a C iteration statement that executes its body once and then repeats it while its controlling condition is true.
- case statementA switch statement is a C selection statement that transfers control to a matching case or default label based on an expression's value.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
