Skip to content

Module 11 — Self-Assessment

Nine questions. Part A checks vocabulary; Part B checks prediction, which is the only part that tells you whether you can use this material. Answer on paper, then mark yourself honestly and note which section of Concepts each miss points back to.


Part A — Recall and comprehension

  1. State RAII in one sentence without naming memory. Then name three resources other than memory it applies to. (See section 1 and section 2.)

  2. Why does RAII matter more once exceptions exist? What exactly does the language guarantee about destructors during stack unwinding, and what does it guarantee about ordinary statements? (Section 2.)

  3. What does each smart pointer express — unique_ptr, shared_ptr, weak_ptr — and which is the default? Give the reason in one clause each. (Sections 4, 6, 7.)

  4. Why can a unique_ptr not be copied, and what replaces the copy? What was wrong with auto_ptr that made the language remove it? (Section 4.)

  5. A shared_ptr is "thread-safe". Say precisely what is safe and what is not. (Section 6.)


Part B — Prediction and explanation

  1. Predict the output of this program, and say why.
#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "dtor\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->next = a;
}

Answer. It prints nothing. No destructor runs, because each node's reference count is held above zero by the other's owning link: when main ends, a and b go away, each count falls from 2 to 1, and stops there. The objects are never destroyed and the memory is never released — a reference cycle.

A full-credit answer adds two things. First, that this is not an error any sanitizer or leak checker will report: the memory is reachable, so a leak checker classifies it as still reachable and a sanitizer sees nothing invalid. Second, the fix: one link becomes std::weak_ptr<Node>, after which the counts reach zero and the destructors run in reverse order of the remaining ownership.

  1. Predict the counters. A shared object is created with make_shared, and two shared_ptrs and one weak_ptr refer to it. The first owner is reset, then the second. Predict use_count() after each step, and say exactly when the destructor runs.
auto p = std::make_shared<Item>();      // use_count ___
auto q = p;                             // use_count ___
std::weak_ptr<Item> w = p;              // use_count ___
p.reset();                              // use_count ___   destructor? ___
q.reset();                              // use_count ___   destructor? ___
std::cout << w.expired() << '\n';       // prints ___

Answer. 1, 2, 2, 1, 0. The destructor runs once, after the last owning pointer is released — that is, at q.reset(). The weak_ptr never affects the count, so w.expired() prints 1 (true) at the end, and w.lock() would return an empty shared_ptr.

A full-credit answer notes two further facts. use_count() on a weak_ptr and on a destroyed object is diagnostic only, never something to branch on. And between q.reset() and the expired() check, nothing needs to be got right — expired() is safe to call at any time, which is precisely what makes weak_ptr the tool for the cache design in Challenge 1.

  1. Explain this design decision. A colleague changes a function from void process(const Item& item) to void process(std::shared_ptr<Item> item), saying "it makes ownership explicit". Evaluate that claim.

Answer. The claim is wrong in a specific, checkable way. The function still does not own the item — it does not store it — so the signature now promises something false. What changed is the cost: the caller must supply a shared_ptr, every call performs two atomic reference-count operations, a caller that has a plain Item (or a unique_ptr<Item>) can no longer call it at all, and a temporary owner now exists for the duration of the call.

A full-credit answer names the three signatures that would be right — const Item& to read, Item& to modify, Item* if absence is possible — and the conditions under which the colleague would be right: if the function stores or returns the object, or if the callee must keep it alive beyond the call, or if the caller genuinely shares ownership with others. It also notes what "explicit ownership" really looks like: a type that states the claim, like unique_ptr<T> by value for a transfer, not a shared_ptr passed by value as a formality.

  1. Explain a silent failure. This code compiles with -Wall -Wextra -Werror and deletes the object twice. Why is the compiler silent, and what are the two fixes?
struct Surface {
    std::shared_ptr<Surface> self() { return std::shared_ptr<Surface>(this); }
};

Answer. Constructing a shared_ptr from a raw pointer creates a new control block. Called twice on the same object, the program has two counts, each of which will reach zero independently, and therefore two deletes of one object. The compiler is silent because both constructions are legal — the language has no way to know that the two raw pointers refer to the same object.

Both fixes change the design rather than the syntax. At the class level: inherit std::enable_shared_from_this<Surface> and return shared_from_this(), which reuses the existing control block. At the call site: never wrap a raw pointer that some shared_ptr already manages — copy an existing shared_ptr instead, and if a factory hands out raw pointers to a shared object, that factory is the bug.

A full-credit answer adds why this is a module-level lesson rather than a trivia item: the mistake produces two owners of one object from a perfectly ordinary line, which is exactly the failure Module 10 taught you to fear, reintroduced by a type whose purpose is to prevent it.


Marking yourself

Score What it means
≥ 80%, every miss explained Move on to Module 12
60–79% Re-read sections 4 to 8 of Concepts, redo the cycle and control-block experiments, then retake in a week
< 60% Redo Example 1 and Example 4 from the worked examples, with counters, before continuing

This module's material is not optional for what follows. Module 13 builds exception safety directly on RAII — if the destructor is not the release point, the failure path cannot be made correct at all — and Module 14's containers, the capstone's architecture and every factory in Module 15 assume you can answer who owns this? without thinking about it.

A quiz is diagnostic, not a grade. Keep the list of misses and reread it before Lab 11.


Next: Summary — the module compressed to what you must retain.