Strategy in Rust: Functions, Closures and Traits¶
Strategy separates a varying decision from the algorithm that uses it. In Rust, that decision may be a function, a closure or a named trait implementation; it need not become an inheritance hierarchy. Choose how to represent the variation from its state, ownership and runtime-selection requirements.
Rust Strategy requirements before choosing a type¶
Read closures, traits and dispatch, and the Part 5 checkpoint. These independent fixtures use stable Rust edition 2024 and C++20, with no additional dependencies.
Count accepted readings from the unchanged input [-500, 0, 46700]. Acceptance means reading >= minimum, inclusive. Nonnegative readings use minimum zero; warm readings use 40000. A lower runtime threshold of -1000 accepts all three. Zero is a real reading, not absence, and an empty input accepts none.
The counting algorithm calls its decision exactly once per input, in input order. That explicit contract matters when a policy records call counts. This is filtering, not parsing: inputs are already valid signed integers and comparisons do not add or subtract them. Both languages should print:
C++: a classic Strategy and a callable alternative¶
Save this complete program as main.cpp. The Policy interface is a conventional Strategy family, with a virtual destructor for potential owned use. Here its implementations live on the stack and are borrowed. count_with also accepts a function or lambda without requiring that interface.
Compile with assertions enabled:
The virtual interface permits calling a chosen implementation through Policy. That requirement does not mandate heap allocation. The callable template is an alternative, not an implementation of the same inheritance relationship. A captured lambda keeps state in its closure; lambda conversion rules do not turn an ordinary capturing lambda into a plain function pointer.
count_with takes its callable by value. Here the counter lambda captures a reference, so its copied closure still updates the external counter. An internally owned counter would be copied with the lambda and needs a different API if the caller expects to observe its accumulated state. Borrowing and copying callable objects are different ownership contracts.
Rust: callables first, named traits when useful¶
Run cargo new strategy --edition 2024, then replace src/main.rs with the complete source below. count_with accepts FnMut because the contract allows the decision to update captured state on repeated calls. Read-only closures and function pointers also satisfy this bound.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
Inside the project:
FnMut permits repeated calls with mutable access to the callable. Fn is a narrower receiver requirement, not a guarantee of mathematical purity: interior mutability or external effects are separate questions. This counter closure borrows calls mutably, so the caller observes its value after the closure has been consumed and the borrow ends. To retain an internally owned closure state across multiple counts, pass a mutable borrow of that closure.
The named Policy trait is useful when the domain needs a documented policy contract or several related operations. It is not required for every one-function variation. count_policy accepts concrete types or a borrowed trait object; ?Sized permits that latter argument. With a dyn Policy argument, the method call uses dynamic dispatch even though count_policy is generic. A generic function does not make every operation inside it statically dispatched.
Changed requirements: state, runtime choice and ownership¶
A captured threshold is runtime data without requiring dynamic dispatch: each invocation can supply a closure of a concrete type holding that threshold. An enum is also appropriate when configuration defines a closed, stable set of policy alternatives. Adding a new enum variant changes exhaustive handling; adding a new trait implementation can extend an open family without adding a variant to that enum.
Our two dyn references can choose between existing owners at runtime, but cannot outlive them. If a context must independently own a chosen open policy, it can store Box
If the policy records per-call history, define whether the count may repeat, cache, parallelise or short-circuit decisions. This fixture promises exactly once in input order; replacing it with an optimisation that skips calls changes the contract. A mutable trait method, FnMut or explicit returned statistics may express stateful behaviour more clearly than hidden shared mutation. Thread-safe execution requires additional bounds and synchronisation, not merely a Fn or const receiver.
Intentional failure: a function pointer cannot store captures¶
Compile this Rust program independently. A plain fn pointer has no storage for the captured threshold:
Repair the return type with impl Fn(i32) -> bool to return this single concrete closure type, and test both a nonnegative and a negative threshold. Alternatively, return a named value whose threshold field is explicit. Box
This C++ program likewise fails to convert a capturing lambda to a function pointer:
Use a deduced callable type for a fixed lambda expression or a suitable owning wrapper if the interface requires type erasure. Making minimum global would change the state/ownership contract rather than repairing it transparently.
Selection criteria and exercises¶
| Variation requirement | Candidate | Main obligation |
|---|---|---|
| No captured state, fixed signature | Function pointer | Document decision semantics |
| Runtime data in one concrete callable | Generic closure / opaque closure return | Specify capture ownership and call receiver |
| Named domain behaviour with concrete implementation | Trait bound / callable or constrained C++ template | Define semantics beyond method signatures |
| Closed runtime configuration alternatives | Enum / closed tagged representation | Handle new alternatives explicitly |
| Borrowed open policy family selected at runtime | &dyn Policy / virtual interface reference | Keep concrete owners alive |
| Independently owned open policy family | Box |
Define lifetime, construction failure and destruction |
- Add a policy with a threshold of 46701. Both concrete and dynamically selected paths must count zero without modifying the input.
- Change >= to > and retain the zero/boundary tests. The changed result is a contract failure, not a speed improvement.
- Repair the capturing-function-pointer example and test two thresholds. Explain why runtime threshold data does not require a trait object.
- Retain one mutable closure across two count operations. Assert six calls and preserve the accepted counts; distinguish borrowed state from copying a C++ closure that owns its counter.
- Choose an enum for a closed configuration format and a trait for an externally extended policy family. Identify what must change when adding a policy, and define who owns the selected value.
Verification and next step¶
On October 10, 2026, the extracted sources passed on the authorised Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust/Cargo 1.99.0 and GCC g++ 14.2.0. Rust check, exact-source formatting, five debug/release tests and exact outputs passed. The opaque closure repair and retained-counter variant passed assertions; capturing-function-pointer and Fn/FnMut mismatches were rejected with E0308/E0525. A deliberate >= to > change compiled but failed the inclusive-boundary test. C++20 normal output/assertions, captured-threshold repair, copied internal state and borrowed counter preservation passed at -O0/-O2; capturing-lambda conversion to a function pointer was rejected. Sources/logs were retained and generated Cargo targets cleaned. No dispatch timing, allocation-efficiency or code-size comparison is claimed.
Next: runtime State representations and typestate, comparing which operations are available and what survives a failed transition.
Previous: address stability and Pin · Next: State and typestate · Course overview