Alias Workshop
In every chapter before this one, every function received a copy of its argument: changes to the parameter stayed inside the function, invisible to the caller. Chapter 12 changes that. References and pointers let two names refer to the same memory — and that aliasing is physical, not just a naming trick.
You will build the Alias Workshop: six small functions that collectively cover
every major reference/pointer pattern in the chapter. Each function either accepts
a reference or pointer (so it can modify the caller's variable) or returns one
(so the caller can modify the original through the return value). The automated
grader does not just check return values — it checks that the original
caller-side variables actually changed. It also compares raw memory addresses
(&maxOf(a, b) == &b) to prove aliasing is physical.
This matters immediately for CS6340: LLVM pass signatures like
bool runOnFunction(Function &F) and Module *M { F.getParent() } are
impossible to read fluently until references and pointers click.
Your tasks
swapByRef(int& a, int& b)— swap through non-const lvalue references. Exchange the values of two ints using a temporary local.aandbare references — writing to them writes to the caller's variables. No return value; the side-effect IS the output. (notes 12.3, 12.5)swapByPtr(int* a, int* b)— swap through pointers, null-safe. Same three-step swap, but the caller passes addresses instead of references. The pointer-specific addition: if either pointer isnullptr, do nothing — return immediately. Otherwise dereference with*aand*bto reach the ints and swap. (notes 12.7, 12.8, 12.10)maxOf(int& a, int& b) → int&— return a modifiable reference. Return a reference to the larger of the two parameters. The returned reference aliases whichever ofaorbis larger — the grader writes through it and checks that the original changed. TRAP: do not create a local variable and return a reference to it — that is a dangling reference (UB). It is safe to return a reference to one of the parameters because they alias the caller's objects and outlive this call. (notes 12.12)describePointer(const int* ptr) → std::string_view— inspect a pointer. Return"null"ifptr == nullptr, otherwise return"value". The parameter isconst int*— you are not allowed to modify the int through it. Check for null before doing anything else. (notes 12.8, 12.9)addBonusInPlace(int& score, int bonus)— textbook in/out parameter. Addbonustoscorein place by writing through thescorereference. The caller's variable changes; there is no return value. (notes 12.13)findFirst(std::string_view text, char target) → std::optional<int>— optional search result. Searchtextfor the first occurrence oftargetand return its 0-based index asstd::optional<int>. Returnstd::nulloptif not found. Use aforloop and cast the index tostd::size_twhen indexing (text.length()returnsstd::size_t).std::optionalis covered in notes 12.15 — the note covers it fully, so it is in scope. (notes 12.15)
Success criteria
swapByRef: checks the originals changed (not just that the function ran)swapByPtr: null pointer on either side must NOT crash; both-null must NOT crashmaxOf: address comparison&maxOf(a,b) == &bproves the reference aliases the correct variable (not a copy with the same value); write-through confirms the original changesmaxOf: equal values — the returned reference must alias one of the two original variables, not a third objectdescribePointer(nullptr)must return"null"— the stub always returns"value"describePointer(&zero)where zero== 0must return"value"(zero is a valid int, not a null pointer)addBonusInPlacewith negative bonus — verifies decrement works (not just +=0)findFirston an empty string — must returnnulloptwithout crashingfindFirstwith repeated characters — must return the first index (0, not later)
Concepts practiced
- Lvalue references (
int& r) — another name for an existing object; cannot be null, cannot be reseated (notes 12.3) - Non-const references as in/out parameters — the caller's variable is modified in-place through the reference (notes 12.5, 12.13)
- Pass by address (
int*) — pass the object's address so the function can modify the pointed-to value (notes 12.10) - Null pointer safety (
nullptr) — always check a pointer before dereferencing; useif (!ptr) return;as the guard (notes 12.8) const int*— pointer to const — read-only pointer; the function cannot modify the object through it (notes 12.9)- Return by reference — return an alias so the caller can write through it; the returned reference must alias something that outlives the function (notes 12.12)
- Dangling reference trap — never return a reference to a local variable; it dies when the function returns, leaving a dangling alias (notes 12.3, 12.12)
std::optional<T>— express "maybe no value" as part of the type, without a magic sentinel (notes 12.15)- Reused from earlier chapters:
std::string_viewfor read-only string parameters (Ch 5),static_cast<std::size_t>at sign-conversion boundaries (Ch 10), header guard and header/impl split (Ch 2),static_asserton types (Ch 7)
Constraints
Allowed:
int&,const int&— lvalue references (Ch 12)int*,const int*— pointers (Ch 12)nullptr— null pointer constant (Ch 12)*ptr— dereference operator (Ch 12)&variable— address-of operator (Ch 12)std::optional<T>,std::nullopt,.has_value(),*opt(Ch 12)std::string_viewfor read-only string parameters (Ch 5)static_cast<std::size_t>for sign-conversion (Ch 10)if, localintvariables,forloops — all from earlier chapters
Forbidden (not taught yet or explicitly out-of-scope):
new/delete/new[]/delete[]— dynamic allocation (Ch 19)std::vector,std::array— containers (Ch 16, 17)- Pointer arithmetic (
ptr + 1,ptr++) — Ch 17 - Smart pointers (
std::unique_ptr,std::shared_ptr) — Ch 22 ->member access through pointer — you don't need it here (formally Ch 13+)
Required idioms:
- Always null-check a pointer before dereferencing it (notes 12.8)
- Never return a reference to a local variable (notes 12.12 — the chapter's number-one trap)
- Use
const int*for read-only pointer parameters (notes 12.9) - Prefer
nullptrover0orNULLfor null pointers (notes 12.8)
Build & run locally
make # compile-check starter/aliases.cpp (already warning-clean)
make test # grade your code -> RED until the TASK blocks are filled in
make solution # run the grader against the reference implementation
make clean # remove build artifactsmake test is the grader — it calls your functions across many inputs, including
every edge case listed in the success criteria below.
Hints
Task 1 — three-step swap with references
void swapByRef(int& a, int& b)
{
int tmp { a }; // save a's current value
a = b; // a = b's old value (writes through the reference)
b = tmp; // b = a's old value (writes through the reference)
}tmp is a plain local int — just a value holder. a and b are references,
so writing to them writes to the caller's variables. This is the whole point of
pass-by-reference: the side-effect escapes the function.
Task 2 — null guard + dereference
void swapByPtr(int* a, int* b)
{
if (!a || !b) // either null -> do nothing
return;
int tmp { *a }; // dereference to get the value
*a = *b;
*b = tmp;
}!a is shorthand for a == nullptr. Checking BOTH before doing ANYTHING is the
safe pattern (notes 12.8). The dereference *a then reaches the int that a
points at — same three-step swap as Task 1, just spelled with *.
Task 3 — returning a reference (and avoiding the trap)
int& maxOf(int& a, int& b)
{
if (a >= b)
return a; // returns a reference to the caller's `a`
return b; // returns a reference to the caller's `b`
}The TRAP is this:
int& bad() {
int local { 5 };
return local; // UNDEFINED BEHAVIOR: local dies when the function returns
}local is destroyed at the closing brace; the returned reference dangles. The
safe pattern: return a reference to a PARAMETER that was passed in by reference —
the caller's object outlives the call (notes 12.12).
Task 4 — null check before anything else
std::string_view describePointer(const int* ptr)
{
if (ptr == nullptr)
return "null";
return "value";
}const int* — you cannot write *ptr = 5 here; the compiler forbids it. You CAN
check ptr == nullptr and you CAN read *ptr (if it's not null), but here you
don't even need to read the value — just its null-ness (notes 12.9).
Task 5 — in/out parameter with +=
void addBonusInPlace(int& score, int bonus)
{
score += bonus; // writes through the reference
}One line. score is an in/out reference: the function reads its current value
(the "in" side) and modifies it (the "out" side). bonus is passed by value — a
copy — because the function doesn't need to modify it. (notes 12.13)
Task 6 — linear search returning std::optional
std::optional<int> findFirst(std::string_view text, char target)
{
for (int i { 0 }; i < static_cast<int>(text.length()); ++i)
{
if (text[static_cast<std::size_t>(i)] == target)
return i; // wraps `i` in optional<int>
}
return std::nullopt; // "no value"
}return i implicitly constructs std::optional<int>{i}. return std::nullopt
produces the empty optional. The caller checks with if (result) or
.has_value() and accesses the value with *result (notes 12.15).
Stretch goals
- Write
maxByPtr(int*, int*) -> int*that mirrors Task 3 but with pointers and a nullptr return for the "both null" case — reinforcing the "must-exist → reference, may-be-absent → pointer" rule from notes 12.8. - Add a
clamp(int& val, int lo, int hi)in/out function (notes 12.13): clampvalto[lo, hi]in place. Tests writevaloutside the range and check it was corrected. - Extend
findFirstto returnstd::optional<std::string_view>of the matching substring (requiresstd::string_view::substr— preview of Ch 5 string ops — and shows that optional isn't just for index types). - Replace the
describePointerlabel with the actual value by dereferencing the non-null pointer:return std::to_string(*ptr)(requires<string>, which is≤ Ch 5— in scope). - Add
int* findFirstPtr(int*, std::size_t len, int target)that returns a pointer (notstd::optional) to the first match in a raw C-array of given length, ornullptrif not found. This is a preview of pointer-as-cursor (formally Ch 17 pointer arithmetic), so label it as such if you attempt it.
// Chapter 12 — Compound Types: References and Pointers · Alias Workshop (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the six TASK blocks below. Each maps 1:1 to a task in the README and
// to a declaration in ../aliases.h. The bodies currently return PLACEHOLDERS so
// the file compiles immediately — that's why `make test` is RED right now. Your
// job is to turn it GREEN by wiring up real references and pointer logic.
//
// make build compile your code (already works — start here)
// make test grade it (RED until you fill these in)
// make test-solution run grader on the reference if you get stuck
//
// KEY TERMS this file practices:
// lvalue reference (&) — another name for an existing object (notes 12.3)
// pointer (*) — a variable that stores an address (notes 12.7)
// nullptr — the "points to nothing" constant (notes 12.8)
// dereference (*) — reach the object through a pointer (notes 12.7)
// in/out parameter — a reference the function reads AND writes (notes 12.13)
// return by reference — return an alias so the caller can write through it (12.12)
// std::optional<T> — a value that may or may not be present (notes 12.15)
#include "../aliases.h"
// ─── TASK 1: swapByRef — swap via NON-CONST LVALUE REFERENCES ─────────────────
// Declare a temporary int to hold one value during the exchange, then copy the
// two values across using the REFERENCES (which aliases the caller's variables).
// Nothing is returned — the modification IS the output (in/out parameters).
//
// Hint: you need a local int to hold the intermediate value.
// int tmp { a }; // save a's current value
// a = b; // overwrite a with b's value
// b = tmp; // give b a's original value
//
// >>> YOUR CODE HERE <<<
//
void swapByRef(int& /*a*/, int& /*b*/)
{
// placeholder — does nothing (swap is a no-op)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: swapByPtr — swap via POINTERS, null-safe ─────────────────────────
// Same swap logic as Task 1, but the caller passes ADDRESSES (int*) instead of
// references. FIRST check that neither pointer is nullptr — if either is null,
// return immediately without touching anything. Then dereference with *a and *b
// to reach the ints, and swap just like Task 1 but writing through the pointers.
//
// Recall (notes 12.8): `if (!a || !b) return;` — short for nullptr check.
// Recall (notes 12.7): `*a = 5` writes 5 to the int that `a` points at.
//
// >>> YOUR CODE HERE <<<
//
void swapByPtr(int* /*a*/, int* /*b*/)
{
// placeholder — does nothing (null-safe no-op is actually correct for nullptr,
// but the non-null case is wrong)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: maxOf — RETURN BY REFERENCE ──────────────────────────────────────
// Return a REFERENCE to the larger of the two parameters. The returned reference
// aliases the original variable in the caller — modifying the return value
// changes the caller's variable.
//
// TRAP WARNING (notes 12.12): do NOT return a reference to a local variable!
// A local dies when the function returns, leaving a dangling reference (UB).
// This function is safe because `a` and `b` are THEMSELVES references that
// alias the caller's objects — returning one of them returns an alias that
// outlives the call.
//
// If a >= b, return a reference to `a`; otherwise return a reference to `b`.
//
// >>> YOUR CODE HERE <<<
//
int& maxOf(int& a, int& /*b*/)
{
return a; // placeholder — always returns a reference to 'a' (wrong when b > a)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: describePointer — read a CONST POINTER, label the state ──────────
// Inspect `ptr` (a pointer to const int). Check for nullptr first (notes 12.8).
// Return "null" if `ptr == nullptr`, otherwise return "value".
// Do NOT dereference a null pointer — that is undefined behavior.
//
// Returning a string literal as string_view is safe: string literals have
// static storage duration and never expire.
//
// >>> YOUR CODE HERE <<<
//
std::string_view describePointer(const int* /*ptr*/)
{
return "value"; // placeholder — never reports "null" (wrong for nullptr)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: addBonusInPlace — textbook IN/OUT PARAMETER ──────────────────────
// Add `bonus` to `score` in place. `score` is the non-const reference (in/out —
// the function both reads and writes it). `bonus` is passed by value (in only).
//
// CS6340 parallel: like incrementing a counter inside a pass without returning
// a new value — the reference IS the output channel.
//
// >>> YOUR CODE HERE <<<
//
void addBonusInPlace(int& /*score*/, int /*bonus*/)
{
// placeholder — does nothing (score unchanged)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: findFirst — std::optional SEARCH ─────────────────────────────────
// Walk `text` character by character with a `for` loop. Compare each character
// to `target`. If they match, return the 0-based index (as std::optional<int>).
// If the loop finishes without a match, return std::nullopt.
//
// Important: when indexing with an `int` index, cast to `std::size_t` to keep
// -Wall -Wextra warning-clean:
// text[static_cast<std::size_t>(i)]
//
// std::optional recap (notes 12.15):
// return i; // wraps the value in an optional
// return std::nullopt; // the "no value" sentinel
// if (result) // true if the optional has a value
// *result // access the value (dereference the optional)
//
// >>> YOUR CODE HERE <<<
//
std::optional<int> findFirst(std::string_view /*text*/, char /*target*/)
{
return std::nullopt; // placeholder — always reports "not found" (wrong when target is present)
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// Chapter 12 — Compound Types: References and Pointers · Alias Workshop (SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// One complete, correct, warning-clean implementation of ../aliases.h.
// Peek only after you've taken a real swing at starter/aliases.cpp — the deep
// understanding comes from wiring the aliasing yourself, then comparing.
//
// KEY INSIGHT running through every function here:
// Modifying a REFERENCE or DEREFERENCED POINTER modifies the ORIGINAL object.
// There is no copy. The tests prove this by checking the original variables —
// not via a return value but via the side effects on the caller's stack.
#include "../aliases.h"
// ─── TASK 1: swapByRef ────────────────────────────────────────────────────────
// The classic three-step swap: save one value in a local, copy across, restore.
// `a` and `b` are REFERENCES — writing to them writes directly to the caller's
// ints (notes 12.3: "a reference is another name for the object").
// The temporary `tmp` is a plain local int — just a value holder, not a ref.
//
// Why references here, not pointers? Because the API guarantees valid objects:
// "must exist -> reference" (notes 12.8 quick-reference table).
void swapByRef(int& a, int& b)
{
int tmp { a }; // save a's current value in a fresh local
a = b; // a now holds b's old value (writes through the reference)
b = tmp; // b now holds a's old value (writes through the reference)
// After this, the caller's two variables have been exchanged.
}
// ─── TASK 2: swapByPtr ────────────────────────────────────────────────────────
// Same three-step swap, but the caller passes ADDRESSES instead of references.
// The pointer-specific addition: a null-check guard at the top (notes 12.8).
//
// Why pointers here? The API documents that null IS a valid input. Passing
// nullptr is how the caller says "I don't have a second value to swap". Safe
// null handling is one of the key pointer disciplines in notes 12.10.
void swapByPtr(int* a, int* b)
{
// ── Null-safety guard (notes 12.8) ─────────────────────────────────────
// If EITHER pointer is null, there is no object to swap — do nothing.
// `!a` is shorthand for `a == nullptr` (both work; `!a` is idiomatic).
if (!a || !b)
return;
// ── Dereference to reach the pointed-to ints, then swap ────────────────
// `*a` is the int that `a` points at; `*b` is the int that `b` points at.
int tmp { *a }; // save *a
*a = *b; // overwrite the int a points at
*b = tmp; // restore to the int b points at
}
// ─── TASK 3: maxOf ────────────────────────────────────────────────────────────
// Return a REFERENCE to the larger of the two int parameters.
//
// This is return-by-reference (notes 12.12). The returned reference aliases
// either `a` or `b` — which are themselves references to the caller's variables.
// So the chain is: return value -> parameter reference -> caller's variable.
// Writing through the returned reference writes all the way back to the caller.
//
// SAFETY: `a` and `b` are references to objects the caller owns. Returning one
// of them returns an alias to an object that OUTLIVES this function call. This
// is the safe pattern from notes 12.12: "return a reference to a parameter".
//
// DANGER PATTERN (never do this):
// int& bad() { int local{5}; return local; } // local dies -> dangling ref!
int& maxOf(int& a, int& b)
{
// If a >= b, the max is a (or either for ties); otherwise the max is b.
// Return the reference, not the value — this is the whole point.
if (a >= b)
return a;
return b;
}
// ─── TASK 4: describePointer ──────────────────────────────────────────────────
// Inspect a pointer-to-const-int and categorize it.
//
// `const int*` means "pointer to const int" (notes 12.9): we cannot modify the
// int through this pointer, but we CAN change where the pointer points.
// "pointer to const" is the right choice for a read-only inspection function.
//
// Null-check FIRST — dereferencing nullptr is undefined behavior (notes 12.8).
// Here we don't even need to dereference: the fact of null vs. non-null is
// enough to produce the label.
std::string_view describePointer(const int* ptr)
{
if (ptr == nullptr)
return "null"; // pointer has no target
return "value"; // pointer has a target (the value exists)
}
// ─── TASK 5: addBonusInPlace ──────────────────────────────────────────────────
// The textbook IN/OUT parameter (notes 12.13):
// `score` — non-const reference: function reads it AND writes it.
// `bonus` — passed by value (in only): just a number to add.
//
// The += writes through the reference, changing the caller's variable directly.
// There is no return value; the modification IS the output.
//
// Notes 12.13 caution: prefer return values when practical (clearer data flow).
// In/out refs are appropriate when mutation is the central operation, exactly
// as in LLVM: `void rewriteInstruction(Instruction &I)`.
void addBonusInPlace(int& score, int bonus)
{
score += bonus; // writes through the reference — the caller's int changes
}
// ─── TASK 6: findFirst ────────────────────────────────────────────────────────
// A linear scan returning std::optional<int> (notes 12.15).
//
// std::optional<T> makes "no result" part of the type — the caller CANNOT
// ignore it (unlike a -1 sentinel where the caller must just know the convention).
//
// notes 12.15 explains the trade-off: optional is slightly more syntax but far
// clearer about the "maybe-empty" contract than magic sentinels.
//
// CS6340 tie-in (from the notes):
// std::optional<CoveragePoint> parseCoverageLine(std::string_view line);
// -> cleaner than "return {0,0}" to signal parse failure.
std::optional<int> findFirst(std::string_view text, char target)
{
// Walk every character. `i` is signed so we can return it as int cleanly.
for (int i { 0 }; i < static_cast<int>(text.length()); ++i)
{
if (text[static_cast<std::size_t>(i)] == target)
return i; // implicit construction: wraps the int in optional<int>
}
return std::nullopt; // the "no value" sentinel (notes 12.15)
}
// Chapter 12 — Compound Types: References and Pointers · Alias Workshop (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// A tiny no-framework unit-test harness (drills/CLAUDE.md Style B).
// Includes ../aliases.h and calls each function across many inputs.
//
// WHAT MAKES THESE TESTS MEANINGFUL:
// Every ref/pointer test does the physical check the chapter demands — it
// verifies that the ORIGINAL caller-side variable changed, not just a copy.
// The aliasing is PHYSICAL: we confirm it by inspecting the originals after
// each call, and for maxOf we compare addresses to prove the reference aliases
// the correct variable (not a copy with the same value).
//
// `make test` -> links against starter/aliases.cpp (RED on stub code)
// `make test-solution` -> links against solution/aliases.cpp (must be GREEN)
#include <iostream>
#include <string_view>
#include <optional>
#include "../aliases.h"
static int fails = 0;
// CHECK: evaluate cond; on failure, report the expression and line number.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── Task 1: swapByRef — aliasing proved by checking the ORIGINALS ─────────
{
int x { 10 };
int y { 20 };
swapByRef(x, y);
CHECK(x == 20); // x must have received y's old value
CHECK(y == 10); // y must have received x's old value
}
{
// Edge: both values equal — swap should leave both unchanged
int a { 5 };
int b { 5 };
swapByRef(a, b);
CHECK(a == 5);
CHECK(b == 5);
}
{
// Edge: negative values
int a { -3 };
int b { -7 };
swapByRef(a, b);
CHECK(a == -7);
CHECK(b == -3);
}
{
// Edge: zero involved
int a { 0 };
int b { 42 };
swapByRef(a, b);
CHECK(a == 42);
CHECK(b == 0);
}
// ── Task 2: swapByPtr — non-null case checks ORIGINALS; null is a no-op ───
{
int x { 100 };
int y { 200 };
swapByPtr(&x, &y);
CHECK(x == 200); // must have swapped through the pointers
CHECK(y == 100);
}
{
// Edge: first pointer is null -> do nothing
int y { 99 };
swapByPtr(nullptr, &y);
CHECK(y == 99); // y must be UNCHANGED
}
{
// Edge: second pointer is null -> do nothing
int x { 42 };
swapByPtr(&x, nullptr);
CHECK(x == 42); // x must be UNCHANGED
}
{
// Edge: BOTH pointers null -> do nothing (no crash)
swapByPtr(nullptr, nullptr); // must not crash
CHECK(true); // reaching here proves no UB/crash
}
{
// Edge: negative values through pointers
int a { -1 };
int b { -2 };
swapByPtr(&a, &b);
CHECK(a == -2);
CHECK(b == -1);
}
// ── Task 3: maxOf — return-by-reference, ALIASING proved by address + write-through
{
int a { 3 };
int b { 7 };
// Address test: the returned reference must alias `b` (the larger)
CHECK(&maxOf(a, b) == &b); // physical aliasing check
// Write-through test: modifying the return changes THE ORIGINAL
maxOf(a, b) = 100;
CHECK(b == 100); // b changed — not a copy
CHECK(a == 3); // a is untouched
}
{
int a { 9 };
int b { 2 };
CHECK(&maxOf(a, b) == &a); // now a is larger
maxOf(a, b) = 55;
CHECK(a == 55); // a changed through the reference
CHECK(b == 2); // b untouched
}
{
// Edge: equal values — must return a reference to one of them (we allow `a`)
int a { 5 };
int b { 5 };
int& m { maxOf(a, b) };
CHECK(&m == &a || &m == &b); // must alias one of the two — not a third object
m = 99;
// At least one of them changed; neither became a mystery third value.
CHECK(a == 99 || b == 99);
}
{
// Edge: negative values — max of negatives is the one closer to zero
int a { -3 };
int b { -1 };
CHECK(&maxOf(a, b) == &b); // -1 > -3, so b is the max
}
// ── Task 4: describePointer — null and value cases ─────────────────────────
{
CHECK(describePointer(nullptr) == std::string_view{"null"});
}
{
int val { 42 };
CHECK(describePointer(&val) == std::string_view{"value"});
}
{
// Edge: pointer to zero — value, not null (zero is a valid integer)
int zero { 0 };
CHECK(describePointer(&zero) == std::string_view{"value"});
}
{
// Edge: pointer to a negative value — still "value"
int neg { -7 };
CHECK(describePointer(&neg) == std::string_view{"value"});
}
{
// Edge: const int can be pointed at
const int cval { 10 };
CHECK(describePointer(&cval) == std::string_view{"value"});
}
// ── Task 5: addBonusInPlace — IN/OUT parameter, ORIGINAL checked ───────────
{
int score { 50 };
addBonusInPlace(score, 10);
CHECK(score == 60); // the original variable must reflect the change
}
{
// Edge: zero bonus — score unchanged
int score { 30 };
addBonusInPlace(score, 0);
CHECK(score == 30);
}
{
// Edge: negative bonus (penalty)
int score { 100 };
addBonusInPlace(score, -25);
CHECK(score == 75);
}
{
// Edge: bonus that drives score negative
int score { 5 };
addBonusInPlace(score, -20);
CHECK(score == -15);
}
{
// Chaining: two addBonusInPlace calls compound correctly
int score { 0 };
addBonusInPlace(score, 7);
addBonusInPlace(score, 3);
CHECK(score == 10);
}
// ── Task 6: findFirst — std::optional<int> search ──────────────────────────
{
// Basic find: first character
auto r { findFirst("hello", 'h') };
CHECK(r.has_value());
CHECK(*r == 0);
}
{
// Basic find: character in the middle
auto r { findFirst("hello", 'l') };
CHECK(r.has_value());
CHECK(*r == 2); // first 'l' is at index 2
}
{
// Basic find: last character
auto r { findFirst("hello", 'o') };
CHECK(r.has_value());
CHECK(*r == 4);
}
{
// Not found — must return nullopt
auto r { findFirst("hello", 'z') };
CHECK(!r.has_value());
}
{
// Edge: empty string — nothing to find
auto r { findFirst("", 'a') };
CHECK(!r.has_value());
}
{
// Edge: single-character string, found
auto r { findFirst("x", 'x') };
CHECK(r.has_value());
CHECK(*r == 0);
}
{
// Edge: single-character string, not found
auto r { findFirst("x", 'y') };
CHECK(!r.has_value());
}
{
// Edge: repeated characters — returns FIRST (index 0, not the later ones)
auto r { findFirst("aaa", 'a') };
CHECK(r.has_value());
CHECK(*r == 0);
}
{
// CS6340 flavor: search for a marker in a coverage-log prefix
auto r { findFirst("BB:42:func", ':') };
CHECK(r.has_value());
CHECK(*r == 2); // first ':' is at index 2
}
// ── Final report ────────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS \xe2\x9c\x85 all alias-workshop checks passed.\n";
else
std::cerr << "\nFAIL \xe2\x9d\x8c " << fails
<< " check(s) failed — fix the TASK blocks in starter/aliases.cpp.\n";
return fails ? 1 : 0;
}
// ============================================================================
// aliases.h — the PUBLIC INTERFACE of the "Alias Workshop" (Chapter 12)
// ----------------------------------------------------------------------------
// This header is COMPLETE and PROVIDED. Do NOT edit it.
//
// Chapter 12 is the chapter where C++ stops copying everything and lets two
// names refer to the SAME memory. This file is the proof: every function here
// either accepts a reference / pointer so it can modify the caller's variable,
// or RETURNS one so the caller can modify the original through the return value.
//
// HEADER GUARD (Chapter 2): the #ifndef / #define sandwich below stops the
// contents being pasted in twice if two translation units both #include it.
//
// CS6340 LLVM tie-in:
// bool runOnFunction(Function &F) <- pass-by-reference, non-const
// void instrumentCoverage(Instruction &I)
// Module *M { F.getParent() } <- may be null → pointer semantics
// Every signature in this file is a miniature of those LLVM patterns.
// ============================================================================
#ifndef ALIASES_H
#define ALIASES_H
#include <optional> // std::optional<T> (12.15)
#include <string_view> // std::string_view (Ch 5, reused here)
// ─── TASK 1: swapByRef ───────────────────────────────────────────────────────
// Swap the values of two ints through NON-CONST LVALUE REFERENCES (notes 12.3,
// 12.5). After the call, `a` holds b's old value and `b` holds a's old value.
//
// Design note: references are the right tool here — an object MUST exist (null
// is impossible). The caller's variables are modified in-place; no pointers
// needed. Compare this to swapByPtr below, where the caller may pass nullptr.
//
// CS6340 parallel: like `void Instrument::modifyInstruction(Instruction &I)` —
// the instruction must exist, so we take a reference, not a pointer.
void swapByRef(int& a, int& b);
// ─── TASK 2: swapByPtr ───────────────────────────────────────────────────────
// Swap the values pointed to by two int POINTERS (notes 12.7, 12.10). If EITHER
// pointer is nullptr, do NOTHING — this is the null-safety contract.
//
// Design note: pointers signal "null is a valid caller-supplied state" (notes
// 12.8: "may be absent → pointer"). Always null-check before dereferencing.
//
// CS6340 parallel: like `Module *M { F.getParent() }` — getParent may return
// nullptr if the function isn't attached to a module; check before using.
void swapByPtr(int* a, int* b);
// ─── TASK 3: maxOf ───────────────────────────────────────────────────────────
// Return a MODIFIABLE REFERENCE to the larger of two ints (notes 12.12).
// The reference aliases whichever of `a` or `b` is larger — the caller can
// assign through it to change the original:
//
// int x{3}, y{7};
// maxOf(x, y) = 100; // y (the max) is now 100
// assert(y == 100); // the ORIGINAL changed — aliasing is physical
//
// DANGER — THE CHAPTER'S NUMBER-ONE TRAP (notes 12.12):
// Never return a reference to a local variable.
// A local dies when the function returns; the reference would DANGLE.
// This function is safe because it returns a reference to one of its
// PARAMETERS, which are themselves references to the caller's variables
// (the aliased objects outlive this call).
//
// Equal values: return a reference to `a` (either is correct).
int& maxOf(int& a, int& b);
// ─── TASK 4: describePointer ─────────────────────────────────────────────────
// Inspect a POINTER TO CONST INT (notes 12.9) and return a string_view label:
// nullptr -> "null"
// anything -> "value" (the pointed-to value is some integer)
//
// The parameter is `const int*` — "pointer to const int" (notes 12.9). The
// function only reads, never writes through the pointer. You are NOT allowed to
// dereference a null pointer; check first (notes 12.8).
//
// Returning std::string_view of a string literal is safe — literals have
// static storage duration (they outlive everything).
std::string_view describePointer(const int* ptr);
// ─── TASK 5: addBonusInPlace ─────────────────────────────────────────────────
// A textbook IN/OUT PARAMETER (notes 12.13): `score` is read and modified
// (it is the in/out parameter), `bonus` is read only (it is the in parameter).
// Add `bonus` to `score` in place. The caller's variable changes.
//
// Notes 12.13 contrast: using a non-const reference signals "I will modify this
// value" — it documents the mutation at the call site. Compare:
// addBonusInPlace(score, 10) <-- `score` is an out param (visibly modified)
// score += 10 <-- same effect, but local, not via function
//
// CS6340 parallel: like `void normalize(Instruction &I)` — the instruction is
// read and rewritten in one pass.
void addBonusInPlace(int& score, int bonus);
// ─── TASK 6: findFirst (std::optional) ───────────────────────────────────────
// Search `text` for the first occurrence of `target` character and return its
// 0-based index as std::optional<int> (notes 12.15).
//
// Return std::nullopt if `target` does not appear.
// Return the 0-based index (an int) if it does.
//
// std::optional<T> makes "no result" EXPLICIT IN THE TYPE — the caller must
// handle the maybe-empty case (notes 12.15). The sentinel -1 approach forces
// the caller to remember the convention; optional enforces it.
//
// CS6340 tie-in (from notes 12.15):
// std::optional<CoveragePoint> parseCoverageLine(std::string_view line);
//
// Parameter is std::string_view: cheap, non-owning, read-only (notes 12.6).
// Cast index to std::size_t when indexing to satisfy -Wsign-conversion (Ch 10).
std::optional<int> findFirst(std::string_view text, char target);
#endif // ALIASES_H
# Chapter 12 — Compound Types: References and Pointers · Alias Workshop
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# Style B unit-test grader: the learner implements aliases.h in starter/aliases.cpp;
# tests/tests.cpp links against it for grading. The -I. flag lets every file
# #include "aliases.h" by its simple chapter-root-relative name.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── build: compile the LEARNER's starter (warning-clean object file, no link) ──
build:
$(CXX) $(CXXFLAGS) -c starter/aliases.cpp -o starter/aliases.o
@echo "OK \xe2\x9c\x85 starter/aliases.cpp compiles. Now run: make test"
# ── run: no interactive driver for this lab — point to test instead ─────────
run: build
@echo "No interactive driver for this lab. Use 'make test' to grade your code."
# ── test: grade the LEARNER's starter — RED on stub bodies, GREEN when done ──
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/aliases.cpp -o tests/run
@./tests/run || echo "FAIL \xe2\x9d\x8c fill in the TASK blocks in starter/aliases.cpp until every check passes."
# ── solution: build + run the REFERENCE solution ─────────────────────────────
solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/aliases.cpp -o tests/run
@./tests/run
# ── test-solution: proof the exercise is solvable — MUST be green ────────────
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/aliases.cpp -o tests/run
@./tests/run
clean:
rm -f starter/aliases.o tests/run
rm -rf tests/run.dSYM
make test locally
(see “Build & run locally” above).