Chapter 15 · More on Classes
Exercise · Chapter 15

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/.cpp member split (notes 15.2) — the layout every production C++ project uses; exactly how LLVM headers like llvm/IR/Value.h are organised.
  • this used explicitly in the setters — the idiomatic this->m_x = x; form from notes 15.1 (a clarity convention here; see Task 3 for when it is mandatory).
  • Method chaining — setOwner/setTitle return IdCard& so calls can chain: card.setOwner("Ada").setTitle("Eng").setRole(IdCard::Role::Engineer);
  • Static members — s_liveCount and s_nextSerial track 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, no new — pure automatic-storage RAII.

Your tasks

  1. Constructor — serial assignment + live count (notes 15.2, 15.6). Initialise m_owner and m_title with a member-initialiser list. In the body, assign s_nextSerial to m_serial, then increment s_nextSerial (so the next card gets a different number), and increment s_liveCount. Use the IdCard::IdCard(...) out-of-class syntax. Do not repeat the default arguments from the header.

  2. 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 without new or delete.

  3. Setters with explicit this + method chaining (notes 15.1). Implement setOwner, setTitle, and setRole. Write this->m_xxx = xxx; — the explicit-this idiom from notes 15.1 (use these exact parameter names: owner, title, role). Because members here are m_-prefixed, the parameter owner and member m_owner are different names, so this-> is a clarity convention, not a requirement — it only becomes mandatory when a parameter has the same name as a member. Return *this by 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.

  4. Const accessors (Ch 14 — reused). Implement owner(), title(), role(), and serial() — each const, returning the matching private member. They are called on a const IdCard& in the tests, so const on the function signature is required.

  5. Static member functions (notes 15.7). Implement liveCount() and nextSerial(). They have no this pointer and may only read the static members s_liveCount and s_nextSerial. Remember: static appears in the declaration (in .h) only — do not write it in the definition.

Success criteria

  • liveCount() == 0 before any object exists — static init working.
  • serial() == 1 on the first card; serial() == 2 on the second — auto-issue.
  • After a { } block closes, liveCount() falls by exactly the number of cards that lived in that block — destructor + --s_liveCount working.
  • &returned == &cardsetOwner returns 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)
  • this pointer — explicit this->member = param; idiom (notes 15.1)
  • Returning *this by reference for method chaining (notes 15.1)
  • Static member variables shared across all instances (notes 15.6)
  • Static member functionsliveCount() / nextSerial(), no this (notes 15.7)
  • Destructor~IdCard() for deterministic cleanup / bookkeeping (notes 15.4)
  • Nested typeIdCard::Role enum 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), const member 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/setRole must return IdCard& (reference, not a copy).
  • Use this->m_owner = owner; style in the setters — the explicit-this idiom from notes 15.1. (Here it is a clarity convention, since m_owner and owner differ; it is required only when a parameter and member share a name.)
  • static omitted from the .cpp definitions of liveCount()/nextSerial().
Build & run locally
shell
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 artifacts
Hints
Task 1 — constructor syntax and static bookkeeping

Out-of-class constructor syntax:

C++
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
C++
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
C++
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
C++
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`)
C++
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 resets s_nextSerial to 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 IdCard non-copyable by = deleteing the copy constructor and copy assignment operator (Ch 14 / 22) — prevents accidental s_liveCount mis-counting when a card is copied.
  • Store all live cards in a static std::vector<IdCard*> registry and add a static printAll() that lists every live card — a preview of std::vector (Ch 16) and the LLVM-style "context owns all Values" pattern (Ch 25).
starter/idcard.cpp C++
// 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
}
// ─────────────────────────────────────────────────────────────────────────────
Run
Submit
Run in your browser — coming soon For now: copy or download the files and use make test locally (see “Build & run locally” above).