Skip to content

Module 11 — Challenge Problems

Open problems. More than one defensible answer exists, and the reasoning is assessed as much as the code.

Each one asks for a decision, a piece of evidence, and a written account of the trade-off you accepted. Write the account before you compare with anything.


Challenge 1 — ownership for a cache with evictions [L5 Design]

The problem. Design the ownership for a document cache with these properties:

  • entries are read many times, so they must stay in memory while anybody is using them;
  • the cache may evict an entry at any moment to stay within a memory budget;
  • a component that has read an entry may still hold a handle to it when it is evicted;
  • an evicted entry may be read from disk again later, and it must be safe for a holder to ask whether its handle is still valid.

Decide what each smart pointer means in your design, and what a holder may legitimately do with an evicted entry.

What a good answer contains.

  • A design in which "evicted" and "in use" cannot contradict each other, expressed in types. The cache stores the only owning shared_ptr; holders get a weak_ptr and lock() when they need to use the entry; the cache evicts by resetting its own pointer, which destroys the entry only if no holder has locked it.
  • The safe acquisition path written out, in the right order: lock(), check the result, use it — never expired() and then use.
  • A worked trace: a holder locks an entry, the cache evicts it, the holder keeps using it safely, and the entry is destroyed when the holder's locked copy goes out of scope. The trace must show a destructor line at a moment you predicted.
  • One paragraph on what a holder may not do with its handle (retain it indefinitely, or assume the entry is still cached), and what the design does about the second point if the holder needs a stable object rather than a borrowed one — for example, the holder taking a copy of the value.

What would make it weak. Giving holders shared_ptrs, which makes eviction impossible while any holder exists — the cache's own budget rule becomes unenforceable. Or storing raw pointers plus a "is it still there?" flag, which is the race this module exists to remove.

Deciding it for yourself. Who decides that an entry must die: the cache's policy, or the last holder to release it? Your answer determines whether the entry can be evicted while in use, and therefore whether the cache can keep its memory promise at all.


Challenge 2 — account for every pointer in a system full of them [L7 Refactor]

The problem. Take a working program that uses raw pointers throughout, with one leak and one double free. Refactor it to smart pointers, and produce a table of every pointer: what it points at, what owns it, and why you assigned that ownership.

A realistic starting point, if you do not have one of your own:

class Session {
public:
    Session(User* user, Logger* logger) : user_(user), logger_(logger) {}
    ~Session() { delete user_; }                 // ← the double free, once sessions are copied
    Logger* logger() const { return logger_; }
private:
    User* user_;                                 // owned? borrowed? the signature does not say
    Logger* logger_;                             // shared by every session; deleted by whoever
};

What a good answer contains.

  • Both bugs gone, demonstrated — with counters or a sanitizer — rather than asserted.
  • A table with one row per pointer, naming its new type and the reason. The rows where the answer is "it never owned anything" are as important as the rows that become owners: a Logger shared by every session is either a shared_ptr with a stated reason, or (better, usually) a reference to something that outlives every session.
  • The Session copy problem resolved explicitly. Session owns a User, so copying a session must either be forbidden or must copy the user — and the design question is which of those the system actually means. Say what you chose and what it cost.
  • A note on which of the two bugs a compiler error would have caught and which it would not (the double free becomes a deleted-copy error; the leak on the shared logger does not become anything until the ownership is made explicit and the counts are balanced).

What would make it weak. A mechanical shared_ptr everywhere, which removes both bugs while answering none of the ownership questions — and which will produce a cycle the first time an object refers back to a session.

Deciding it for yourself. For each pointer, ask: if this object died here, would that be a bug? Every "yes" is an owner, every "no" is a borrow, and the two require different types.


Challenge 3 — judge shared_ptr-everywhere against unique_ptr-and-borrow [L8 Evaluate]

The problem. Build the same subsystem twice:

A. every long-lived object held by std::shared_ptr, and every parameter that needs one takes a
   shared_ptr by value;
B. every long-lived object held by std::unique_ptr, every non-owning parameter taking a reference
   or a raw non-owning pointer, and sharing introduced only where the lifetime genuinely cannot be
   attributed to one owner.

Then evaluate them on correctness, cost, clarity and maintenance, and say which you would ship for each of: a learning codebase, a long-running service, and a single-threaded command-line tool.

What a good answer contains.

  • Both designs building and running with the same behaviour, from the same scripted sequence.
  • Measurements where they exist: sizeof of the handles, the number of atomic reference-count operations per pass (instrument a copy/destruction counter around the pointers, or count use_count() transitions), and the size of each type's parameter lists.
  • A real cost of the design you prefer. For A: the cycle risk, the atomic traffic, the false promise of a shared_ptr parameter that is only read. For B: more parameter plumbing, callers who cannot keep an object alive by holding a copy, and a design where ownership has to be established in advance rather than discovered.
  • A distinction between correctness (both are correct) and the probability of staying correct as the system grows — which is where the two genuinely differ, and where your real answer lives.
  • One sentence on what measurement would change your mind.

What would make it weak. "shared_ptr is easier, therefore better" without naming what it costs; or "unique_ptr is faster" without showing the difference in a case where it matters. Also weak: declaring one design "more modern" — this module exists partly to unteach that reflex.

Deciding it for yourself. Which design would you rather debug at 2 a.m. six months from now, when a destructor does not run and you cannot see why? Then check whether that reason is the same one you gave for all three settings. If it is not, you have found a genuine context dependence, which is the answer this challenge is after.


Evaluating your own answer

Write these four lines before reading any suggested approach:

My decision:
The trade-off I accepted:
The evidence I collected:
What I would do differently with more time:

Levels: [L5 Design] · [L7 Refactor] · [L8 Evaluate] — this module's exit level is 7, and Challenge 3 goes beyond it.


Next: Self-assessment — nine questions, including a prediction with no output at all.