skip to content

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%

answer

  1. weak unless argued
  2. an invariant, not an expectation
  3. one module, one sentence
  4. never across a public or thread boundary
  5. review the lifetime, not the declaration

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.

solid answer

~50 s

Set the default first: **weak unless argued**, because a needless check costs a branch and a wrong unchecked handle costs a debugging week. Then define what an argument looks like — a one-sentence enclosing-lifetime invariant written at the declaration, naming who creates and who destroys both objects, verifiable without leaving the module. Draw hard lines where no argument is accepted: across a public API boundary, across a thread boundary, and on any edge whose target can be removed independently of the holder. Back it with tooling rather than vigilance: a checked build that keeps the record alive after destruction and traps on the first unchecked read, so the failure fires at the edge instead of as corrupted data later. Finally, review the *invariants*, not the handle types — the proof is about code other people will change.

go deeper

for a junior

Follow the default: use the checked weak handle unless someone has written down why the target must outlive its holder. The unchecked form is not the beginner's tool.

for a middle

Be able to state the invariant an unchecked handle depends on, in one sentence naming who creates and who destroys both objects, and recognise when that sentence is not true.

for a senior

Argue the asymmetry with evidence: a per-use saving of a predicted branch against a defect that may not fault, surfaces far from its cause and resists reproduction.

for a principal

Own the rule and its enforcement — a restrictive default, a stated invariant at each exception, hard boundaries where no argument is accepted, and tooling that makes a broken invariant fail loudly in test.

## Why this needs a policy at all Every other strength decision is local and self-correcting. Choose owning where you meant weak and the symptom is retention, visible in a live-set graph. Choose weak where you meant owning and the symptom is an absent read the first time you exercise the path. Choose *unchecked* where you meant weak and the symptom is nothing at all — until an unrelated timing change turns it into wrong data in production, weeks later, in a module nobody has touched. A defect class that is cheap to introduce, invisible in review, and expensive to find is exactly the kind that needs a written rule rather than individual judgment. And the pressure to introduce it is constant, because the unchecked form is always the fastest thing to type and feels free. ## The shape of a policy that holds 1. **Weak is the default and needs no justification.** The cost is a predictable branch and an absent path — and the absent path is usually real product behaviour someone should have designed anyway. 2. **An unchecked handle requires a stated invariant at its declaration.** One sentence, in the form "X is created by Y, stored only inside Y, and destroyed when Y is destroyed, so Y outlives every X." If the sentence needs "usually", "should" or "as long as nobody", the invariant does not exist. 3. **The invariant must be checkable in one place.** If a reviewer has to open three modules to confirm it, the proof will not survive the next refactor, whatever it looks like today. 4. **No unchecked handles across a public boundary.** Once the edge is visible to code you do not own, the lifetime argument depends on callers you cannot see. 5. **No unchecked handles across a thread boundary.** A lifetime argument that holds in program order stops holding when two schedules interleave. 6. **Loud failure in test builds.** Keep the shared record alive after destruction and trap on the first unchecked read, so a broken invariant fires at the edge that broke it. ## The trade you are actually making | you gain | you pay | |---|---| | no count update on copy and release | a proof obligation that must stay true as the code changes | | no liveness check or branch at use | a failure mode that need not fault and may surface as wrong data | | no optional type, so no unreachable absent path | debugging that depends on allocation timing and reproduces poorly | | an explicit statement of the lifetime relationship | a rule that has to be taught to everyone who joins | The gains are per-use and small; the costs are per-incident and large. That asymmetry is the whole argument for a restrictive default, and it is the argument to make to a team that wants the unchecked form everywhere "for speed": the saving is a branch the processor predicts correctly, and the exposure is a class of bug that resists reproduction. ## Where the honest exceptions live The policy should not be a ban, because there are edges where the checked form is genuinely worse: - A **back pointer from a part to its whole**, where the whole creates the part, stores it and destroys it. The invariant is one sentence and the absent path is unreachable — dead code nobody tests. - A **tightly scoped internal edge** inside a single type, where both lifetimes are declared a few lines apart. - A **hot traversal** where a profile actually attributes time to the check — rare, and usually better fixed by promoting once outside the loop than by removing the check. Even then, the invariant goes in the comment, because the next reader's job is to decide whether it is still true. ## Making the policy survive people The part leads get wrong is treating this as a code-review rule aimed at handle types. Reviewing the *type* catches nothing: an unchecked handle looks correct in isolation, and the mistake is always about lifetimes elsewhere. Three things actually work: - **Review the invariant, not the declaration.** The question in review is "who destroys the target, and is that guaranteed to be after the holder?" — not "is this handle the right kind?". - **Re-check invariants when ownership changes.** When an object gains a second owner or becomes shareable, every unchecked edge pointing at it is now suspect; that should be an explicit step in the change, not an afterthought. - **Keep the count of unchecked edges small enough to enumerate.** If nobody can list them, nobody can re-verify them, and the policy has already failed regardless of what the document says.

  • A team argues unchecked handles everywhere would measurably speed the product up. How do you respond?
    Ask for the measurement, and scope it. The saving is a well-predicted branch and one indirection per use, which is noise against real work; if a profile really does attribute time to it, the targeted fix is to promote once outside the hot loop. Then price the other side: a defect class that need not fault, surfaces far from its cause and reproduces poorly.
  • What change to a codebase should trigger a re-review of existing unchecked edges?
    Any change that alters who destroys the target: an object gaining a second owner, becoming shareable, being cached, being handed across a thread boundary, or having its destruction moved to a different lifecycle point. Each of those invalidates the enclosing-lifetime argument without touching the declaration that relied on it, which is why the edges need to be enumerable.
  • How do you keep the policy from decaying as the team changes?
    Make it mechanical rather than cultural. Require the invariant comment at the declaration so the argument travels with the code, keep the set of unchecked edges small enough to list, and run a checked build that traps on use-after-destroy in test so a broken invariant fails loudly rather than waiting for production timing to expose it.

saying these in an interview costs you the question

  • Sets policy by handle type in review instead of by lifetime argument
  • Accepts "the target is long-lived" as a justification
  • Trades a predictable branch for an unreproducible defect class
  • Allows unchecked edges across public API or thread boundaries
  • Assumes tests will catch a use-after-destroy that need not fault
  • Lets the set of unchecked edges grow beyond anyone's ability to list