Chapter 20 — Functions II: the `calc-core` Lab
calc-core is a small arithmetic library built in three movements, each
targeting one of Chapter 20's central ideas:
Movement 1 — Recursion (notes 20.3). You implement factorial, a
recursive digit-sum (contrasted with Chapter 8's loop version), and a naive
fibonacci that makes the inefficiency of recursive overlap visible. Each
function forces you to identify the base case and the recursive case —
the two-part anatomy that every recursive function requires.
Movement 2 — Function pointers (notes 20.1). You implement apply,
which receives a binary operation as a function pointer and calls it —
the classic "pass behavior as a parameter" pattern. You also implement
pickOp, which maps a character to the address of the right operation
function and returns that pointer, giving you a tiny "jump table" whose
two steps (dispatch and execute) are decoupled by design.
Movement 3 — Lambdas and captures (notes 20.6, 20.7). You implement
countMatching, which accepts any callable — a plain function pointer, a
non-capturing lambda, or a capturing lambda that closes over a local
variable — through a std::function<bool(int)> parameter. The capturing-
lambda test is the payoff: it is the one that would fail to compile if the
parameter were a raw function pointer, because capturing lambdas are closure
objects, not free functions.
An ungraded demo.cpp (built by make run) ties all three
movements together in one program and shows CLI argument handling (notes 20.4)
while it's there.
Your tasks
factorial(n)— basic recursion. Returnn!recursively. Your base case must ben == 0returning1. Your recursive case returnsn * factorial(n - 1). Return type islong longso results up tofactorial(20)don't overflow.sumDigitsRecursive(n)— same job as the Chapter 8 loop, now recursive. Chapter 8'ssumOfDigitsused awhileloop; here you use recursion instead. Base case:n < 10(single digit; returnn). Recursive case:(n % 10) + sumDigitsRecursive(n / 10). Handle negativenby working on|n|: call yourself with-nwhenn < 0.fibonacci(n)— observe the exponential call tree. Two base cases:fibonacci(0) == 0,fibonacci(1) == 1. Recursive case:fibonacci(n-1) + fibonacci(n-2). Notes 20.3 flag this pattern as INEFFICIENT (exponential calls from overlapping sub-problems) — keep test values atn <= 20.apply(a, b, op)— call through a function pointer. Dispatch to the operation pointed-to byop:return op(a, b);. Guard first: ifopisnullptr, return0without calling (calling a null function pointer is undefined behavior).pickOp(c)— RETURN a function pointer (mini jump table). Map a character to the address of the matching free function and return it.'+'→&addOp,'-'→&subOp,'*'→&mulOp, anything else →nullptr. Use aswitchorif/elsechain. The caller passes the result straight toapply.countMatching(values, predicate)— count elements satisfying a callable. Iterate overvalueswith a range-forloop. For each elementv, callpredicate(v); if it returnstrue, increment a counter. Return the count. The predicate isstd::function<bool(int)>— it must accept both a plain function pointer (&isEven) and a capturing lambda ([threshold](int x){ return x >= threshold; }).
Success criteria
factorial(0) == 1— the base case (0! is 1, not 0)sumDigitsRecursive(0) == 0andsumDigitsRecursive(7) == 7— single-digit base cases land in the right branchsumDigitsRecursive(-123) == 6— magnitude handling for negativesfibonacci(0) == 0andfibonacci(1) == 1— both base cases are correctapply(10, 3, nullptr) == 0— the null guard must not crash the programpickOp('?') == nullptr— the default case returns null for unknownsapply(6, 2, pickOp('/')) == 0— full null-dispatch path is safe end-to-end- The capturing-lambda test for
countMatchingwiththresholdcaptured from a local variable — this is the test that requiresstd::function
Concepts practiced
- Recursion — base case / recursive case anatomy, call-stack growth, depth limits (notes 20.3)
- Function pointers — type syntax
int (*name)(int, int), passing as parameter (apply), returning from a function (pickOp), null-pointer guard (notes 20.1) usingtype alias for a function-pointer type (BinaryIntOp) — makes gnarly pointer syntax readable (notes 20.1, Ch 10 reuse)- Lambdas — unnamed function objects, non-capturing and capturing forms (notes 20.6, 20.7)
- Lambda captures by value —
[threshold](int x){ … }captures a local variable; the lambda stores its own copy (notes 20.7) std::function— type-erased callable wrapper that accepts pointers, non-capturing lambdas, and capturing lambdas uniformly (notes 20.1, 20.6)- Reused from earlier chapters:
std::vectorand range-for(Ch 16),usingaliases (Ch 10), header guards and multi-file layout (Ch 2),long longarithmetic (Ch 4), control flow (if/switch) (Ch 8)
Constraints
Allowed: recursion (if for base case, recursive call for recursive case);
raw function pointers (BinaryIntOp / int (*)(int, int)); std::function;
lambdas with value captures ([var](...){ ... }); std::vector; range-for;
switch/if; long long; <functional> (already included via calccore.h).
Forbidden (not taught yet or out of scope):
std::bind(not taught in Ch 20, inferior to lambdas)- variadic templates or generic
autolambdas as required syntax (mentioned in notes 20.6 but not required here —std::functionis the correct tool for the parameter type) - smart pointers (Ch 22)
goto
Required idioms (from notes 20.3): every recursive function must have a clearly reachable base case; every recursive call must move strictly toward that base case.
Recursion depth: keep n <= 20 for factorial and fibonacci in any
code you add — this avoids stack overflow on reasonable systems.
Build & run locally
make # compile-check starter/calccore.cpp + build the demo
make run # run the ungraded demo (shows all three movements live)
make test # grade your code -> RED until TASK blocks are filled in
make solution # build + run the reference demo
make cleanmake test is the real goal — it compiles the grader together with your
starter/calccore.cpp and exits non-zero on any failure.
Hints
Task 1 — factorial: anatomy of a recursive function
long long factorial(int n)
{
if (n == 0) // BASE CASE — no call, direct answer
return 1;
return n * factorial(n - 1); // RECURSIVE CASE — smaller sub-problem
}Mental model: factorial(3) waits for factorial(2), which waits for
factorial(1), which waits for factorial(0) → 1. Then answers unwind in
reverse: 1, 1, 2, 6. The call stack grows one frame per call, so depth == n.
Task 2 — sumDigitsRecursive vs. the Chapter 8 loop
Chapter 8 loop version:
int sumOfDigits(int n) {
if (n < 0) n = -n;
int sum { 0 };
while (n > 0) { sum += n % 10; n /= 10; }
return sum;
}Recursive version:
int sumDigitsRecursive(int n)
{
if (n < 0) return sumDigitsRecursive(-n); // magnitude
if (n < 10) return n; // BASE CASE: single digit
return (n % 10) + sumDigitsRecursive(n / 10); // RECURSIVE
}Both peel digits with % 10 / / 10. The loop uses a counter variable to
track progress; the recursion shrinks the problem through the argument.
The maximum call depth equals the number of digits — at most 10 for a 32-bit
int — so this recursion is shallow and safe.
Task 3 — why naive recursive fibonacci is slow
fibonacci(5) calls fibonacci(4) AND fibonacci(3). fibonacci(4) calls
fibonacci(3) AND fibonacci(2). Notice: fibonacci(3) is computed TWICE.
At n=20 this duplication reaches ~6765 calls. This is the "exponential call
tree" the notes warn about (notes 20.3 — "Recursion can be inefficient").
The correct recursive skeleton is:
int fibonacci(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}Keep test values ≤ 20 to avoid waiting. An iterative loop would be O(n) instead of O(2^n).
Task 4 — calling through a function pointer
A function pointer is called just like a regular function:
int apply(int a, int b, BinaryIntOp op)
{
if (!op) // null guard — ALWAYS check before calling
return 0;
return op(a, b); // call through the pointer
}BinaryIntOp is the alias from calccore.h for int (*)(int, int). Reading
the raw form: "op is a pointer to a function that takes two ints and
returns int." The *op grouping is mandatory for this meaning — without the
parentheses, int* op(int, int) means "function returning int*" instead.
Task 5 — returning a function pointer from a function
BinaryIntOp pickOp(char c)
{
switch (c)
{
case '+': return &addOp;
case '-': return &subOp;
case '*': return &mulOp;
default: return nullptr;
}
}You return the address of the function. The caller gets a BinaryIntOp
back and passes it to apply — the dispatch and the execution are decoupled.
This two-step pattern (get pointer, then call through it) is common in
dispatch tables, plugin loaders, and LLVM pass pipelines.
Task 6 — why std::function, and what a capture looks like
A raw function pointer (bool (*)(int)) can only point to plain, free
functions. A capturing lambda like [threshold](int x){ return x >= threshold; }
is a closure object — the compiler generates a class with a data member for
threshold. A raw pointer can't hold it.
std::function<bool(int)> uses type erasure to store any callable with the
right call signature — plain pointers, non-capturing lambdas, and capturing
lambdas all work.
int countMatching(const std::vector<int>& values,
std::function<bool(int)> predicate)
{
int count { 0 };
for (int v : values)
if (predicate(v)) ++count;
return count;
}Calling it with a capturing lambda:
int threshold { 7 };
int n { countMatching(values, [threshold](int x){ return x >= threshold; }) };The lambda captures threshold by value — it stores its own copy, so even
if threshold goes out of scope, the lambda's copy is safe.
Stretch goals
- Add
power(base, exp)using recursion:power(b, 0) == 1,power(b, n) == b * power(b, n-1). Observe the depth is O(exp). - Make
fibonaccinon-exponential by adding memoisation with astatic std::vector<int>cache (notes 20.3 — "a loop or memoisation is usually better"). - Add a
divOpandmodOpto the function-pointer movement and updatepickOpto dispatch'/'and'%'(add a zero-denominator guard). - In
countMatching, measure the overhead ofstd::functionvs. a template parameter: replacestd::function<bool(int)>withtemplate <typename Pred>and benchmark the two (template version — a preview of Ch 26 — must live in the header, so you'd move the body tocalccore.h). - Extend the demo to accept multiple integers on the command line and print a
table of factorial / fibonacci / digit-sum for each (needs
argc/argvloop iteration — notes 20.4).
// ============================================================================
// starter/calccore.cpp — the LEARNER's implementation file (Chapter 20)
// ----------------------------------------------------------------------------
// Fill in the six TASK blocks below. Each maps 1:1 to a task in the README
// and to a declaration in ../calccore.h. The bodies currently return WRONG
// PLACEHOLDERS so the file compiles immediately — that is why `make test` is
// RED right now. Your job is to turn it GREEN.
//
// make build compile-check your code (should already succeed)
// make test grade your code (RED until you fill these in)
// make solution run the reference solution to see expected output
//
// Everything here is PURE: arguments in, value out. No std::cin/cout inside
// the library functions — keep them deterministic and testable.
// ============================================================================
#include "../calccore.h"
// ─── MOVEMENT 1: RECURSION ──────────────────────────────────────────────────
// Anatomy review (notes 20.3):
// BASE CASE — the simplest input that can be answered directly (stops the
// recursion; EVERY path must reach it).
// RECURSIVE CASE — reduce the problem, then call yourself on the smaller part.
// The call stack grows one frame per call; always know the maximum depth.
// ─── TASK 1: factorial — recursion on integers ───────────────────────────────
// Implement factorial(n) recursively.
// Base case : if n == 0, return 1 (0! == 1 by convention)
// Recursive case: return n * factorial(n - 1)
//
// Return type is long long (can hold up to factorial(20) without overflow).
// Precondition: n >= 0. You do NOT need to handle negative n.
//
// >>> YOUR CODE HERE <<<
//
long long factorial(int n)
{
(void)n; // suppress unused-parameter warning on placeholder
return 0; // placeholder — wrong for every input (even factorial(0), which is 1)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: sumDigitsRecursive — same job as Ch8's loop, now recursive ───────
// Chapter 8 introduced sumOfDigits using a WHILE loop (notes 8, engine.h Task 3).
// Here you solve the same problem with recursion so you can feel the difference:
//
// Base case : n < 10 (single digit) — the digit IS the answer, return n.
// Recursive case: (n % 10) + sumDigitsRecursive(n / 10)
// ^last digit ^everything else, recurse on it
//
// Handle negative n by working on the magnitude:
// sumDigitsRecursive(-123) must equal 6 (same as for 123).
// sumDigitsRecursive(0) must equal 0.
//
// >>> YOUR CODE HERE <<<
//
int sumDigitsRecursive(int n)
{
(void)n; // suppress unused-parameter warning on placeholder
return 0; // placeholder — only correct for n == 0
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: fibonacci — witness the exponential call tree ────────────────────
// Implement fibonacci(n) recursively.
// Base cases : fibonacci(0) == 0, fibonacci(1) == 1
// Recursive case: fibonacci(n) == fibonacci(n-1) + fibonacci(n-2) for n >= 2
//
// *** WARNING (notes 20.3) *** naive recursive Fibonacci is EXPONENTIAL in the
// number of calls — fibonacci(n-1) and fibonacci(n-2) each re-solve overlapping
// sub-problems. This is intentional: you are meant to observe that recursion is
// not always the most efficient tool. Keep test values n <= 20 for this version.
//
// >>> YOUR CODE HERE <<<
//
int fibonacci(int n)
{
(void)n; // suppress unused-parameter warning on placeholder
return 0; // placeholder — wrong for all inputs except fibonacci(0)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── MOVEMENT 2: FUNCTION POINTERS ──────────────────────────────────────────
// Syntax reminder (notes 20.1):
//
// int (*name)(int, int) — `name` is a POINTER TO a function that takes two
// ints and returns int.
//
// BinaryIntOp op — same thing, via the `using` alias in calccore.h.
//
// Calling: op(3, 4) — call the function pointed-to by op.
// (*op)(3, 4) — explicit dereference; same result.
// Null check: if (op) { ... } before calling, if pointer may be null.
// Three plain free functions — addOp, subOp, mulOp ──────────────────────────
// These are ALREADY PROVIDED and complete. Do not change them.
// They exist so a caller can take their address: BinaryIntOp p { &addOp };
int addOp(int a, int b) { return a + b; }
int subOp(int a, int b) { return a - b; }
int mulOp(int a, int b) { return a * b; }
// ─── TASK 4: apply — call op(a, b) through the function pointer ──────────────
// The classic "callback" pattern (notes 20.1): receive BEHAVIOR as a parameter
// and delegate to it without knowing the specific operation.
//
// Implement: call op(a, b) and return the result.
// Guard : if op is nullptr, return 0 as a safe sentinel (do NOT call nullptr).
//
// You CALL a function pointer just like a regular function: op(a, b).
//
// >>> YOUR CODE HERE <<<
//
int apply(int a, int b, BinaryIntOp op)
{
(void)a; (void)b; (void)op; // suppress unused-parameter warnings on placeholder
return 0; // placeholder — returns 0 for everything
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: pickOp — RETURN a function pointer (mini jump table) ────────────
// Map a character to the address of the matching operation function.
//
// Return the ADDRESS of the right function — e.g. `return &addOp;` — or
// nullptr for an unrecognised character.
// '+' -> &addOp
// '-' -> &subOp
// '*' -> &mulOp
// anything else -> nullptr
//
// Use a switch or if/else chain. Return type is BinaryIntOp (see calccore.h).
// The caller will pass the returned pointer straight into apply().
//
// >>> YOUR CODE HERE <<<
//
BinaryIntOp pickOp(char c)
{
(void)c; // suppress unused-parameter warning on placeholder
return nullptr; // placeholder — always returns null (unknown op)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── MOVEMENT 3: LAMBDAS + CAPTURES ─────────────────────────────────────────
// Reminder (notes 20.6, 20.7):
//
// [](int x){ return x > 0; } — non-capturing lambda, no outer state
// [threshold](int x){ return x >= threshold; } — capture `threshold` by VALUE
// [&total](int x){ total += x; } — capture `total` by REFERENCE
//
// std::function<bool(int)> can store ANY of the above forms, plus a plain
// function pointer. That flexibility is exactly why countMatching uses it.
// ─── TASK 6: countMatching — count elements satisfying predicate ──────────────
// Iterate over `values` with a range-for loop.
// For each element v, call predicate(v); if it returns true, increment a counter.
// Return the final count.
//
// The predicate is std::function<bool(int)> — it will be tested with:
// • a plain function pointer (no captures needed)
// • a CAPTURING lambda that captures a local threshold variable
//
// >>> YOUR CODE HERE <<<
//
int countMatching(const std::vector<int>& values,
std::function<bool(int)> predicate)
{
(void)values; (void)predicate; // suppress warnings on placeholder
return 0; // placeholder — always reports zero matches
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// ============================================================================
// solution/calccore.cpp — REFERENCE SOLUTION (Chapter 20)
// ----------------------------------------------------------------------------
// Complete, correct, warning-clean. Peek only after you have made a real
// attempt at starter/calccore.cpp — the learning is in wiring up the recursion
// and function-pointer mechanics yourself, then comparing.
// ============================================================================
#include "../calccore.h"
// ─── MOVEMENT 1: RECURSION ──────────────────────────────────────────────────
// ─── TASK 1: factorial ───────────────────────────────────────────────────────
// The key anatomy (notes 20.3):
// BASE CASE — n==0, no recursive call, immediately returns 1.
// RECURSIVE CASE — n * factorial(n-1): one smaller sub-problem per call.
//
// Call-stack view for factorial(3):
// factorial(3)
// -> 3 * factorial(2)
// -> 2 * factorial(1)
// -> 1 * factorial(0)
// -> return 1 (base)
// return 1*1 = 1
// return 2*1 = 2
// return 3*2 = 6
//
// CS6340: the call graph of a recursive function is literally a tree — exactly
// what static analysis tools trace when they compute "how deep can this call
// go?" or "can this path loop forever?"
long long factorial(int n)
{
if (n == 0) // BASE CASE: 0! == 1 by convention
return 1;
return n * factorial(n - 1); // RECURSIVE CASE: shrink toward base
}
// ─── TASK 2: sumDigitsRecursive ──────────────────────────────────────────────
// Compare with Chapter 8's sumOfDigits (engine.h Task 3) which used a WHILE
// loop: `while (n > 0) { sum += n % 10; n /= 10; }`. Here the "shrinking"
// happens through the recursive call rather than through a loop variable.
//
// BASE CASE: n < 10 — the number IS a single digit; no more splitting needed.
// RECURSIVE: peel the last digit (n % 10), recurse on the remaining digits
// (n / 10). The call stack depth = number of digits — shallow.
//
// Magnitude handling: negate once at the top so the '%' and '/' that follow
// always work on a non-negative value (a negative dividend produces a negative
// remainder in C++17, which would corrupt the sum).
int sumDigitsRecursive(int n)
{
if (n < 0) // handle negative input: work on |n|
return sumDigitsRecursive(-n);
if (n < 10) // BASE CASE: single digit
return n; // digit IS the answer (0 lands here too -> 0)
return (n % 10) + sumDigitsRecursive(n / 10); // RECURSIVE CASE
}
// ─── TASK 3: fibonacci ───────────────────────────────────────────────────────
// Two base cases because the Fibonacci definition uses the two immediately
// preceding values: fib(0)=0, fib(1)=1.
//
// INEFFICIENCY NOTE (notes 20.3): fibonacci(n-1) and fibonacci(n-2) each
// redundantly recompute overlapping sub-problems. fibonacci(5) alone creates
// 15 recursive calls. For n=20 it's ~6765 calls; for n=40 it would be ~300M.
// The lesson: recursion is a TOOL, not always the right one for efficiency.
// A loop or memoisation would be O(n) instead of O(2^n).
int fibonacci(int n)
{
if (n == 0) return 0; // BASE CASE 1
if (n == 1) return 1; // BASE CASE 2
return fibonacci(n - 1) + fibonacci(n - 2); // RECURSIVE CASE
}
// ─── MOVEMENT 2: FUNCTION POINTERS ──────────────────────────────────────────
// Free functions (provided — unchanged from starter) ─────────────────────────
// These are PLAIN functions: no captures, concrete signatures, addressable.
// A pointer-to-function can point to any of them because they all match the
// `int(int, int)` signature captured by the `BinaryIntOp` alias.
int addOp(int a, int b) { return a + b; }
int subOp(int a, int b) { return a - b; }
int mulOp(int a, int b) { return a * b; }
// ─── TASK 4: apply ───────────────────────────────────────────────────────────
// The canonical "callback" pattern (notes 20.1): the algorithm is fixed (call
// op with a and b), but the BEHAVIOR is variable (which function op points to).
//
// Null check first: calling through a null function pointer is UNDEFINED
// BEHAVIOR (notes 20.1) — the program would crash in a deeply confusing way.
// Return a safe sentinel instead.
//
// Calling through a function pointer reads identically to calling a function
// directly: op(a, b) dereferences the pointer and invokes the pointed-to
// function. The explicit form (*op)(a, b) means the same thing.
int apply(int a, int b, BinaryIntOp op)
{
if (!op) // guard against null — never call a null function pointer
return 0;
return op(a, b); // call through the pointer: dispatch to addOp/subOp/mulOp
}
// ─── TASK 5: pickOp ──────────────────────────────────────────────────────────
// A mini "jump table": map a character selector to the ADDRESS of the right
// operation function and RETURN that pointer. The caller gets back a
// `BinaryIntOp` (i.e., `int (*)(int, int)`) and passes it straight to apply().
//
// This two-step — pickOp(...) then apply(...) — decouples dispatch from
// execution, the same pattern as a dispatch table in a virtual machine or an
// LLVM pass pipeline.
BinaryIntOp pickOp(char c)
{
switch (c)
{
case '+': return &addOp; // return the ADDRESS of addOp
case '-': return &subOp;
case '*': return &mulOp;
default: return nullptr; // unknown operation -> null pointer
}
}
// ─── MOVEMENT 3: LAMBDAS + CAPTURES ─────────────────────────────────────────
// ─── TASK 6: countMatching ───────────────────────────────────────────────────
// The predicate parameter is `std::function<bool(int)>` — the flexible wrapper
// (notes 20.1, 20.6) that can store:
// • a plain function pointer (e.g. &isEven)
// • a NON-capturing lambda ([](int x){ return x > 0; })
// • a CAPTURING lambda ([threshold](int x){ return x >= threshold; })
//
// A RAW function pointer (`bool (*)(int)`) would NOT accept the capturing
// lambda because captures turn a lambda into a closure object, not a free
// function. std::function handles type erasure so we can write one parameter
// type that works for all three forms.
//
// CS6340 tie-in: std::find_if, std::count_if, and every LLVM traversal helper
// that takes a predicate follow this exact shape — you pass a small, local
// predicate that captures the context it needs.
int countMatching(const std::vector<int>& values,
std::function<bool(int)> predicate)
{
int count { 0 };
for (int v : values) // range-for over the vector (Chapter 16)
{
if (predicate(v)) // call through std::function — works for any callable
++count;
}
return count;
}
// ============================================================================
// tests/tests.cpp — automated grader for the calc-core library (Chapter 20)
// ----------------------------------------------------------------------------
// Tiny no-framework harness (same style as drills/CLAUDE.md spec).
// The Makefile compiles this against starter/calccore.cpp for `make test`
// and against solution/calccore.cpp for `make test-solution`.
//
// Strategy: every test targets a known property, and at least one check in
// each group is an EDGE CASE designed to catch a common wrong answer:
// - factorial base case (n=0) and first recursive step (n=1)
// - sumDigitsRecursive: zero, single digit, negative input
// - fibonacci: both base cases (0 and 1), modest depth (n=10)
// - apply: null pointer guard (must NOT crash), correct dispatch
// - pickOp: all three known characters and an unknown one
// - countMatching: empty vector, all-match, no-match, and — critically —
// a CAPTURING lambda that captures a local `threshold` variable (the test
// would FAIL to compile if countMatching only accepted a raw fn pointer)
// ============================================================================
#include <iostream>
#include <vector>
#include "../calccore.h"
static int fails = 0;
#define CHECK(cond) \
do { if (!(cond)) { \
std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; \
++fails; \
} } while(0)
// ── A plain (non-capturing) predicate function — for testing that countMatching
// also accepts a raw function-pointer-shaped callable (notes 20.1).
static bool isEven(int x) { return x % 2 == 0; }
int main()
{
// ─────────────────────────────────────────────────────────────────────────
// TASK 1 — factorial
// ─────────────────────────────────────────────────────────────────────────
// Base case: 0! == 1 (not 0!)
CHECK(factorial(0) == 1); // EDGE: base case
CHECK(factorial(1) == 1); // first recursive step (1 * 0!)
CHECK(factorial(2) == 2);
CHECK(factorial(3) == 6);
CHECK(factorial(5) == 120);
CHECK(factorial(10) == 3628800LL);
// Ensure long long range — 20! fits; 13! would overflow int
CHECK(factorial(12) == 479001600LL);
CHECK(factorial(15) == 1307674368000LL);
// ─────────────────────────────────────────────────────────────────────────
// TASK 2 — sumDigitsRecursive
// (Same job as Chapter 8's sumOfDigits loop — same tests, different path.)
// ─────────────────────────────────────────────────────────────────────────
CHECK(sumDigitsRecursive(0) == 0); // EDGE: base case, n < 10 -> return 0
CHECK(sumDigitsRecursive(7) == 7); // EDGE: single digit, direct return
CHECK(sumDigitsRecursive(10) == 1); // 1 + 0
CHECK(sumDigitsRecursive(99) == 18);
CHECK(sumDigitsRecursive(123) == 6); // 1 + 2 + 3
CHECK(sumDigitsRecursive(1000) == 1); // zeros contribute nothing
CHECK(sumDigitsRecursive(-7) == 7); // EDGE: negative single digit -> |n|
CHECK(sumDigitsRecursive(-123) == 6); // EDGE: negative -> same as +123
// ─────────────────────────────────────────────────────────────────────────
// TASK 3 — fibonacci
// ─────────────────────────────────────────────────────────────────────────
CHECK(fibonacci(0) == 0); // EDGE: base case 1
CHECK(fibonacci(1) == 1); // EDGE: base case 2
CHECK(fibonacci(2) == 1); // first real recursive case
CHECK(fibonacci(3) == 2);
CHECK(fibonacci(4) == 3);
CHECK(fibonacci(5) == 5);
CHECK(fibonacci(6) == 8);
CHECK(fibonacci(7) == 13);
CHECK(fibonacci(10) == 55);
CHECK(fibonacci(15) == 610);
CHECK(fibonacci(20) == 6765); // largest safe test for naive recursive fib
// ─────────────────────────────────────────────────────────────────────────
// TASK 4 — apply
// ─────────────────────────────────────────────────────────────────────────
CHECK(apply(10, 3, &addOp) == 13); // dispatch to addOp
CHECK(apply(10, 3, &subOp) == 7); // dispatch to subOp
CHECK(apply(10, 3, &mulOp) == 30); // dispatch to mulOp
CHECK(apply(0, 0, &addOp) == 0); // EDGE: zero arguments
CHECK(apply(5, 5, &subOp) == 0); // EDGE: subtraction -> zero result
CHECK(apply(7, 0, &mulOp) == 0); // EDGE: multiply by zero
// Null-pointer guard: must NOT crash or produce nonsense; returns 0
CHECK(apply(10, 3, nullptr) == 0); // EDGE: null guard
// ─────────────────────────────────────────────────────────────────────────
// TASK 5 — pickOp
// ─────────────────────────────────────────────────────────────────────────
// pickOp gives back a pointer; apply exercises that pointer
CHECK(apply(6, 2, pickOp('+')) == 8); // '+' -> addOp
CHECK(apply(6, 2, pickOp('-')) == 4); // '-' -> subOp
CHECK(apply(6, 2, pickOp('*')) == 12); // '*' -> mulOp
// pickOp on an unknown character must return nullptr
CHECK(pickOp('?') == nullptr); // EDGE: unknown op
CHECK(pickOp('\0') == nullptr); // EDGE: null char
// apply(null) must be safe (tested above, but let's chain the full path)
CHECK(apply(6, 2, pickOp('/')) == 0); // '/' unknown -> nullptr -> safe sentinel
// ─────────────────────────────────────────────────────────────────────────
// TASK 6 — countMatching
// ─────────────────────────────────────────────────────────────────────────
const std::vector<int> nums { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// --- plain function pointer (non-capturing) ---
// std::function must accept a plain fn pointer, not just lambdas
CHECK(countMatching(nums, &isEven) == 5); // evens: 2,4,6,8,10
// --- non-capturing lambda ---
CHECK(countMatching(nums, [](int x){ return x > 5; }) == 5); // 6,7,8,9,10
CHECK(countMatching(nums, [](int x){ return x == 1; }) == 1); // exactly one
CHECK(countMatching(nums, [](int x){ return x < 0; }) == 0); // EDGE: no matches
// --- CAPTURING lambda (must capture a local variable) ---
// This test is the HEART of Task 6: `threshold` is a local int; the lambda
// captures it by value. A raw `bool (*)(int)` parameter would FAIL to
// accept this — only `std::function` (or a template) can hold it.
{
int threshold { 7 };
CHECK(countMatching(nums,
[threshold](int x){ return x >= threshold; }) == 4); // 7,8,9,10
}
{
int threshold { 3 };
CHECK(countMatching(nums,
[threshold](int x){ return x < threshold; }) == 2); // 1, 2
}
{
int threshold { 1 };
CHECK(countMatching(nums,
[threshold](int x){ return x == threshold; }) == 1); // only 1
}
// --- edge cases for countMatching ---
// Empty vector -> always 0
CHECK(countMatching({}, [](int x){ return x > 0; }) == 0); // EDGE: empty
// All elements match
CHECK(countMatching({ 2, 4, 6 }, &isEven) == 3); // all match
// Single element
CHECK(countMatching({ 5 }, [](int x){ return x == 5; }) == 1);
// ─────────────────────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all calc-core checks passed.\n";
else
std::cerr << "\nFAIL \xE2\x9D\x8C " << fails
<< " check(s) failed — fix the TASK blocks in starter/calccore.cpp.\n";
return fails ? 1 : 0;
}
// ============================================================================
// calccore.h — PUBLIC INTERFACE of the calc-core library (Chapter 20)
// ----------------------------------------------------------------------------
// This header is COMPLETE and PROVIDED. Do not edit it. Read it closely —
// it is a contract: every .cpp and the tests include this exact file.
// Change a signature and nothing links.
//
// WHAT THIS CHAPTER IS ABOUT — "functions as values + functions that call
// themselves":
//
// 1) RECURSION — a function that solves a problem by calling itself on a
// smaller version of the same problem (notes 20.3).
// Key anatomy: BASE CASE (stops the calls) + RECURSIVE
// CASE (shrinks toward the base).
//
// 2) FUNCTION POINTERS — store the ADDRESS of a compiled function so you can
// pass behavior as a parameter (notes 20.1). A raw function
// pointer can point to any ordinary (non-capturing) function
// whose signature matches.
//
// Syntax cheat-sheet (keep this in mind as you read below):
//
// int (*name)(int, int) pointer to int(int,int)
// ^--- return type
// ^--- this * must be grouped with the name!
// ^--- parameter types
// (reading pattern: "name is a pointer
// to a function taking two ints,
// returning int")
//
// Using `int* name(int,int)` instead means "function
// returning int*" — a completely different thing!
//
// 3) LAMBDAS + CAPTURES — unnamed function-like objects written inline (20.6,
// 20.7). A capturing lambda copies/references nearby
// variables and behaves like a small object. Store one
// callable slot that accepts BOTH a lambda and a plain
// function using `std::function` (notes 20.1, 20.6).
//
// CS6340 / LLVM tie-in: LLVM passes are full of "visit each instruction,
// apply this predicate, call this callback." Function pointers, lambdas, and
// `std::function` are the modern C++ way to wire those predicates in. You are
// building that muscle here.
//
// Header guard (Chapter 2 recap): stops the file from being pasted in twice
// per translation unit when more than one file includes it.
// ============================================================================
#ifndef CALCCORE_H
#define CALCCORE_H
#include <functional> // std::function (notes 20.1)
#include <vector> // std::vector (Chapter 16)
// ─── MOVEMENT 1: RECURSION ──────────────────────────────────────────────────
// ─── TASK 1 ─────────────────────────────────────────────────────────────────
// factorial(n) — compute n! using RECURSION.
//
// Base case : factorial(0) == 1 (by mathematical convention)
// Recursive case: factorial(n) == n * factorial(n-1) for n > 0
//
// Precondition: n >= 0. n==0 MUST be your base case — check it first.
// Return type is long long so results up to factorial(20) don't overflow.
long long factorial(int n);
// ─── TASK 2 ─────────────────────────────────────────────────────────────────
// sumDigitsRecursive(n) — sum the decimal digits of n using RECURSION.
//
// Contrast: Chapter 8 introduced sumOfDigits with a LOOP (notes 8 / engine.h
// Task 3). Here you do the same job with recursion so you can feel the
// difference:
//
// Base case : single digit (n < 10) — the digit IS the answer.
// Recursive case: last digit (n % 10) + sumDigitsRecursive(n / 10)
//
// Work on the magnitude so negative n matches its positive twin:
// sumDigitsRecursive(-123) == 6 (same as sumDigitsRecursive(123)).
// sumDigitsRecursive(0) == 0.
int sumDigitsRecursive(int n);
// ─── TASK 3 ─────────────────────────────────────────────────────────────────
// fibonacci(n) — return the nth Fibonacci number using RECURSION.
//
// fibonacci(0) == 0
// fibonacci(1) == 1
// fibonacci(n) == fibonacci(n-1) + fibonacci(n-2) for n >= 2
//
// Note: the notes (20.3) point out that naive recursive Fibonacci is
// INEFFICIENT because it recomputes sub-problems (exponential calls for large
// n). That's intentional here — witnessing the call-tree growth teaches you
// *why* we mention memoization and iterative alternatives. Keep n small in
// tests (n <= 20 is safe for this naive version).
//
// Precondition: n >= 0.
int fibonacci(int n);
// ─── MOVEMENT 2: FUNCTION POINTERS ──────────────────────────────────────────
// A type alias (Chapter 10's `using`) that names the "int binary operator"
// signature so we don't have to spell out `int (*)(int, int)` everywhere.
// (notes 20.1 — "Type aliases make this readable")
using BinaryIntOp = int (*)(int, int);
// Three free functions — addOp, subOp, mulOp — implemented in calccore.cpp.
// They exist so a caller can take their ADDRESS and store it in a BinaryIntOp.
int addOp(int a, int b); // return a + b
int subOp(int a, int b); // return a - b
int mulOp(int a, int b); // return a * b
// ─── TASK 4 ─────────────────────────────────────────────────────────────────
// apply(a, b, op) — call the function pointed-to by `op` with arguments a, b.
//
// This is the archetypal "callback" pattern (notes 20.1 — "Passing behavior
// into a function"): one function receives BEHAVIOR (an op) as a parameter and
// delegates to it without knowing which specific operation was passed.
//
// `op` has type `BinaryIntOp` (== `int (*)(int, int)`). Check for null before
// calling: if op is nullptr, return 0 as a safe sentinel.
int apply(int a, int b, BinaryIntOp op);
// ─── TASK 5 ─────────────────────────────────────────────────────────────────
// pickOp(c) — RETURN a function pointer based on a character selector.
//
// This is a tiny "jump table": a char goes in, a POINTER TO THE RIGHT FUNCTION
// comes out.
//
// '+' -> &addOp
// '-' -> &subOp
// '*' -> &mulOp
// anything else -> nullptr (unknown operation)
//
// Return type is `BinaryIntOp` (i.e., `int (*)(int, int)`). The caller then
// passes the returned pointer into `apply` — a two-step dispatch.
BinaryIntOp pickOp(char c);
// ─── MOVEMENT 3: LAMBDAS + CAPTURES ─────────────────────────────────────────
// ─── TASK 6 ─────────────────────────────────────────────────────────────────
// countMatching(values, predicate) — count how many elements of `values`
// satisfy `predicate`.
//
// The predicate parameter is `std::function<bool(int)>` — the flexible callable
// wrapper (notes 20.1, 20.6) that accepts:
// • a plain function pointer (e.g. &isEven)
// • a non-capturing lambda ([](int x){ return x > 0; })
// • a CAPTURING lambda ([threshold](int x){ return x >= threshold; })
//
// Using std::function here is the correct choice for a parameter that must
// accept ALL of those forms — including capturing lambdas, which a raw
// function pointer CANNOT hold (notes 20.1 tradeoff table).
//
// Implement with a range-for loop over `values`, calling predicate(v) for each
// element and incrementing a counter when it returns true. Return the count.
int countMatching(const std::vector<int>& values,
std::function<bool(int)> predicate);
#endif // CALCCORE_H
// ============================================================================
// demo.cpp — ungraded demonstration driver (Chapter 20)
// ----------------------------------------------------------------------------
// This file is NOT graded. It exists to show the library in action and to
// demonstrate Chapter 20 features that don't belong in the pure calc-core API:
// • CLI arguments (notes 20.4) — the user can pass a number on the command
// line and see recursive computations on it.
// • Lambda captures observed live — a local `threshold` captured into a
// lambda passed to countMatching.
// • The full pickOp -> apply dispatch chain end-to-end.
//
// Build and run with `make run`. You do NOT need to edit this file.
// ============================================================================
#include <iostream>
#include <string>
#include <vector>
#include "calccore.h"
// A plain (non-capturing) predicate — demonstrates that countMatching accepts
// both a raw function pointer and a lambda (notes 20.1, 20.6).
static bool isEven(int x) { return x % 2 == 0; }
int main(int argc, char* argv[])
{
// ── CLI argument parsing (notes 20.4) ────────────────────────────────────
// Parse an optional integer from argv[1]; default to 7 if not provided.
// The notes 20.4 CLI example guards std::stoi with try/catch because stoi
// throws on non-numeric text. try/catch is a PREVIEW here — exceptions are
// formally Chapter 27. It lives only in this ungraded demo (never in the
// graded library), so you are not asked to write it yourself.
int n { 7 }; // default demo value
if (argc >= 2)
{
try // (a preview — formally Chapter 27)
{
n = std::stoi(argv[1]);
}
catch (...)
{
std::cerr << "usage: calc-demo [integer]\n";
return 1;
}
}
std::cout << "=== calc-core demo (n = " << n << ") ===\n\n";
// ── MOVEMENT 1: Recursion ────────────────────────────────────────────────
std::cout << "--- Recursion ---\n";
if (n >= 0 && n <= 20)
std::cout << "factorial(" << n << ") = " << factorial(n) << "\n";
else
std::cout << "(factorial demo skipped: keep n in 0..20)\n";
std::cout << "sumDigitsRecursive(" << n << ") = " << sumDigitsRecursive(n) << "\n";
if (n >= 0 && n <= 20)
std::cout << "fibonacci(" << n << ") = " << fibonacci(n) << "\n";
else
std::cout << "(fibonacci demo skipped: keep n in 0..20)\n";
std::cout << "\n";
// ── MOVEMENT 2: Function Pointers ────────────────────────────────────────
std::cout << "--- Function Pointers ---\n";
int a { n }, b { n / 2 + 1 };
// Apply uses a function POINTER passed as a parameter.
std::cout << "apply(" << a << ", " << b << ", +) = " << apply(a, b, &addOp) << "\n";
std::cout << "apply(" << a << ", " << b << ", -) = " << apply(a, b, &subOp) << "\n";
std::cout << "apply(" << a << ", " << b << ", *) = " << apply(a, b, &mulOp) << "\n";
// pickOp returns a function pointer; pass it straight to apply.
for (char op : { '+', '-', '*', '?' })
{
BinaryIntOp fn { pickOp(op) };
if (fn)
std::cout << "pickOp('" << op << "') -> apply(" << a << "," << b << ") = "
<< apply(a, b, fn) << "\n";
else
std::cout << "pickOp('" << op << "') -> nullptr (unknown op)\n";
}
std::cout << "\n";
// ── MOVEMENT 3: Lambdas + Captures ───────────────────────────────────────
std::cout << "--- Lambdas + Captures ---\n";
std::vector<int> values { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Plain function pointer as predicate
std::cout << "countMatching(1..10, isEven) = " << countMatching(values, &isEven) << "\n";
// Capturing lambda: `threshold` is a LOCAL variable captured by value.
// (notes 20.7) — the lambda stores its own copy of `threshold`.
int threshold { (n > 0 && n <= 10) ? n : 5 };
std::cout << "threshold = " << threshold << "\n";
std::cout << "countMatching(1..10, x >= " << threshold << ") = "
<< countMatching(values, [threshold](int x){ return x >= threshold; })
<< "\n";
return 0;
}
# Chapter 20 — Functions II (Recursion, Function Pointers, Lambdas)
# Project: calc-core · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. Recipes use TABS.
#
# Layout:
# calccore.h — API declarations (provided, complete)
# starter/calccore.cpp — LEARNER fills in the TODO blocks
# solution/calccore.cpp — reference implementation
# demo.cpp — ungraded CLI demo (shows recursion, fn ptrs, lambdas live)
# tests/tests.cpp — automated grader; links against starter or solution
#
# -I. puts the chapter root on the include path so every file can write
# #include "calccore.h" by its simple name.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── Starter build: compile-check the learner's calccore.cpp and build demo ───
# Two targets: an object-file compile-check (catches warnings fast) and the
# full demo executable so `make run` works.
build: starter/demo
starter/demo: demo.cpp calccore.h starter/calccore.cpp
$(CXX) $(CXXFLAGS) demo.cpp starter/calccore.cpp -o starter/demo
@echo "OK \xE2\x9C\x85 starter/calccore.cpp compiles. Run: make test (or: make run to see the demo)"
# ── Run: demo with the learner's implementation ───────────────────────────────
run: build
./starter/demo
# ── Test: grade the LEARNER's starter code ───────────────────────────────────
# RED until the TASK blocks are filled in; GREEN once they are.
test: tests/tests.cpp calccore.h starter/calccore.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp starter/calccore.cpp -o tests/run
@./tests/run || echo "FAIL \xE2\x9D\x8C fill in the TASK blocks in starter/calccore.cpp until every check passes."
# ── Solution: build + run the reference demo ─────────────────────────────────
solution: solution/demo
./solution/demo
solution/demo: demo.cpp calccore.h solution/calccore.cpp
$(CXX) $(CXXFLAGS) demo.cpp solution/calccore.cpp -o solution/demo
# ── test-solution: proof the exercise is solvable (MUST be green) ────────────
test-solution: tests/tests.cpp calccore.h solution/calccore.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp solution/calccore.cpp -o tests/run
@./tests/run
clean:
rm -f starter/demo solution/demo tests/run
rm -rf starter/demo.dSYM solution/demo.dSYM tests/run.dSYM
make test locally
(see “Build & run locally” above).