skip to content

questions

5

In a reference-counted object graph, what happens to a weak handle when the last owning handle to its object is dropped?

level: middleimportance: must knowfreq 68%

answer

  1. only owners are counted
  2. a claim versus a question
  3. zero owners means destroy now
  4. weak handles read absent afterwards
  5. shared record outlives the payload

basics

~20 s

It reads as absent from that moment on. Only owning handles are counted, so the last one dropping destroys the object immediately; weak handles are not counted, cannot delay that, and are never allowed to yield destroyed storage.

solid answer

~50 s

Only owning handles contribute to the count that decides liveness. When the last one is dropped the count reaches zero and the object is destroyed there and then, at the point of the drop, releasing the owning handles it held in turn. A weak handle is not counted, so it cannot postpone any of that; what it gets instead is a guarantee that it will never hand you a destroyed object — from that moment every weak handle to it reads as absent. Implementations get that guarantee by routing weak handles through a small shared control record that holds the counts, so destroying the payload only has to clear one pointer in one place. The record itself survives until the weak handles are gone too, which is why a weak handle still pins a little metadata after its object has died.

code

pseudocode · 14 lines
pseudocode
release_owning(handle):
    record = handle.record
    record.owning = record.owning - 1
    if record.owning == 0:
        destroy_payload(record.payload)   # runs now, on this thread
        record.payload = NONE             # weak handles now read absent
        if record.weak == 0:
            free(record)                  # no observers left either

release_weak(handle):
    record = handle.record
    record.weak = record.weak - 1
    if record.weak == 0 and record.owning == 0:
        free(record)

go deeper

for a junior

Remember the one-line split: an owning handle is counted and keeps the object alive, a weak handle is not counted and merely watches. Zero owners means the object is destroyed right then.

for a middle

Explain the mechanics: two counts in one shared record, the payload destroyed at zero owners, the record freed only once the weak handles are gone too, and destruction cascading into the handles the object held.

for a senior

Show what this costs in a running system — destruction cascades inline on the dropping thread, and a registry of weak handles to dead objects still grows until you prune it.

for a principal

Frame the trade you are buying: deterministic release at the exact moment of the last drop, paid for with an indirection, an extra count and an absent path that every observer has to implement.

## The count only counts owners A reference-counted object carries a number — in a header word on the object, or in a side record keyed by its address — recording how many **owning handles** currently refer to it. Copying an owning handle raises that number; destroying or reassigning one lowers it. The number is the object's licence to exist: while it is above zero, some part of the program has declared that it needs the object. A **weak handle** deliberately stays out of that number. It refers to the object without asserting that the object should continue to exist. That is the whole difference in one line: **an owning handle is a claim, a weak handle is a question.** ## What happens at the moment of the last drop 1. The last owning handle is destroyed or reassigned and the owning count goes from one to zero. 2. The object's destruction hook runs immediately, on whichever thread performed that drop — not at some later collection point. 3. The owning handles that the object itself held are released in turn, so one drop can cascade through a whole subgraph. 4. The payload's storage is no longer valid to read. 5. Every weak handle to that object must, from now on, answer "gone". Step 5 is the one candidates skip, and the interesting question is *how*. Nothing walks the heap hunting for weak handles to null out one by one — that would need a back-pointer list per object and would cost about as much as tracing. Instead every weak handle reaches the object indirectly, through one shared record, and destruction clears a single field in that record. ## Why a weak handle is an indirection, not a bare address The shared record typically holds two numbers: the **owning count**, which decides when the payload is destroyed, and a **weak count**, which decides when the record itself may be freed. Two counts mean two lifetimes: - the object's lifetime ends when the owning count hits zero; - the record's lifetime ends when the last weak handle goes away as well. | | owning handle | weak handle | unchecked non-owning handle | |---|---|---|---| | counted toward liveness | yes | no (counted separately, for the record) | no | | keeps the payload alive | yes | no | no | | after the payload dies | cannot happen | reads as absent | dangles; reading it is undefined | | cost at each use | none | a liveness check, usually a promotion | none | Designs differ in where that record lives. Some allocate the record and the payload together in one block, which means a single surviving weak handle keeps the whole block reserved even though the payload's contents are destroyed. Others allocate the record separately, so only a few bytes survive but every object that might ever need a weak handle pays an extra allocation or an extra word. Ecosystems make this trade differently; the mechanism is the same either way. ## The cost that survives the object A weak handle is cheap, not free, and it is not free *after* the object dies either: - It pins the shared record. A registry that accumulates weak handles to long-dead objects still grows; you prune dead slots, you just do not leak the objects themselves. - Each use costs a branch, and a **correct** use costs a promotion to a temporary owning handle, because liveness can change between the test and the read. - The code that holds it must carry an "absent" path. That path is real program behaviour someone has to design: re-fetch, clear the selection, skip the notification. ## Where designers get this wrong The common error is reading "weak" as a schedule rather than as a strength — believing the object is freed *later* for weak holders, or that a weak handle delays destruction until it is convenient. It does neither. The second error is expecting resurrection: once the owning count has reached zero the object is gone, and a weak handle that later finds the count at zero cannot revive it. The third is assuming the record disappears with the object, which is why a leak audit that counts only live objects can miss a pile of retained metadata. The payoff for getting it right is deterministic: the moment the last genuine owner releases an object, the memory goes back, and every observer that was merely watching discovers it politely rather than by reading rubble.

  • Why does the shared record have to outlive the object it describes?
    Because the weak handles are still out there, and each of them has to be able to find out that the object is gone. They reach the object only through the record, so the record is the one place that can answer. Freeing it with the payload would leave every surviving weak handle reading freed memory — exactly the failure weak handles exist to prevent.
  • If a weak handle never keeps the object alive, can a program leak through weak handles?
    Not the objects, but yes the metadata. Every weak handle pins its shared record, so a cache or registry that keeps adding weak entries and never prunes the dead ones grows without bound. Where a design places the record inside the same allocation as the payload, the dead payload's storage is held too. Prune dead slots on a schedule or on access.
  • Does destruction at count zero happen on a particular thread?
    On whichever thread performed the drop that took the count to zero. That is worth knowing because the destruction cascade — and any work the destructor does — runs inline on that thread, so a single release can cost far more than a decrement if it tears down a large subgraph.

A library card holds a book on loan; a note in your diary saying "chapter 4 of that book" does not. When the last loan ends the book goes back on the shelf, and your note now points at a title, not a copy in your hands.

saying these in an interview costs you the question

  • Says weak handles are freed later than owning ones, as a schedule
  • Believes the runtime hunts down and nulls each weak handle individually
  • Thinks a weak handle delays destruction until it is convenient
  • Expects a weak handle to revive an object whose count reached zero
  • Assumes the shared control record dies with the payload
open as a page

In a reference-counted design, which edges of a catalogue should own: its entries, its selected-entry pointer, or its registered progress observers?

level: seniorimportance: must knowfreq 56%

basics

~20 s

Only the containment edge. The catalogue owns its entries; the selected-entry pointer and the observer registrations must not, or removing an entry frees nothing and a finished observer is kept alive by the catalogue it registered with.

open as a page

Why would you choose an unchecked non-owning handle instead of a checked weak handle in a reference-counted design?

level: middleimportance: should knowfreq 40%

basics

~20 s

For speed and for intent. An unchecked non-owning handle skips the count update and the per-use liveness check, and it declares that the target is guaranteed to outlive its holder. When that guarantee is wrong, the read dangles instead of reporting absence.

open as a page

What policy should a technical lead set for when unchecked non-owning handles may be used in a shared codebase?

level: principalimportance: should knowfreq 31%

basics

~20 s

Make the checked weak handle the default and the unchecked form an exception that must be argued: allowed only where an enclosing-lifetime invariant can be stated in one sentence at the declaration, kept inside a module, and backed by a build that traps on use-after-destroy.

open as a page

Why must a weak handle be promoted to a temporary owning handle before use rather than only tested for liveness?

level: seniorimportance: nice to knowfreq 27%

basics

~20 s

Because liveness can change between the test and the read. Promotion takes a temporary owning handle in one indivisible step when the count is still above zero, so the object cannot be destroyed mid-use; a bare is-alive test leaves a window open.

open as a page