Part IV · C++ and Objects

14 Resource Ownership: RAII, the Rules of Zero, Three, and Five, and Smart Pointers

PairWeek 11Lab: A rule-of-five audit of an agent-written classOwnership

What still holds

Ownership is a rule, and in C++ the destructor enforces it.

The agent may: implement a resource-owning class from your ownership rules, which you then audit.

Learning objectives

After this chapter you should be able to:

Do unaided:

  1. Draw a program's lifetime timeline with every constructor and destructor call, and explain RAII with it.
  2. Explain shallow versus deep copy, predict what a shallow copy does to memory, and write the five special members for an owning class.
  3. Explain what std::move does (a cast) and state the moved-from state of a class you wrote.
  4. Choose unique_ptr, shared_ptr, weak_ptr or a raw observer for an ownership situation, with a one-sentence reason.
  5. Predict how copy and move cost grow with size, and check with a timing run.

May delegate:

  1. A resource-owning class implemented from your declaration and ownership rules, followed by your own rule-of-five audit.

Opening case

A composite case, not a real student or course.

Priyanka's PA4 box blur works on the station's sky-camera frames. She wants a scratch area for intermediate rows, so she asks the agent for "a small Buffer class that owns a heap array of doubles, fast, no vector overhead." The answer arrives in seconds:

The chapter continues from here.

The loop in this chapter

Open a step to see what it asks of you and how it plays out in the Pair phase.

  1. PredictWithout the agent, draw the timeline and memory after Buffer b = a;, predict the ASan error kind, and write cost formulas for copy and move.

    Step 1 of 6 · You, no AI

    What you hand in: A written prediction: the output, the memory state, the operation count, and how sure you are.

    Why this step: A prediction written before the run is the only way to learn whether your model of the machine is right. A miss is the most useful result there is, because it shows exactly where the model is wrong.

    The first prediction is never delegated.

    Ask yourself: What will this print, what will memory look like when it ends, and how many operations will it take?

  2. WriteBy hand, the copy constructor and copy-and-swap assignment.

    Step 2 of 6 · You, no AI

    What you hand in: One small piece written by hand, to build the mental model.

    Why this step: You can only check what you could have built. Writing a small piece yourself gives you the model you will need to judge the agent's version later.

    Ask yourself: Could I write this piece again tomorrow without notes?

  3. SpecifyThe declaration, the ownership rules (never delegated), tests from the rules, and a budget: copy proportional to n, move constant.

    Step 3 of 6 · You

    What you hand in: The header or class interface, unit tests, a complexity budget and the ownership rules.

    Why this step: The specification is the contract the code will be held to. Tests written first, a cost budget and ownership rules turn "looks right" into something you can check.

    The ownership and lifetime section of a specification is never delegated.

    Ask yourself: Who allocates, who frees, what does it cost, and what input would break it?

  4. DelegateThe Pair writes the remaining members or a rule-of-zero rewrite; save the transcript.

    Step 4 of 6 · The agent

    What you hand in: The implementation from your specification, or an explanation in the Tutor phase, with the prompts and transcript saved.

    Why this step: The agent does the typing; you keep the judgment. Saving the prompts and the transcript makes the delegation something you can audit afterward.

    Pair Pair phase: the agent writes small units from your specification, and you audit every line.

    Ask yourself: Did I hand over my specification and tests, and did I save what the agent said?

  5. AuditThe five-member checklist, then ASan, Valgrind, the growth count and the timing; record each defect with its instrument.

    Step 5 of 6 · You and the tools

    What you hand in: The Audit Report: tests, sanitizer and Valgrind output, your own cost derivation against a timing plot, the defects found, and a keep-or-rewrite decision.

    Why this step: What the agent says is testimony until an instrument confirms it. Tests, sanitizers, Valgrind and a timing plot are evidence.

    The final judgment that the code is correct is never delegated.

    Ask yourself: What did the sanitizer report, does the timing plot match my derivation, and would I keep this code or rewrite it?

  6. DefendWhere the array is freed, what a move leaves, why each pointer type.

    Step 6 of 6 · You, live

    What you hand in: A five-minute oral defense.

    Why this step: If you can explain it live, you own it. The oral checks that the understanding is yours and not the agent's.

    Ask yourself: Why this bound, what input breaks it, and where is this memory freed?

What agents get wrong here

Patterns observed in agent-written code, to test for, not guarantees. If the agent no longer makes the mistake, your job is to prove it got it right, with the same instrument.

The rule of three without moves, or a move constructor without noexcept

How to catch it Count copies and moves over 1,000 insertions; static_assert(std::is_nothrow_move_constructible_v<T>)

Section 14.3.1

Raw delete on memory a unique_ptr owns

How to catch it ASan attempting double-free; every delete and free needs a written reason

Section 14.3.2 · AddressSanitizer

shared_ptr everywhere, "the safest choice"

How to catch it Name two owners with non-nested lifetimes for each; LeakSanitizer on a build-and-destroy test

Section 14.3.3 · LeakSanitizer, Tests, Timing and log-log plots, Your own trace

What the chapter covers

  1. 14.1 The concept, by hand
    • 14.1.1 Destructors and the object lifetime timeline
    • 14.1.2 RAII for files, buffers and locks
    • 14.1.3 The double free: what the default copy does
    • 14.1.4 The rules of three and five, and copy-and-swap
    • 14.1.5 Move semantics and what std::move actually does
    • 14.1.6 The moved-from state
    • 14.1.7 The rule of zero
    • 14.1.8 unique_ptr, shared_ptr, weak_ptr and raw observers
    • 14.1.9 A shared_ptr cycle, and weak_ptr to break it
  2. 14.2 Predict and check
    • 14.2.1 Construction and destruction order
    • 14.2.2 Copy cost against move cost
  3. 14.3 What agents get wrong here
    • 14.3.1 The rule of three without move
    • 14.3.2 Raw delete mixed with unique_ptr
    • 14.3.3 shared_ptr everywhere
  4. 14.4 Working with the agent
    • 14.4.1 The spec you write first
    • 14.4.2 Prompt patterns
    • 14.4.3 What to check, in order
  5. 14.5 Lab: A rule-of-five audit of an agent-written class
  6. 14.6 Defend it

Cost thread

Table 14.3 gives the median of five runs of copy_vs_move.cpp on the course container (g++ 13.3, -O2, a 2.1 GHz server processor), with the range of copy times.

Summary

  • Destructors run at points you can mark on the lifetime timeline, so RAII releases files, buffers and locks on every exit path.
  • The generated copy is memberwise, so a raw owning pointer is copied shallowly: one 32-byte block freed twice from one closing brace.
  • Write any of the five special members and you must consider all five; copy-and-swap is self-assignment safe. The rule of zero is the default, but only as good as its members.
  • std::move is a cast. Without noexcept on the move constructor, std::vector growth made 1,023 copies and no moves.
  • unique_ptr is no bigger than a raw pointer, even with a FILE* deleter. shared_ptr pays on every copy (30 to 60 times a raw observer once a thread had run, on our machine), and a two-way link leaked 152 bytes that weak_ptr recovered.

Key terms

  • destructor: the member ~T(), run when an object's lifetime ends, that releases what it owns.
  • RAII: acquiring a resource in a constructor and releasing it in the destructor.
  • shallow copy: a copy of a pointer but not of the resource behind it.
  • deep copy: a copy of the resource itself, so each object owns its own.
  • copy constructor: T(const T&), which builds a new object as a copy.
  • copy assignment operator: T& operator=(const T&), which overwrites an existing object with a copy.
  • copy-and-swap: assignment by copying into a temporary, then swapping it with *this.
  • rule of three: a class needing a destructor, copy constructor or copy assignment needs all three.
  • rule of five: the rule of three plus the two move members.
  • rule of zero: members manage every resource, so the class declares none of the five.
  • move semantics: taking a soon-discarded object's resources instead of copying them.
  • rvalue reference: T&&, which binds to temporaries and to std::move results.
  • std::move: a cast to an rvalue reference; it moves nothing.
  • moved-from state: what a move leaves behind; it must stay destroyable and assignable.
  • noexcept: a promise not to throw.
  • std::unique_ptr: a move-only smart pointer that is an object's sole owner.
  • std::shared_ptr: a smart pointer sharing ownership through a reference count in a control block.
  • std::weak_ptr: a non-owning observer of a shared_ptr-managed object.
  • observer pointer: a raw pointer or reference to an object owned elsewhere; it never deletes.
  • custom deleter: a callable a smart pointer runs instead of delete.
  • smart pointer: a class that holds a pointer and enforces an ownership rule in its special members.
  • reference count: the number of shared_ptrs that currently own an object, kept in its control block.

Companion code

10 files for this chapter, compiled with the book's flags.

Read the code Download ch14.zip

Connections

Where this chapter and another point to each other most often.

See it on the map

Further reading

  • Stroustrup (2013), on resource management and on copy and move: the designer's own account of RAII.
  • Meyers (2014), the items on std::move, noexcept and smart pointers: why std::move does not move.
  • Stroustrup and Sutter (2024), Core Guidelines C.20 to C.22 and section R: numbered rules to cite in an Audit Report.
  • Serebryany et al. (2012): how ASan finds double frees, and so what it cannot see.