Why would you choose an unchecked non-owning handle instead of a checked weak handle in a reference-counted design?
answer
- one axis, three answers
- a check traded for a proof
- no count, no branch, no absent path
- correct only when lifetimes enclose
- failure is a silent dangling read
basics
~20 sFor 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.
solid answer
~50 sThe three strengths sit on one axis: an owning handle keeps the object alive, a weak handle asks each time whether it is still there, and an unchecked non-owning handle asks nothing. You take the unchecked form when the target's lifetime provably encloses the holder's — a part that cannot exist without its whole, a back pointer from an entry to the catalogue that creates and destroys it. In exchange you drop the count update on copy and release, the liveness check and its branch at every use, and the optional type that forces callers to write an absent path that can never be taken. The price is that the guarantee is now yours to keep, not the machine's: if it is broken you read destroyed storage, and designs differ in whether that traps loudly in a checked build or corrupts silently.
go deeper
Learn the three strengths as one axis: owning keeps it alive, weak asks each time, unchecked asks nothing and trusts you. The third is the only one that can dangle.
Be able to say exactly what the unchecked form removes — the count update, the liveness check, the absent branch, the indirection — and what invariant you are promising in return.
Show that you have debugged the failure mode: a read of destroyed storage that may not fault, surfaces as wrong data far from its cause, and reproduces only under particular allocation timing.
Weigh a marginal, usually unmeasurable saving against a defect class that resists reproduction, and decide where in a codebase the proof can even be maintained as people change the code.
## Three strengths on one axis Every edge in a reference-counted object graph answers one question — *does the thing at this end need the thing at the other end to exist?* — and there are three possible answers. | strength | holds the count | behaviour once the target is destroyed | what the holder must prove | |---|---|---|---| | owning | yes | cannot happen while the handle lives | nothing | | weak | no | reads as absent, checked on every use | nothing | | unchecked non-owning | no | dangles; reading it is undefined | that the target outlives the holder | Only the last row moves an obligation from the runtime onto the author. That is the entire proposition: **you are trading a check for a proof you make yourself.** ## What the unchecked handle actually removes - **The count update.** Copying and releasing it touches no counter, so passing it around is as cheap as passing an address. - **The per-use check.** A weak handle is tested before every read; an unchecked one is read directly. - **The absent path.** A weak handle yields something optional, so every call site must decide what to do when the answer is "gone". Where the object genuinely cannot be gone, that branch is dead code, and dead code with no test coverage is its own liability. - **The extra indirection.** A weak handle usually reaches its object through a shared record; an unchecked one can be a direct address. None of those is large on its own. On an edge traversed once per request the difference is unmeasurable, which is exactly why the unchecked form is over-used: it feels free, and the bill arrives elsewhere. ## The proof obligation you take on The unchecked form is correct only when the target's lifetime **encloses** the holder's — the target exists before the holder is created and is still alive when the holder is destroyed. The situations where that is genuinely provable share a shape: 1. The holder is created by the target and destroyed by the target, so the target's own destruction is what ends the holder. 2. The holder is a part of a whole, stored inside it, reachable only through it. 3. Both objects are visible in one module, so a reviewer can see the whole lifetime without leaving the file. And the shapes where it is *not* provable, however tempting: the target is "long-lived", the target is "usually" alive, the target is owned by someone else who promised, or the two objects are handed between threads. ## What failure looks like A broken weak-handle assumption produces an absent read and a program that takes its not-found path. A broken unchecked-handle assumption produces a read of storage that is no longer valid, and that is a different class of event: - it need not fault at all, because the memory is often still mapped and may already have been handed to another object; - it can therefore surface as wrong data rather than as a crash, arbitrarily far from the edge that caused it; - it depends on allocation timing, so it reproduces poorly and looks like a heisenbug. Some designs help: a checked build can keep the record alive and trap on the first unchecked use after destruction, turning silence into a loud, local failure. Others offer nothing at all. You cannot assume a crash will find these for you. ## Choosing between weak and unchecked The practical decision rule is short: - If you can state the enclosing-lifetime invariant in one sentence, and a reviewer can verify it without leaving the module, an unchecked handle is defensible — and the sentence belongs in a comment at the declaration. - If stating it needs the word "usually", "should" or "as long as nobody", use a weak handle and write the absent path. - If the edge crosses a public boundary or a thread boundary, use a weak handle regardless of what you can prove today, because the proof is about code that other people will change. The default that survives contact with a team is *weak unless argued*. A weak handle that was never needed costs a branch; an unchecked handle that was not justified costs a debugging week.
- Why is "the target is long-lived" not a sufficient justification for an unchecked handle?Because it is a probability, not an invariant. An unchecked handle is correct only if the target cannot be destroyed first, and "long-lived" allows shutdown ordering, error paths and future refactors to destroy it first. The moment the argument needs the word "usually", the correct choice is a weak handle and an absent path.
- How would you make a broken unchecked-handle assumption easier to find?Make the failure loud and local. In test or checked builds, keep the shared record alive after destruction and trap on the first unchecked read, so the fault fires at the offending edge rather than as wrong data later. Pair that with stating the enclosing-lifetime invariant at the declaration, so a reviewer can see what was assumed.
- Does an unchecked handle help with a hot loop's performance?Rarely enough to matter. It removes a predictable branch and one indirection per use, which is noise against any real work in the loop body. If a profile genuinely points at the check, the better answer is usually to promote once outside the loop and hold a temporary owning handle across it.
saying these in an interview costs you the question
- Justifies it with "the target is long-lived" rather than an invariant
- Believes an unchecked handle reports absence like a weak one
- Assumes a use-after-destroy will reliably crash and be found
- Thinks skipping the check is a meaningful speed win on a cold edge
- Uses it across a public API or thread boundary