Chapter 13 · Compound Types: Enums and Structs
Exercise · Chapter 13

Inventory Ledger

You are building the Inventory Ledger — a small library that tracks game-shop items by rarity tier. An item has a name, a Rarity (Common, Rare, or Epic), a stock quantity, and a base unit value. The library computes how much a full stock is worth, restocks shelves, and answers catalogue-identity queries.

The design enforces the two central lessons of Chapter 13:

  1. Scoped enumerations (enum class)Rarity is an enum class. Its enumerators live inside its own scope (Rarity::Common, not Common), it will not silently convert to int, and it switches cleanly on all three values. You will implement the enum-to-string and enum-to-multiplier helpers that every real codebase needs once it adopts scoped enums.

  2. Struct aggregates and their free-function APIItem is a data-only struct with four members. All the logic that operates on items lives in free functions (outside the struct), not as member functions. That boundary is deliberate: Chapter 14 introduces member functions; keeping Chapter 13 structs data-only makes the transition from "bag of data" to "object with behaviour" vivid. You will practice aggregate initialization, passing structs by const reference, returning them by value, and mutating through a non-const reference.

CS6340 connection: LLVM uses this exact pairing constantly — an enum like Instruction::BinaryOps or AtomicOrdering combined with free helper functions that map enumerators to strings, costs, or legality tables. The struct pattern appears in every value-carrying utility type (e.g. DebugLoc, MaybeAlign).

Your tasks

  1. rarityLabel — switch on a scoped enum. Return "Common", "Rare", "Epic", or "Unknown" for the default arm. Use a switch on the scoped enum — prefix each case label with the type name: case Rarity::Common:. Return std::string_view (string literals have static storage, so a non-owning view is safe and allocates nothing).

  2. rarityMultiplier — switch returning an int. Map Common → 1, Rare → 3, Epic → 10, default → 1. Same switch pattern as Task 1. Keeping the multiplier in one helper means every caller stays in sync when the numbers change.

  3. itemWorth — const reference + member access. Compute item.quantity × item.unitValue × rarityMultiplier(item.rarity). The parameter is const Item& — a const reference avoids copying the std::string member and promises you will not mutate the caller's data. Access members with .. Delegate to rarityMultiplier; do not inline the numbers.

  4. makeItem — return a struct by value. Aggregate-initialize an Item with the four supplied fields and return it. Member order must match the declaration in ledger.h: name, rarity, quantity, unitValue. Convert the std::string_view name parameter to std::string for the owning member.

  5. restock — mutate through a non-const reference. Add amount to item.quantity. If amount is negative, clamp it to 0 first (a restock function should never accidentally reduce stock). The Item& item parameter — a non-const reference — is what makes the mutation visible to the caller.

  6. isSameItem — compare struct fields (the capstone). Return true if a.name == b.name and a.rarity == b.rarity. Quantity and unit value are irrelevant to catalogue identity. Both parameters are const Item&. Scoped enum values compare with == directly — no cast needed.

Success criteria

  • Every named Rarity round-trips through rarityLabel and rarityMultiplier.
  • An out-of-range static_cast<Rarity>(99) hits the default arm — the switch must be exhaustive.
  • itemWorth with zero quantity returns 0 (the zero-edge).
  • itemWorth with zero unit value returns 0.
  • makeItem fields are set correctly in declaration order.
  • restock with a negative amount does NOT reduce stock (clamp to 0).
  • isSameItem returns true for two Items with the same name and rarity but different quantity/unitValue — those fields must be ignored.
  • isSameItem returns false when only the rarity differs, and when only the name differs.
Concepts practiced

New this chapter (notes 13.1–13.12):

  • enum class (scoped enum) — enumerators scoped to the type, no implicit int conversion, switch with default arm (notes 13.6)
  • Contrast with unscoped enum — name leakage and implicit int conversion are the problems enum class solves (notes 13.2, 13.3)
  • struct aggregate — named members, member access with . (notes 13.7)
  • Aggregate initialization — brace-init fills members in declaration order (notes 13.8)
  • Default member initializers — members start in a safe state (notes 13.9)
  • Passing structs by const reference — read-only, no copy (notes 13.10)
  • Returning structs by value — clean API, compiler handles copy elision (notes 13.10)
  • Mutating through a non-const reference — the restock pattern (notes 13.10, 13.12)
  • Enum-to-string helper using a switch (notes 13.4)
  • Struct containing a program-defined enum member (notes 13.11)

Reused from earlier chapters:

  • std::string and std::string_view (Ch 5)
  • const correctness (Ch 5)
  • if / switch / break / default (Ch 8)
  • References (&) and const references (const &) (Ch 12)
  • static_cast used only in tests for out-of-range enum edges (Ch 4/10)
Constraints

Allowed constructs:

  • enum class, struct, aggregate initialization with {}
  • Member access with .
  • switch / case / default / break (notes 13.4, 13.6)
  • if / else for clamping
  • const Item& and Item& parameters (Ch 12 refs)
  • std::string and std::string_view (Ch 5)
  • Integer arithmetic (*, +, +=) (Ch 1)
  • bool, int, function templates already in scope (Ch 4, 11)

Forbidden (not yet taught or explicitly out-of-scope):

  • Member functions / methods inside the struct — that is Chapter 14. Keep Item data-only.
  • Operator overloading (operator==, operator<<) — Chapter 21.
  • std::vector — Chapter 16.
  • Constructors — Chapter 14. Use aggregate initialization ({} braces) only.
  • Raw int magic numbers inlined into itemWorth — delegate to rarityMultiplier so the mapping is centralized.

Required idioms:

  • Aggregate initialization everywhere: Item x { ... };, not Item x; x.name = ...;
  • Default member initializers in ledger.h — already provided; do not remove them.
  • const Item& for every read-only struct parameter; Item& (no const) only when mutation is the goal.
Build & run locally
shell
make            # compile-check starter/ledger.cpp  (warning-clean)
make test       # grade your code  ->  RED until the TASK blocks are filled in
make solution   # run the grader against the reference solution
make test-solution  # verify the reference passes all checks
make clean      # remove build artifacts

make test is the grader. make solution shows the reference output so you can check your understanding — but peek at solution/ledger.cpp only after you have made a genuine attempt.

Hints
Task 1 — switching on a scoped enum
C++
switch (rarity)
{
case Rarity::Common: return "Common";
case Rarity::Rare:   return "Rare";
case Rarity::Epic:   return "Epic";
default:             return "Unknown";
}

Each case label must use the full Rarity:: prefix — that is what "scoped" means. The default arm is essential: it satisfies -Wswitch-default and catches any value that was created via static_cast from an integer.

Task 3 — why const reference and how to access members

The function signature is:

C++
int itemWorth(const Item& item)

const — you promise not to modify the item. & — no copy; the parameter IS the caller's object (notes 13.10, 13.12).

Inside the body, access members with . (not ->, which is for pointers):

C++
return item.quantity * item.unitValue * rarityMultiplier(item.rarity);
Task 4 — aggregate initialization and std::string_view → std::string
C++
Item makeItem(std::string_view name, Rarity rarity, int quantity, int unitValue)
{
    return Item{ std::string{name}, rarity, quantity, unitValue };
}

std::string{name} converts the non-owning view to an owning string for the struct member — important so the member doesn't dangle (notes 13.11). The brace list fills members in declaration order: name, rarity, quantity, unitValue — exactly as they appear in the struct definition in ledger.h.

Task 5 — non-const reference and negative clamping
C++
void restock(Item& item, int amount)
{
    if (amount < 0)
        amount = 0;
    item.quantity += amount;
}

The & (no const) is the entire difference between "modifies the caller's item" and "modifies a throwaway copy that vanishes when the function returns".

Task 6 — comparing enum class values with ==
C++
bool isSameItem(const Item& a, const Item& b)
{
    return a.name == b.name && a.rarity == b.rarity;
}

Scoped enum values support == directly. No static_cast<int> needed — the whole point of enum class is that it gives you a proper type with proper comparisons (notes 13.6).

Stretch goals
  • Add a printItem function that outputs "Sword [Rare] qty=2 worth=240 coins". Right now you would pass an std::ostream& out parameter and call rarityLabel/itemWorth inside. In Chapter 21 you could overload operator<< so std::cout << sword just works.
  • Replace isSameItem with operator== once you reach Chapter 21 operator overloading.
  • Add a totalLedgerWorth(Item items[], int count) function once you learn C-style arrays (Ch 17) or std::vector (Ch 16). The loop accumulates itemWorth(items[i]).
  • Add a Rarity fromString(std::string_view) returning std::optional<Rarity> using the std::optional pattern from Chapter 12 (already in scope!). This is the exact parse-from-CLI-arg pattern shown in notes 13.4.
starter/ledger.cpp C++
// Chapter 13 — Compound Types: Enums & Structs · Inventory Ledger   (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the six TASK blocks below. Each maps 1:1 to a task in the README and
// to a declaration in ../ledger.h.  The function bodies currently return STUB
// VALUES so the file compiles immediately — that is why `make test` is RED right
// now.  Your job is to implement the real logic and turn it GREEN.
//
//     make build          compile your code (should already work as-is)
//     make test           grade it          (RED until you fill these in)
//     make solution       build + run the reference solution if you get stuck
//     make test-solution  verify the reference passes all checks
//
// SCOPE REMINDER (notes 13.1–13.12):
//   • Allowed up through Ch 12: refs, const refs, static_cast, std::string,
//     std::string_view, int arithmetic, if/switch/for (Ch 8), bool (Ch 4).
//   • NEW this chapter: enum class, struct, aggregate init, member access with .,
//     pass/return by const ref or value (notes 13.6–13.10).
//   • FORBIDDEN here: member functions/methods (Ch 14), operator overloading (Ch 21),
//     std::vector (Ch 16). Keep the struct DATA-ONLY. That boundary IS the lesson.
//
// KEY TERMS to keep in mind as you read the scaffolding:
//   SCOPED ENUM — enum class with :: accessor (notes 13.6)
//   AGGREGATE   — data-only struct, initialized with {} braces (notes 13.7–13.9)
//   CONST REF   — const Item& avoids copying the std::string member (notes 13.10)

#include "../ledger.h"

// ─── TASK 1: rarityLabel — switch on a scoped enum ───────────────────────────
//
// Return a std::string_view label for `rarity`:
//   Rarity::Common -> "Common"
//   Rarity::Rare   -> "Rare"
//   Rarity::Epic   -> "Epic"
//   anything else  -> "Unknown"
//
// Use a SWITCH statement (practicing switch on a scoped enum is the point):
//   switch (rarity)
//   {
//   case Rarity::Common: return "Common";
//   ...
//   default: return "Unknown";
//   }
//
// DO NOT convert to int. Scoped enums switch cleanly (notes 13.6).
//
//   >>> YOUR CODE HERE <<<
//
std::string_view rarityLabel(Rarity /*rarity*/)
{
    return "Unknown";   // stub — replace with the switch above
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 2: rarityMultiplier — switch returning an int ──────────────────────
//
// Return the integer value multiplier for `rarity`:
//   Rarity::Common -> 1
//   Rarity::Rare   -> 3
//   Rarity::Epic   -> 10
//   default        -> 1   (safe fallback for any future enumerator)
//
// Use a SWITCH on the scoped enum, matching the style in Task 1.
//
//   >>> YOUR CODE HERE <<<
//
int rarityMultiplier(Rarity /*rarity*/)
{
    return 1;   // stub — correct only for Common; Rare and Epic will be wrong
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 3: itemWorth — pass by const ref, multiply three values ─────────────
//
// Total coin value of all stock of this item:
//   item.quantity  ×  item.unitValue  ×  rarityMultiplier(item.rarity)
//
// IMPORTANT: the parameter is `const Item& item` — a CONST REFERENCE.
//   • `const` means you cannot modify the struct (read-only).
//   • `&` (reference) means no copy is made — especially important for the
//     std::string member inside Item (notes 13.10, 13.12).
//
// Access members with the DOT operator: item.quantity, item.rarity, etc.
// Reuse rarityMultiplier(...) — do not inline the multiplier values here.
//
//   >>> YOUR CODE HERE <<<
//
int itemWorth(const Item& /*item*/)
{
    return 0;   // stub — always 0 (correct only for quantity-0 edge case)
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 4: makeItem — return a struct by value ──────────────────────────────
//
// Construct and RETURN an Item with the four supplied fields.
// Use AGGREGATE INITIALIZATION with braces (notes 13.8):
//   return Item{ std::string{name}, rarity, quantity, unitValue };
//
// Notes:
//   • `name` is a std::string_view; the Item's std::string member needs a copy.
//     Write  std::string{name}  to construct the owning string from the view.
//     The std::string-from-string_view constructor is EXPLICIT, so a bare
//     `name` will NOT implicitly convert — you must spell out std::string{name}.
//   • Return by VALUE (notes 13.10): the compiler performs copy elision (NRVO),
//     so there is no copy overhead in practice.
//
//   >>> YOUR CODE HERE <<<
//
Item makeItem(std::string_view /*name*/, Rarity /*rarity*/,
              int /*quantity*/, int /*unitValue*/)
{
    return Item{};   // stub — returns a default-constructed empty Item
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 5: restock — mutate through a non-const reference ──────────────────
//
// Add `amount` units to item.quantity. If `amount` is negative, treat it as 0
// (do NOT decrease the stock count below what was there).
//
// PASS BY NON-CONST REFERENCE `Item& item` — that is what lets us modify the
// caller's object. The & is essential: without it, we'd modify a local copy and
// the caller would never see the change (notes 13.10, 13.12).
//
// Pattern:
//   if (amount < 0) amount = 0;
//   item.quantity += amount;
//
//   >>> YOUR CODE HERE <<<
//
void restock(Item& /*item*/, int /*amount*/)
{
    // stub — does nothing; the quantity will never change in tests
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 6: isSameItem — compare two items by name and rarity ───────────────
//
// Return true if a and b have the same name AND the same rarity.
// The quantity and unitValue do NOT matter for catalogue identity.
//
// Both parameters are `const Item&` — read-only references; no copies needed.
// Access members with `.`:  a.name,  a.rarity,  b.name,  b.rarity
// std::string supports == for lexicographic comparison.
// enum class values support == directly (no cast needed, notes 13.6).
//
//   >>> YOUR CODE HERE <<<
//
bool isSameItem(const Item& /*a*/, const Item& /*b*/)
{
    return false;   // stub — always says "different" (wrong for matching items)
}
// ─────────────────────────────────────────────────────────────────────────────
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).