Skip to content

← RAII: Objects With Lifetimes step 2 of 4

Medium Primitives

Write the guard yourself

std::lock_guard, std::unique_ptr, std::fstream, std::scoped_lock — every one of these is the same twenty lines. Writing those twenty lines once is the difference between using them and understanding them.

The shape

class Guard {
public:
    explicit Guard(Resource& r) : r_(r) { r_.acquire(); }   // acquire
    ~Guard() { r_.release(); }                              // release

    Guard(const Guard&) = delete;                           // and this
    Guard& operator=(const Guard&) = delete;

private:
    Resource& r_;
};

Four members, and the last two are the ones people leave out.

Why copying must be deleted

Suppose you allow it. Now:

Guard a{resource};
Guard b = a;          // copy — acquired once, about to be released twice

Both destructors run. The resource is released twice: a lock unlocked twice, a pointer freed twice, a refcount driven below zero. Double-free is one of the classic memory-safety bugs, and this is the shape it usually arrives in.

So a guard deletes its copy operations, and the compiler turns a whole category of bug into a compile error. = delete is not a hint; the function exists, overload resolution picks it, and using it fails to compile with a message naming the deleted function.

You will meet the fuller version of this rule in Track 3 as the rule of five. Today the short version is enough: if the destructor does something, copying is probably wrong.

explicit, and why it is there

explicit Guard(Resource& r)

Without explicit, a single-argument constructor is an implicit conversion — the compiler is allowed to build a Guard out of a Resource silently, in any context that wants one. For a type whose construction acquires a lock, being constructed by accident is exactly what you do not want. Track 6 gives this its own problem; for now, one-argument constructors get explicit unless you have a reason.

Your task

A Counter is given: acquire() increments a live count and a total, release() decrements the live count. Write the guard.

class CounterGuard {
    // acquire in the constructor, release in the destructor,
    // no copying
};

int max_live(Counter& counter, const std::vector<int>& depths);

depths describes nesting: for each entry d, create d nested guards, then let them all go out of scope. Return the highest live count ever observed.

For depths = {1, 3, 2} the answer is 3 — three guards alive at once during the middle entry, and none left alive between entries.

The starter’s guard compiles and does nothing — it stores the counter and never touches it — so every test comes back [0, 0]. The recursion is already written.

Note the second number the tests check: the live count after everything has gone out of scope. It is there to catch a guard that acquires and forgets to release, which would otherwise pass on the peak alone.