Chapter 15 — More on Classes: IdCard Badge Printer
You are building an IdCard class — a simple employee-badge system — but the
right way for a real codebase: the class declaration lives in idcard.h
(provided, complete, do not edit), and you implement every member body in
starter/idcard.cpp using the ClassName::method() scope-resolution syntax.
Along the way, five Chapter-15 concepts become physical rather than abstract:
- The
.h/.cppmember split (notes 15.2) — the layout every production C++ project uses; exactly how LLVM headers likellvm/IR/Value.hare organised. thisused explicitly in the setters — the idiomaticthis->m_x = x;form from notes 15.1 (a clarity convention here; see Task 3 for when it is mandatory).- Method chaining —
setOwner/setTitlereturnIdCard&so calls can chain:card.setOwner("Ada").setTitle("Eng").setRole(IdCard::Role::Engineer); - Static members —
s_liveCountands_nextSerialtrack counts and auto-issue serial numbers class-wide, not per-object (notes 15.6, 15.7). - Destructors (notes 15.4) — the destructor decrements
s_liveCount; tests create cards in nested{ }scopes so you can see the count fall exactly when each scope closes. No heap, nonew— pure automatic-storage RAII.
Your tasks
Constructor — serial assignment + live count (notes 15.2, 15.6). Initialise
m_ownerandm_titlewith a member-initialiser list. In the body, assigns_nextSerialtom_serial, then increments_nextSerial(so the next card gets a different number), and increments_liveCount. Use theIdCard::IdCard(...)out-of-class syntax. Do not repeat the default arguments from the header.Destructor — decrement the live count (notes 15.4). Write
~IdCard()in out-of-class form. The only job:--s_liveCount. Tests create cards in nested{ }scopes and verify the count falls as each scope closes — destructors made physical withoutnewordelete.Setters with explicit
this+ method chaining (notes 15.1). ImplementsetOwner,setTitle, andsetRole. Writethis->m_xxx = xxx;— the explicit-thisidiom from notes 15.1 (use these exact parameter names:owner,title,role). Because members here arem_-prefixed, the parameterownerand memberm_ownerare different names, sothis->is a clarity convention, not a requirement — it only becomes mandatory when a parameter has the same name as a member. Return*thisby reference (IdCard&) so callers can chain. The grader takes the address of the returned reference and confirms it equals&card— the proof that no copy was made.Const accessors (Ch 14 — reused). Implement
owner(),title(),role(), andserial()— eachconst, returning the matching private member. They are called on aconst IdCard&in the tests, soconston the function signature is required.Static member functions (notes 15.7). Implement
liveCount()andnextSerial(). They have nothispointer and may only read the static memberss_liveCountands_nextSerial. Remember:staticappears in the declaration (in.h) only — do not write it in the definition.
Success criteria
liveCount() == 0before any object exists — static init working.serial() == 1on the first card;serial() == 2on the second — auto-issue.- After a
{ }block closes,liveCount()falls by exactly the number of cards that lived in that block — destructor +--s_liveCountworking. &returned == &card—setOwnerreturns a reference to the same object, not a copy (method chaining identity test).- Chained
card.setOwner("Alan Turing").setTitle("Cryptanalyst").setRole(…)leaves the card with all three fields correctly set. - Deep three-level nesting: count goes 0 → 1 → 2 → 3 → 2 → 1 → 0 deterministically.
Concepts practiced
- Out-of-class member definitions with
ClassName::method()syntax (notes 15.2) thispointer — explicitthis->member = param;idiom (notes 15.1)- Returning
*thisby reference for method chaining (notes 15.1) - Static member variables shared across all instances (notes 15.6)
- Static member functions —
liveCount()/nextSerial(), nothis(notes 15.7) - Destructor —
~IdCard()for deterministic cleanup / bookkeeping (notes 15.4) - Nested type —
IdCard::Roleenum class lives inside the class (notes 15.3) - Default arguments declared once in the header, absent from the
.cpp(notes 15.2) - Reused from earlier: constructors / member-init lists (Ch 14),
constmember functions (Ch 14),std::string/std::string_view(Ch 5),enum class(Ch 13), header guards (Ch 2)
Constraints
Allowed: class, member-init lists, const member functions, std::string,
std::string_view, enum class (Ch 13), static members and static member
functions, this, *this, header guards, the full Chapter ≤ 14 toolkit.
Forbidden (not taught yet): operator<< overloading (Ch 21), virtual /
override (Ch 25), new/delete (Ch 19), std::vector (Ch 16). No static
keyword in out-of-class definitions (it belongs only in the declaration).
Required idioms:
- Every out-of-class definition uses
ClassName::method(...)scope resolution. - Default arguments appear in the header declaration only (notes 15.2).
setOwner/setTitle/setRolemust returnIdCard&(reference, not a copy).- Use
this->m_owner = owner;style in the setters — the explicit-thisidiom from notes 15.1. (Here it is a clarity convention, sincem_ownerandownerdiffer; it is required only when a parameter and member share a name.) staticomitted from the.cppdefinitions ofliveCount()/nextSerial().
Build & run locally
make # compile-check starter/idcard.cpp (warning-clean)
make test # grade your code -> RED until the TASK blocks are filled in
make solution # run the grader against the reference (see what GREEN looks like)
make test-solution # same as solution but an explicit green-proof step
make clean # remove build artifactsHints
Task 1 — constructor syntax and static bookkeeping
Out-of-class constructor syntax:
IdCard::IdCard(std::string_view owner, std::string_view title)
: m_owner { owner }, m_title { title } // member-init list
{
m_serial = s_nextSerial; // claim this card's number
++s_nextSerial; // advance the dispenser
++s_liveCount; // one more live card
}s_nextSerial and s_liveCount are static members — they belong to the class,
not to *this, so you access them by name with no prefix (or IdCard::s_…).
Task 2 — destructor shape
IdCard::~IdCard()
{
--s_liveCount;
}No return type, no parameters. The ~ prefix is the entire signature. C++ calls
this automatically when the object's lifetime ends — you never call it directly.
Task 3 — explicit `this` and returning *this
IdCard& IdCard::setOwner(std::string_view owner)
{
this->m_owner = owner; // explicit-this idiom (m_owner != owner, so optional)
return *this; // *this is the object; return by reference (IdCard&)
}this is a pointer (IdCard*). *this dereferences it to the object itself.
Returning by reference means the caller gets back the same object — not a copy.
Without the & in the return type, a copy would be made and chaining would
operate on a temporary, never reaching the original card.
Task 4 — const accessors
std::string_view IdCard::owner() const { return m_owner; }
std::string_view IdCard::title() const { return m_title; }
IdCard::Role IdCard::role() const { return m_role; }
int IdCard::serial() const { return m_serial; }The const after () matches the header declaration and allows calling these on
a const IdCard&. Outside the class, note that Role must be qualified:
IdCard::Role — it is a nested type (notes 15.3).
Task 5 — static member functions (no `this`)
int IdCard::liveCount() { return s_liveCount; }
int IdCard::nextSerial() { return s_nextSerial; }Two things to remember: (a) do not write static in the definition — it
belongs only in the declaration; (b) there is no implicit object, so only static
members are accessible by name.
Stretch goals
- Add a
static void resetSerials()that resetss_nextSerialto 1 — useful for test isolation. (Stays within Ch 15 scope; calls the chapter's static-function lesson from the other direction.) - Add a
friend std::ostream& operator<<(std::ostream&, const IdCard&)that prints"[123] Ada Lovelace — Chief Analyst (Engineer)"(notes 15.8 — a preview;operator<<is formally Chapter 21). - Make
IdCardnon-copyable by= deleteing the copy constructor and copy assignment operator (Ch 14 / 22) — prevents accidentals_liveCountmis-counting when a card is copied. - Store all live cards in a
static std::vector<IdCard*>registry and add a staticprintAll()that lists every live card — a preview ofstd::vector(Ch 16) and the LLVM-style "context owns all Values" pattern (Ch 25).
// Chapter 15 — More on Classes · Project: IdCard Badge Printer (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Your job: fill in the five TASK blocks below. Each maps 1:1 to a task in
// README.md and to a declaration in ../idcard.h.
//
// The bodies currently return PLACEHOLDERS so the file compiles out of the box —
// that's why `make test` is RED right now. Turn it GREEN.
//
// make build compile-check your code (should already work)
// make test grade it (RED until you fill the TASKs)
// make test-solution see what green looks like (uses the reference)
//
// SYNTAX reminder — out-of-class member definitions use the scope-resolution
// operator (::) to tell the compiler which class each body belongs to:
//
// ReturnType ClassName::methodName(params) { … }
//
// That :: prefix is the whole point of notes 15.2. The compiler now links this
// definition to the declaration it found in idcard.h.
//
// STYLE RULE from notes 15.2: default arguments are declared ONCE in the header.
// Do NOT repeat them in any definition below (the compiler will reject it).
// ─────────────────────────────────────────────────────────────────────────────
#include "../idcard.h" // the class declaration + includes for std::string etc.
#include <string>
#include <string_view>
// ─── TASK 1: Constructor ─────────────────────────────────────────────────────
// Initialise m_owner and m_title from the parameters using a MEMBER-INITIALISER
// LIST (the `:` syntax from Chapter 14). Then, inside the body:
// • Assign the current value of s_nextSerial to m_serial.
// • Increment s_nextSerial so the NEXT card gets a different number.
// • Increment s_liveCount by 1 (this card is now alive).
//
// Note: m_role already has a default (Role::Other) set in the class definition —
// you do NOT need to initialise it here unless you want to change it.
//
// Syntax hint: IdCard::IdCard(std::string_view owner, std::string_view title)
// : m_owner { owner }, m_title { title }
// { … }
//
// >>> YOUR CODE HERE <<<
//
IdCard::IdCard(std::string_view /*owner*/, std::string_view /*title*/)
{
// placeholder — s_liveCount is NOT incremented, serials NOT assigned.
// Touch m_serial and m_role so the compiler doesn't warn about unused fields.
m_serial = 0;
m_role = Role::Other;
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: Destructor ──────────────────────────────────────────────────────
// The destructor runs AUTOMATICALLY when an IdCard object's lifetime ends —
// either when it goes out of scope, or when execution returns from the block
// that holds it. The tests create cards in nested { } scopes and verify that
// s_liveCount falls exactly as objects are destroyed (notes 15.4).
//
// Your only job: decrement s_liveCount by 1.
//
// >>> YOUR CODE HERE <<<
//
IdCard::~IdCard()
{
// placeholder — s_liveCount is NOT decremented
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: setOwner and setTitle — explicit `this` + method chaining ───────
// Each setter:
// 1. Writes `this->m_xxx = xxx;` — the explicit-`this` form (notes 15.1).
// A clarification, NOT a contradiction: because this class prefixes members
// with `m_`, the parameter `owner` and the member `m_owner` are DIFFERENT
// names, so plain `m_owner = owner;` is already unambiguous — `this->` is
// never strictly required here. We still write `this->m_owner = owner;` to
// make the intent explicit ("the member of THIS object") and to practice the
// exact idiom notes 15.1 demonstrates with `Person::setName`. (The one time
// `this->` becomes mandatory is when a parameter has the SAME name as a
// member — e.g. a parameter literally named `m_owner`, or a member without
// the `m_` prefix. We avoid that here on purpose.)
// 2. Returns `*this` (the object itself, by reference) so calls can be chained:
// card.setOwner("Ada").setTitle("Engineer")
// `this` is a POINTER; `*this` dereferences it to get the object.
// The return type IdCard& (reference!) ensures no copy is made.
//
// Please use these EXACT parameter names in your definitions (they are what the
// hints and the notes-15.1 idiom use):
// setOwner(std::string_view owner) — pairs with member m_owner
// setTitle(std::string_view title) — pairs with member m_title
// setRole(IdCard::Role role) — pairs with member m_role
//
// >>> YOUR CODE HERE <<<
//
IdCard& IdCard::setOwner(std::string_view /*owner*/)
{
return *this; // placeholder — does NOT update m_owner
}
IdCard& IdCard::setTitle(std::string_view /*title*/)
{
return *this; // placeholder — does NOT update m_title
}
IdCard& IdCard::setRole(IdCard::Role /*role*/)
{
return *this; // placeholder — does NOT update m_role
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: Const accessors ─────────────────────────────────────────────────
// Simple read-only getters. Each must be marked `const` (matching the declaration
// in the header) so it can be called on a const IdCard object.
// Return the matching private member value.
//
// >>> YOUR CODE HERE <<<
//
std::string_view IdCard::owner() const
{
return {}; // placeholder — returns empty string_view
}
std::string_view IdCard::title() const
{
return {}; // placeholder — returns empty string_view
}
IdCard::Role IdCard::role() const
{
return IdCard::Role::Other; // placeholder — always Other
}
int IdCard::serial() const
{
return 0; // placeholder — always returns 0 (wrong; serials are 1-based)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: Static member functions ─────────────────────────────────────────
// Static member functions belong to the CLASS, not to any particular object.
// They have NO `this` pointer (notes 15.7), so they can only access:
// • static data members of the class (s_liveCount, s_nextSerial), and
// • local variables / parameters.
// They CANNOT access non-static members (m_owner, m_title, …) without an object.
//
// liveCount() — return the current value of s_liveCount.
// nextSerial() — return the current value of s_nextSerial (the value the NEXT
// constructed card will receive — useful for tests that predict it).
//
// Syntax: the `static` keyword appears in the DECLARATION (in .h) only.
// Do NOT write `static` in the DEFINITION (in .cpp). This is the same rule as
// for virtual functions in later chapters (a preview — formally Chapter 25).
//
// >>> YOUR CODE HERE <<<
//
int IdCard::liveCount()
{
return 0; // placeholder — ignores s_liveCount
}
int IdCard::nextSerial()
{
return 0; // placeholder — ignores s_nextSerial
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// Chapter 15 — More on Classes · Project: IdCard Badge Printer (REFERENCE SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// One complete, correct, warning-clean implementation of ../idcard.h.
// Peek only after you've taken a real swing at starter/idcard.cpp — the
// learning is in writing the out-of-class bodies yourself.
//
// KEY PATTERNS this solution demonstrates (map to the notes' sections):
//
// notes 15.1 — explicit `this->member = param;` idiom (clarity; not required
// here because members are `m_`-prefixed — see setOwner below)
// notes 15.1 — return *this by reference for method chaining
// notes 15.2 — ClassName::method(...) syntax for out-of-class definitions
// notes 15.4 — destructor decrements a class-wide counter (RAII preview)
// notes 15.6 — static member variables s_liveCount, s_nextSerial
// notes 15.7 — static member functions liveCount(), nextSerial() — no `this`
//
// CS6340 note: LLVM's Pass infrastructure uses a near-identical pattern for
// counting registered passes and issuing unique IDs — static counters + inline
// static class-level storage. See llvm/IR/PassInstrumentation.h for the spirit.
// ─────────────────────────────────────────────────────────────────────────────
#include "../idcard.h"
#include <string>
#include <string_view>
// ─── TASK 1: Constructor (notes 15.2 — out-of-class definition) ──────────────
// We initialise m_owner and m_title from the parameters in the member-initialiser
// list (the `: m_owner{owner}, m_title{title}` part). The body then:
// • Claims the next serial number (read THEN increment s_nextSerial).
// • Increments s_liveCount so the object is counted as alive immediately.
//
// NOTE: default arguments ("Unnamed", "Untitled") are declared ONCE in the
// header — they MUST NOT appear again here. This is notes 15.2's rule.
IdCard::IdCard(std::string_view owner, std::string_view title)
: m_owner { owner } // initialise member from parameter (Chapter 14)
, m_title { title }
{
// Claim this card's unique serial: read the class-wide counter, then advance.
// s_nextSerial is a static member — it belongs to the class, not this object.
m_serial = s_nextSerial; // the value THIS card gets
++s_nextSerial; // the NEXT card will get a different number
// Register that one more IdCard is alive. The destructor undoes this.
++s_liveCount;
}
// ─── TASK 2: Destructor (notes 15.4 — RAII / deterministic cleanup) ──────────
// The destructor name is ~ClassName. No return type, no parameters.
//
// When an IdCard goes out of scope (or is otherwise destroyed), C++ calls this
// automatically — deterministically, at a known program point, not "eventually".
// That determinism is what makes the tests' nested-scope trick reliable: create
// cards inside a { } block, leave the block, and s_liveCount falls predictably.
//
// In a real class, the destructor might close a file handle or release a lock.
// Here it simply bookkeeps — but the RAII lesson is the same.
IdCard::~IdCard()
{
--s_liveCount; // one fewer live IdCard in the program
}
// ─── TASK 3a: setOwner (notes 15.1 — explicit `this`, return *this) ──────────
// The parameter `owner` pairs with the private member m_owner. Because of the
// `m_` prefix they are DIFFERENT names, so `m_owner = owner` is already
// unambiguous — the compiler resolves m_owner as the member with no help needed.
// We still write `this->m_owner = owner` to make the intent explicit: "I mean the
// member of THIS object." `this->` would be strictly REQUIRED only if a parameter
// shared a member's exact name. The notes' Person::setName shows this idiom (15.1).
//
// Returning `*this` by reference (IdCard&) means the caller gets back a reference
// to the same object, not a copy — so the next chained call operates on the same
// card.
IdCard& IdCard::setOwner(std::string_view owner)
{
this->m_owner = owner; // explicit-`this` idiom (optional here: m_owner != owner)
return *this; // return the object itself, by reference
}
// ─── TASK 3b: setTitle (same pattern as setOwner) ────────────────────────────
IdCard& IdCard::setTitle(std::string_view title)
{
this->m_title = title;
return *this;
}
// ─── TASK 3c: setRole (same chaining idiom) ──────────────────────────────────
// Note the fully-qualified parameter type: IdCard::Role. Outside the class body,
// `Role` alone is not in scope — we must write `IdCard::Role`.
IdCard& IdCard::setRole(IdCard::Role role)
{
this->m_role = role;
return *this;
}
// ─── TASK 4: Const accessors ─────────────────────────────────────────────────
// Plain read-only getters. The `const` qualifier on the function (after the `()`)
// matches the declaration in the header and tells the compiler this function does
// not modify the object — so it can be called on const IdCard objects.
//
// std::string_view is non-owning: it points into the underlying std::string's
// buffer. That is safe as long as the IdCard outlives the caller's use of the view
// — fine here, since tests read the view before modifying the card again.
std::string_view IdCard::owner() const { return m_owner; }
std::string_view IdCard::title() const { return m_title; }
IdCard::Role IdCard::role() const { return m_role; }
int IdCard::serial() const { return m_serial; }
// ─── TASK 5a: liveCount (notes 15.7 — static member function) ────────────────
// Static member functions have NO implicit object — no `this` pointer. They can
// only touch static members (s_liveCount, s_nextSerial) or things passed in.
//
// IMPORTANT: `static` appears in the DECLARATION (in .h), NOT in the definition
// here. Writing `static int IdCard::liveCount()` is a compiler error. (The same
// rule will apply to `virtual` — a preview, formally Chapters 24–25 — which also
// goes only on the in-class declaration, never on the out-of-class definition.)
int IdCard::liveCount()
{
return s_liveCount; // class-wide; not tied to any one object
}
// ─── TASK 5b: nextSerial ─────────────────────────────────────────────────────
// Lets tests predict what serial the next-constructed card will get, enabling
// precise assertions without hard-coding absolute serial values.
int IdCard::nextSerial()
{
return s_nextSerial;
}
// Chapter 15 — More on Classes · Project: IdCard Badge Printer (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// Tiny no-framework unit-test harness matching the drills/CLAUDE.md spec.
// Includes ../idcard.h (the contract) and calls the API across many inputs.
// The Makefile links this against starter/idcard.cpp (your code) for `make test`
// and against solution/idcard.cpp for `make test-solution`.
//
// Each CHECK that fails prints its expression and source line. Any failure causes
// a non-zero exit, so `make test` turns RED until every check passes.
//
// WHAT IS BEING TESTED
//
// Task 1 — Constructor: serial auto-assignment, s_liveCount increments.
// Task 2 — Destructor: s_liveCount decrements when objects leave scope.
// The key trick: we create cards in nested { } scopes and verify
// the count falls at the exact point the scope closes. No heap,
// no `new` — just automatic variables with deterministic lifetimes
// (notes 15.4: "scope-based destruction").
// Task 3 — setOwner/setTitle/setRole: correct mutation + chaining identity.
// The chaining test takes the ADDRESS of the returned reference and
// verifies it equals the address of the original card — same object,
// not a copy (notes 15.1: "return *this by reference").
// Task 4 — Const accessors: values roundtrip correctly; called on const refs.
// Task 5 — liveCount() / nextSerial(): static functions, no object needed.
// ─────────────────────────────────────────────────────────────────────────────
#include <iostream>
#include "../idcard.h"
static int fails = 0;
// CHECK: assert a condition; on failure, report expression + line number.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ──────────────────────────────────────────────────────────────────────────
// Task 5 sanity: static functions work without any object existing.
// s_liveCount starts at 0; s_nextSerial starts at 1.
// ──────────────────────────────────────────────────────────────────────────
CHECK(IdCard::liveCount() == 0); // no objects yet
CHECK(IdCard::nextSerial() == 1); // first card will be serial 1
// ──────────────────────────────────────────────────────────────────────────
// Task 1 + 5: constructor increments live count; serial is auto-assigned.
// ──────────────────────────────────────────────────────────────────────────
{
IdCard a { "Ada Lovelace", "Chief Analyst" };
CHECK(IdCard::liveCount() == 1); // one card alive
CHECK(IdCard::nextSerial() == 2); // next card will be serial 2
CHECK(a.serial() == 1); // first card gets serial 1
{
IdCard b { "Charles Babbage", "Engine Designer" };
CHECK(IdCard::liveCount() == 2);
CHECK(IdCard::nextSerial() == 3);
CHECK(b.serial() == 2); // second card gets serial 2
// ── Task 2: destructor test — nested scope ────────────────────────
// When b leaves THIS inner scope its destructor runs, decrementing
// s_liveCount. After the closing brace, a is still alive (count = 1).
}
CHECK(IdCard::liveCount() == 1); // b is gone; only a survives
// Third card inside a's scope: gets serial 3.
{
IdCard c { "Grace Hopper", "Compiler Pioneer" };
CHECK(c.serial() == 3);
CHECK(IdCard::liveCount() == 2);
}
CHECK(IdCard::liveCount() == 1); // c gone, a still alive
}
CHECK(IdCard::liveCount() == 0); // a gone — count back to zero
// ──────────────────────────────────────────────────────────────────────────
// Task 4: default-argument constructor ("Unnamed" / "Untitled").
// ──────────────────────────────────────────────────────────────────────────
{
IdCard def {}; // uses both defaults
CHECK(def.owner() == "Unnamed");
CHECK(def.title() == "Untitled");
CHECK(def.role() == IdCard::Role::Other);
}
CHECK(IdCard::liveCount() == 0);
// ──────────────────────────────────────────────────────────────────────────
// Task 4: const accessor roundtrip — called on a const reference.
// ──────────────────────────────────────────────────────────────────────────
{
IdCard card { "Linus Torvalds", "Kernel Hacker" };
const IdCard& cref { card }; // const reference — only const methods OK
CHECK(cref.owner() == "Linus Torvalds");
CHECK(cref.title() == "Kernel Hacker");
CHECK(cref.role() == IdCard::Role::Other);
CHECK(cref.serial() >= 1); // some valid serial was assigned
}
// ──────────────────────────────────────────────────────────────────────────
// Task 3: setOwner / setTitle — mutation correctness.
// ──────────────────────────────────────────────────────────────────────────
{
IdCard card { "Original Name", "Original Title" };
card.setOwner("Updated Name");
card.setTitle("Updated Title");
CHECK(card.owner() == "Updated Name");
CHECK(card.title() == "Updated Title");
card.setRole(IdCard::Role::Engineer);
CHECK(card.role() == IdCard::Role::Engineer);
card.setRole(IdCard::Role::Manager);
CHECK(card.role() == IdCard::Role::Manager);
}
// ──────────────────────────────────────────────────────────────────────────
// Task 3: method chaining — the chained calls must mutate the SAME object.
//
// This is the CRITICAL chaining test: we capture the address of the object
// returned by setOwner and verify it equals &card. If the method returned a
// COPY instead of a reference, the addresses would differ and this check fails.
// Then we chain all three setters and confirm the final state is correct.
// ──────────────────────────────────────────────────────────────────────────
{
IdCard card { "Wrong Name", "Wrong Title" };
// Verify setOwner returns a reference to the SAME object (not a copy).
IdCard& returned = card.setOwner("Right Name");
CHECK(&returned == &card); // same address = same object
// Full chained call: all three setters in one expression.
card.setOwner("Alan Turing")
.setTitle("Cryptanalyst")
.setRole(IdCard::Role::Engineer);
CHECK(card.owner() == "Alan Turing");
CHECK(card.title() == "Cryptanalyst");
CHECK(card.role() == IdCard::Role::Engineer);
}
// ──────────────────────────────────────────────────────────────────────────
// Task 3 + 1: chaining from construction — set fields right after creation.
// ──────────────────────────────────────────────────────────────────────────
{
int serialBefore = IdCard::nextSerial();
IdCard card { "Temp", "Temp" };
card.setOwner("Margaret Hamilton").setTitle("Software Engineer");
CHECK(card.owner() == "Margaret Hamilton");
CHECK(card.title() == "Software Engineer");
CHECK(card.serial() == serialBefore); // serial assigned at construction
}
// ──────────────────────────────────────────────────────────────────────────
// Task 5: static functions accessible without an object.
// ──────────────────────────────────────────────────────────────────────────
{
int countBefore = IdCard::liveCount();
int serialBefore = IdCard::nextSerial();
{
IdCard x { "X", "X" };
IdCard y { "Y", "Y" };
CHECK(IdCard::liveCount() == countBefore + 2);
CHECK(IdCard::nextSerial() == serialBefore + 2);
CHECK(x.serial() == serialBefore);
CHECK(y.serial() == serialBefore + 1);
}
CHECK(IdCard::liveCount() == countBefore); // both x and y destroyed
CHECK(IdCard::nextSerial() == serialBefore + 2); // serial dispenser never resets
}
// ──────────────────────────────────────────────────────────────────────────
// Edge case: deeply nested scopes — count tracks every level.
// ──────────────────────────────────────────────────────────────────────────
{
CHECK(IdCard::liveCount() == 0);
{
IdCard a { "A", "A" };
CHECK(IdCard::liveCount() == 1);
{
IdCard b { "B", "B" };
CHECK(IdCard::liveCount() == 2);
{
IdCard c { "C", "C" };
CHECK(IdCard::liveCount() == 3);
} // c destroyed here
CHECK(IdCard::liveCount() == 2);
} // b destroyed here
CHECK(IdCard::liveCount() == 1);
} // a destroyed here
CHECK(IdCard::liveCount() == 0);
}
// ──────────────────────────────────────────────────────────────────────────
// Edge case: nested type is scoped — IdCard::Role::Intern, not just Role::Intern.
// ──────────────────────────────────────────────────────────────────────────
{
IdCard intern { "Sam", "Intern" };
intern.setRole(IdCard::Role::Intern);
CHECK(intern.role() == IdCard::Role::Intern);
intern.setRole(IdCard::Role::Other);
CHECK(intern.role() == IdCard::Role::Other);
}
// ──────────────────────────────────────────────────────────────────────────
// Final liveCount must be 0 — all scopes above are closed.
// ──────────────────────────────────────────────────────────────────────────
CHECK(IdCard::liveCount() == 0);
// ── Result ────────────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS ✅ all badge-printer checks passed.\n";
else
std::cerr << "\nFAIL ❌ " << fails
<< " check(s) failed — fix the TASK blocks in starter/idcard.cpp.\n";
return fails ? 1 : 0;
}
// Chapter 15 — More on Classes · Project: IdCard Badge Printer
// ─────────────────────────────────────────────────────────────────────────────
// This header is the CONTRACT between the learner, the grader, and the solution.
// DO NOT EDIT THIS FILE. tests/tests.cpp includes it; both starter/idcard.cpp
// and solution/idcard.cpp include it. Change a signature and nothing links.
//
// THE BIG PICTURE OF THIS LAB
// ────────────────────────────
// A real software system often splits a class into TWO files:
//
// idcard.h — the CLASS DEFINITION: member declarations, access specifiers,
// nested types, static member *declarations*. ONE canonical copy.
// idcard.cpp — the OUT-OF-CLASS DEFINITIONS of every non-trivial member body.
//
// That split (notes 15.2) is what you implement. The header you are reading right
// now is fully complete and compiles. Your job is to write the matching bodies in
// starter/idcard.cpp using the ClassName::method() syntax (notes 15.2).
//
// Along the way you will make five Chapter-15 concepts physical:
//
// (a) `this` – the explicit `this->m_member = param;` idiom (15.1).
// (b) Method chaining – setOwner/setTitle return IdCard& so calls can chain.
// (c) Static members – s_liveCount tracks live IdCard objects class-wide;
// s_nextSerial auto-assigns unique serial numbers.
// (d) Destructor – decrements s_liveCount; tests use nested { } scopes so
// you can OBSERVE the count fall deterministically.
// (e) Nested type – Role enum class lives inside IdCard (notes 15.3).
//
// CS6340 tie-in: LLVM's class hierarchy (llvm::Value, llvm::Instruction, …) uses
// exactly this layout — declarations in .h, definitions in .cpp, static helpers
// for global counts/tables, and explicit `this->` in template methods. Reading
// this header and then writing the .cpp mirrors how you'll navigate LLVM source.
//
// Header guard (notes 15.2 / Chapter 2): prevents double-inclusion in one TU.
#ifndef IDCARD_H
#define IDCARD_H
#include <string> // std::string (Chapter 5)
#include <string_view> // std::string_view (Chapter 5)
// ═════════════════════════════════════════════════════════════════════════════
// class IdCard
// ═════════════════════════════════════════════════════════════════════════════
// An employee ID card that tracks a name, a title, a role category, and a
// unique serial number. The class also counts how many IdCard objects are
// currently alive (s_liveCount) and auto-issues serial numbers (s_nextSerial).
//
// Key class-mechanics at a glance (see the notes' "Class mechanics map"):
//
// IdCard(std::string_view owner, std::string_view title); // ctor
// ~IdCard(); // dtor — Chapter 15.4
//
// IdCard& setOwner(std::string_view owner); // returns *this — Chapter 15.1
// IdCard& setTitle(std::string_view title); // returns *this — Chapter 15.1
//
// std::string_view owner() const; // const accessor
// std::string_view title() const; // const accessor
// Role role() const; // const accessor
// int serial() const; // const accessor
//
// IdCard& setRole(Role r); // role setter — returns *this
//
// static int liveCount(); // class-level counter — Chapter 15.7
// static int nextSerial(); // peek at the next serial to be issued
// ─────────────────────────────────────────────────────────────────────────────
class IdCard
{
public:
// ── Nested type (notes 15.3) ──────────────────────────────────────────────
// Role lives *inside* IdCard because it is meaningless outside the badge
// system. Callers use IdCard::Role::Engineer, not a bare Role. (LLVM uses
// the same pattern for e.g. llvm::Instruction::OtherOps.)
enum class Role
{
Engineer,
Manager,
Intern,
Other,
};
// ── Constructor ───────────────────────────────────────────────────────────
// Initialises the card, increments s_liveCount, and claims the next serial.
// Default arguments (notes 15.2): declared once HERE in the header only —
// do NOT repeat them in the .cpp definition.
explicit IdCard(std::string_view owner = "Unnamed",
std::string_view title = "Untitled");
// ── Destructor (notes 15.4) ───────────────────────────────────────────────
// Called automatically when the object's lifetime ends (scope exit, etc.).
// Decrements s_liveCount so you can OBSERVE the count fall in tests.
~IdCard();
// ── Setters — each returns *this by reference (notes 15.1) ───────────────
// Returning IdCard& (reference, NOT a copy!) enables chaining:
// card.setOwner("Ada").setTitle("Eng").setRole(IdCard::Role::Engineer);
// Task: write the explicit-`this` idiom `this->m_member = param;` (notes 15.1).
// Here the `m_` prefix means `m_owner` and the parameter `owner` are different
// names, so `this->` is a clarity convention; it is strictly REQUIRED only when
// a parameter and a member share the same name.
IdCard& setOwner(std::string_view owner);
IdCard& setTitle(std::string_view title);
IdCard& setRole(Role role);
// ── Const accessors ───────────────────────────────────────────────────────
std::string_view owner() const;
std::string_view title() const;
Role role() const;
int serial() const;
// ── Static member functions (notes 15.7) ─────────────────────────────────
// Static functions belong to the CLASS, not to any object — no `this`.
// Call as IdCard::liveCount(), not card.liveCount() (though both compile).
static int liveCount(); // how many IdCard objects currently exist
static int nextSerial(); // value the NEXT constructed card will receive
private:
// ── Instance (per-object) data ────────────────────────────────────────────
std::string m_owner {}; // who holds this card
std::string m_title {}; // their job title
Role m_role { Role::Other };
int m_serial {}; // unique ID assigned at construction time
// ── Static (class-wide) data (notes 15.6) ────────────────────────────────
// `inline static` lets us define + initialise these right in the header
// without a separate out-of-class definition line in a .cpp (C++17+).
// Both live once in the program, shared by every IdCard object.
inline static int s_liveCount { 0 }; // count of live IdCard objects
inline static int s_nextSerial { 1 }; // next serial to hand out (1-based)
};
#endif // IDCARD_H
# Chapter 15 — More on Classes · IdCard Badge Printer · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# Layout:
# idcard.h — the class declaration + nested types (DO NOT EDIT)
# starter/idcard.cpp — learner fills in the 5 TASK blocks
# solution/idcard.cpp — reference implementation
# tests/tests.cpp — the grader (includes ../idcard.h; links the .cpp)
#
# -I. puts the chapter root on the include path so every file can write
# #include "idcard.h" (or "../idcard.h" from subdirs — both work).
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test solution test-solution clean
all: build
# build — compile-check the LEARNER's starter code (warning-clean object, then
# build a tiny demo so the learner can run their code without waiting for tests).
build:
$(CXX) $(CXXFLAGS) -c starter/idcard.cpp -o starter/idcard.o
@echo "OK ✅ starter/idcard.cpp compiles. Now run: make test"
# run — not a graded target; build passes so there is nothing interesting to run
# without main(). Use `make test` to exercise the code.
run: build
@echo "(No standalone driver for this exercise — try: make test)"
# test — grade the LEARNER's code: link the grader against starter/idcard.cpp.
# RED until the TASK blocks are filled in; GREEN once they're correct.
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/idcard.cpp -o tests/run
@./tests/run
# solution — build and run a quick demo using the REFERENCE implementation.
# (Since there is no main.cpp driver, we run the test suite against the solution.)
solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/idcard.cpp -o solution/run
@./solution/run
# test-solution — proof the lab is solvable: the reference MUST pass every check.
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/idcard.cpp -o tests/run
@./tests/run
clean:
rm -f starter/idcard.o tests/run solution/run
rm -rf tests/run.dSYM solution/run.dSYM
make test locally
(see “Build & run locally” above).