cswork / libraryOpen practice workspace

Articles

C++ Smart Pointer Interview Questions: Ownership Cycles, Aliasing, and Custom Deleters

Prepare for C++ smart pointer interviews with tested ownership cycles, weak_ptr, aliasing constructors, custom deleters, and lifetime-boundary exercises.

C++ smart pointer interview questions are ownership questions before they are library questions. Start by identifying who may destroy the resource, which references merely observe it, and whether a stored pointer still names a valid object. unique_ptr, shared_ptr, and weak_ptr express different lifetime relationships. Replacing every raw pointer with shared_ptr can preserve a leak, introduce unnecessary ownership, or hide a dangling alias.

Consider a parent object that owns a child, a child that needs to find its parent, and a callback that wants access to one field. That small design reaches cycles, weak references, aliasing constructors, and lifetime boundaries. For the wider language context, see C++ Interview Questions for Software Engineers.

Evidence boundary: Primary-source facts are attributed to the C++ Core Guidelines and the public C++ working draft below. The draft is a living specification; these examples use established C++20 features. Candidate reports: no verified report establishes this exact exercise. Inference: ownership diagrams, fixtures, and design recommendations are original practice material, with execution limits recorded explicitly.

Which smart pointer matches the ownership contract?

Use unique_ptr when one handle owns destruction and ownership may be transferred. Use shared_ptr when several independent owners must keep the same resource alive. Use weak_ptr to observe a shared ownership group without extending its resource lifetime. A raw pointer or reference can remain appropriate for a non-owning borrow whose lifetime is already guaranteed elsewhere.

Primary-source guidance: the C++ Core Guidelines recommend unique ownership unless shared ownership is needed. That is a design recommendation, not a ban on shared ownership. Explain the actual independent owners instead of choosing a type because it seems safer.

  • Factory transfer: start with unique_ptr<T> and identify who receives sole destruction responsibility.
  • Independent asynchronous owners: consider shared_ptr<T> when each user must extend the object's lifetime.
  • Child observing a shared parent: use weak_ptr<T> and define what happens after the parent expires.
  • Use during a bounded call: a T& or T* can express a borrow; identify which caller guarantees validity.
  • External resource handle: choose an RAII wrapper or a suitable smart pointer with a deleter, and name the operation that correctly releases the resource.

Moving a unique_ptr transfers the handle's ownership; it does not move the managed object to a different address. Copying a shared_ptr creates another owner in the same group. Those distinctions matter when someone stores a raw address elsewhere: neither operation automatically validates that observer's lifetime contract.

Why does a shared_ptr cycle survive scope exit?

Suppose A strongly owns B, and B strongly owns A. Destroying the external variables removes external owners, but each object still has an incoming strong reference from the other. Reference counting does not discover that the remaining ownership graph is unreachable from the application.

The relevant question is which edge should be non-owning. In a tree-like design, a parent can own its child while the child's parent link observes. Changing the back-reference to weak_ptr expresses that relationship directly.

struct Node {
    std::shared_ptr<Node> child;
    std::weak_ptr<Node> parent;
};

std::weak_ptr<Node> observed_parent, observed_child;
{
    auto parent = std::make_shared<Node>();
    auto child = std::make_shared<Node>();
    parent->child = child;
    child->parent = parent;
    observed_parent = parent;
    observed_child = child;
    assert(child->parent.lock() == parent);
}
assert(observed_parent.expired());
assert(observed_child.expired());

Our fixture confirms that neither observation keeps these nodes alive. It does not establish that every parent-child model should use shared ownership. If one tree object exclusively owns all descendants, unique_ptr children and carefully bounded parent observers may describe the design more clearly.

Look for indirect cycles too. An object can own a callback that captures a shared_ptr back to that object. No member needs to be named parent for the cycle to exist. Draw closures, queues, subscriptions, and caches as nodes or edges in the ownership graph.

Do not “repair” the cycle by randomly weakening an edge. A worker that must finish after a request object leaves scope may need a strong reference. A weak capture changes behavior by allowing work to discover that the object is gone. State whether skipping that work is permitted, then choose the ownership policy.

How should you use weak_ptr without a check-then-use race?

Primary-source fact: the weak_ptr specification defines lock() as obtaining shared ownership if the managed object is still available, otherwise returning an empty shared_ptr. The operation is atomic with respect to the ownership transition it tests.

Prefer a single acquisition such as if (auto owner = weak.lock()) and retain that local owner while using the object. Checking expired() first does not reserve the object. It may expire before a later operation, so a successful preliminary check does not justify dereferencing a raw pointer.

Our interview recommendation is to put the expired case into the function's contract. An optional UI callback may do nothing. A required background task may need an error, retry, or different lifetime owner. weak_ptr provides a way to observe lifetime; it does not choose the correct product behavior when lifetime ends.

The acquired owner also protects only lifetime. If another thread changes the object's fields, you still need the appropriate synchronization. A successfully locked pointer can reference a live object whose mutable state is being accessed incorrectly.

What does the aliasing constructor actually own?

A shared_ptr has two concepts you must keep separate: its stored pointer, used by get() and dereference operations, and the ownership group responsible for lifetime. They usually refer to the same object, but an aliasing constructor deliberately separates them.

Primary-source fact: the shared_ptr constructor specification permits a new handle to store one pointer while sharing another handle's ownership. Our example exposes a field while retaining the containing allocation.

struct Box { int value = 7; };
auto owner = std::make_shared<Box>();
std::weak_ptr<Box> observed = owner;
std::shared_ptr<int> field(owner, &owner->value);
owner.reset();
assert(!observed.expired());
assert(*field == 7);
field.reset();
assert(observed.expired());

Resetting owner does not destroy the box because field still participates in its ownership group. Resetting the final alias releases that ownership. The integer field is not separately deleted as though it had been independently allocated with new int.

This helps with APIs that need a subobject view and a lifetime guarantee together. However, retaining an owner does not make every address derived from it stable. An alias to an element inside a vector can become invalid if the vector reallocates, even while the containing object remains alive. The caller must preserve both the owner lifetime and the pointed-to element's validity.

An advanced boundary is that an alias can have a non-null stored pointer while sharing an empty ownership group. Its truth value then does not prove that it owns anything. Treat operator bool, lifetime ownership, and dereference validity as separate questions rather than using one check as evidence for all three.

How do custom deleters change resource cleanup?

Some resources must be closed or released through an API rather than deleted. A file opened through a C library needs the corresponding close operation. A unique_ptr deleter can encode that release policy in the handle type.

int closes = 0;
auto close_file = [&closes](std::FILE* file) noexcept {
    ++closes;
    std::fclose(file);
};
{
    std::unique_ptr<std::FILE, decltype(close_file)> file(
        std::tmpfile(), close_file);
    if (!file) {
        throw std::runtime_error("temporary file failed");
    }
    assert(std::fputs("example", file.get()) >= 0);
}
assert(closes == 1);

The complete local fixture includes the standard headers and runs this example. The counter confirms one cleanup call on the successful acquisition path. The reference captured by the deleter remains alive longer than the handle; reversing that lifetime relationship would make the cleanup callback unsafe.

Primary-source fact: the unique_ptr destructor specification defines invoking the deleter for a non-null owned pointer. Choose the matching deallocation operation and a deleter that does not throw. A deleter may carry state, which can affect the handle's size and move behavior; do not promise every smart pointer is exactly one machine word.

Our file example intentionally ignores the close result during destruction. If a successful flush or close is part of the application's correctness contract, provide an explicit operation that reports failure before destruction, with the destructor as a cleanup fallback. RAII guarantees a cleanup path; it does not automatically turn every cleanup failure into an observable business result.

Distinguish reset from release. Primary-source fact: the unique_ptr modifier specification describes release() returning the stored pointer and relinquishing ownership without deleting it. The receiver must take responsibility. Calling release() merely to silence an ownership problem can create a leak.

Which mistakes remain possible with smart pointers?

Do not construct a second independent shared_ptr from existing.get(). That creates another ownership group for the same address rather than joining the first group, potentially causing repeated destruction. Copy the existing owner, use an appropriate alias, or use an established enable_shared_from_this relationship.

Primary-source fact: enable_shared_from_this supports obtaining a shared owner connected to an existing ownership group. It does not make shared_from_this() safe before that relationship exists. Calling it during construction is not a general technique for publishing an already-owned object.

For concurrency, separate three objects: the control block, a particular smart-pointer handle, and the pointee. Different shared handles can participate in the same ownership group without making unsynchronized mutation of one handle or of the pointee safe. See the shared_ptr specification for the library's data-race boundaries.

A use_count() observation is not a lock. Another owner can appear after the count is read, and the count says nothing sufficient about concurrent raw observers or field access. Explain the actual synchronization policy rather than treating a momentary count of one as exclusive access permission.

Practice proving the lifetime boundary

Our checks compile the three article fixtures in C++20 mode and run them with assertions. Additional checks construct a strong cycle, confirm that external-owner removal leaves it alive, then deliberately break the cycle so the test itself does not retain leaked objects. This validates the example lifetimes, not a general absence of leaks or races in a production program.

Practice questionWhat to demonstrate
Repair Unsafe C++ Ownership with Smart PointersIdentify the responsible owner and repair the specific defect
Explain Weak Pointers and Sketch Shared-Pointer Control-Block SemanticsSeparate stored address, control block, and observation
Implement and Explain a C++ Smart PointerDefend destruction, movement, and cleanup invariants
Implement a reference-counted smart pointerExplain what reference counting cannot discover
Compare C Generic Techniques and C++ Smart-Pointer OwnershipChoose a handle based on resource and ownership requirements

Start with Repair Unsafe C++ Ownership with Smart Pointers. Mark every edge as owning or observing, then remove the external roots on paper. If you cannot predict destruction, changing pointer types is premature.

Sources and Further Reading