2Brute2Best
🔥0
F5

Ownership in Designs

High-frequency interview topicRAIIOwnership Semantics std::make_unique = delete
⚡ 60-Second Architecture SummaryF5 · Foundation
4 Core ConceptsC++23 Verified Demo5 Rules of Thumb
Why It Matters in High-Scale Systems

In garbage-collected runtimes like Java or Go, developers rarely think about who deletes an object. In C++ Low-Level Design, object lifetime is an explicit architectural decision. Every pointer or reference in a class diagram answers a critical contract: who allocates this object, who is allowed to mutate it, and who is responsible for deleting it? Ambiguous ownership leads directly to memory leaks, dangling pointers, double-free crashes, and concurrency races. Modern C++ makes ownership explicit through its type system: std::unique_ptr for exclusive ownership, std::shared_ptr when multiple subsystems co-own a resource, and raw references/pointers T& or T* for non-owning observers.

Core Mental Model

The C++ Ownership Ladder:

  1. Exclusive Ownership (std::unique_ptr<T>): One parent owns the child. The child lives and dies with its owner. Ownership transfers via move semantics (std::move). This is your default choice for 90% of designs (e.g., a Controller owning its Service, a Service owning its Strategy).
  2. Shared Ownership (std::shared_ptr<T>): Multiple components share lifetime via atomic reference counting. The resource is destroyed when the last owner releases it. Use only when lifetime is truly non-deterministic across concurrent threads or shared caches.
  3. Weak Observation (std::weak_ptr<T>): Observes a std::shared_ptr without keeping it alive and breaks reference cycles between collaborating objects (such as parent-child or observer-subject links).
  4. Non-Owning Borrowing (const T& or T*): The caller owns the object and temporarily lends it for the duration of a function call. Zero overhead, zero lifetime coupling.
01

Why this matters in LLD interviews

In garbage-collected runtimes like Java or Go, developers rarely think about who deletes an object. In C++ Low-Level Design, object lifetime is an explicit architectural decision. Every pointer or reference in a class diagram answers a critical contract: who allocates this object, who is allowed to mutate it, and who is responsible for deleting it? Ambiguous ownership leads directly to memory leaks, dangling pointers, double-free crashes, and concurrency races. Modern C++ makes ownership explicit through its type system: std::unique_ptr for exclusive ownership, std::shared_ptr when multiple subsystems co-own a resource, and raw references/pointers T& or T* for non-owning observers.

Mental model

The C++ Ownership Ladder:

  1. Exclusive Ownership (std::unique_ptr<T>): One parent owns the child. The child lives and dies with its owner. Ownership transfers via move semantics (std::move). This is your default choice for 90% of designs (e.g., a Controller owning its Service, a Service owning its Strategy).
  2. Shared Ownership (std::shared_ptr<T>): Multiple components share lifetime via atomic reference counting. The resource is destroyed when the last owner releases it. Use only when lifetime is truly non-deterministic across concurrent threads or shared caches.
  3. Weak Observation (std::weak_ptr<T>): Observes a std::shared_ptr without keeping it alive and breaks reference cycles between collaborating objects (such as parent-child or observer-subject links).
  4. Non-Owning Borrowing (const T& or T*): The caller owns the object and temporarily lends it for the duration of a function call. Zero overhead, zero lifetime coupling.
02

Core concepts

2.1Exclusive Ownership with std::unique_ptr & std::move

In classic object-oriented design, when class A composes class B, A has full responsibility for B's lifetime. In Modern C++, express this with std::unique_ptr<B>. A unique_ptr cannot be copied—only moved. Construct it using std::make_unique<T>(), which ensures exception safety and avoids raw new. Transfer ownership into constructors using std::move. Once moved, the caller's pointer becomes nullptr, preventing accidental use-after-transfer.

C++23 snippet
class OrderService {
    std::unique_ptr<PaymentProcessor> processor_;
public:
    // Takes exclusive ownership via move
    explicit OrderService(std::unique_ptr<PaymentProcessor> processor)
        : processor_(std::move(processor)) {}
};

2.2Shared Ownership and the std::weak_ptr Cycle Breaker

Use std::shared_ptr<T> only when multiple independent entities co-own a resource and neither can predict when the other will finish (e.g. a shared user session across asynchronous worker threads). However, overusing shared_ptr is a design smell: it hides ownership boundaries and introduces atomic increment/decrement overhead. Even worse, if object A holds a shared_ptr to B, and B holds a shared_ptr to A, their reference counts will never hit zero—creating a permanent memory leak. Break cycles by turning one side into a std::weak_ptr<T>, which observes without incrementing the reference count and must be temporarily upgraded using .lock() before access.

C++23 snippet
class AuditObserver {
    std::weak_ptr<UserSession> sessionWeak_; // Does not prevent session destruction
public:
    void log() const {
        if (auto session = sessionWeak_.lock()) {
            // Safe to access session->userId()
        }
    }
};

2.3Non-Owning Observers: Borrowing with T* and T&

A common beginner mistake is passing const std::unique_ptr<T>& or std::shared_ptr<T> to methods that just need to read data. This unnecessarily couples the function signature to the caller's memory management strategy. Pass a raw reference T& or pointer T* for non-owning borrowing. A raw pointer in modern C++ means: 'I am borrowing this object for the scope of this call; I do not own it, and I will not delete it.'

C++23 snippet
// Clean borrowing: accepts any logger, regardless of how caller allocates it
bool checkout(const Order& order, const ILogger& logger) {
    logger.log(std::format("Order {}", order.orderId));
    return processor_->process(order.amountRupees);
}

2.4The Rule of 5 and Deleting Copy Operations (= delete)

Polymorphic resource owners should never be copied implicitly. Accidental copying can cause object slicing (copying only the base subobject) or duplicate handles to external hardware and sockets. In C++20, explicitly mark copy operations as = delete on your service and manager classes, while defaulting move operations if transfer of the entire manager is permitted.

C++23 snippet
class OrderService {
public:
    // Prevent accidental slicing and duplicated ownership
    OrderService(const OrderService&) = delete;
    OrderService& operator=(const OrderService&) = delete;

    OrderService(OrderService&&) noexcept = default;
    OrderService& operator=(OrderService&&) noexcept = default;
};
03

UML class structure

OrderService exclusively owns PaymentProcessor via unique_ptr (composes ◆──▶). It shares UserSession via shared_ptr (aggregates ◇──▶), while AuditObserver holds a weak_ptr observer. ILogger is borrowed as a non-owning reference (depends ╌╌▶).

OrderService
Exclusive owner of processor; shares session
◆ composesPaymentProcessor◇ aggregatesUserSession⇢ depends onILogger
  • - processor_: unique_ptr<PaymentProcessor>
  • - session_: shared_ptr<UserSession>
  • + checkout(order: Order, logger: ILogger&): bool
  • + session(): shared_ptr<UserSession>
«interface»
PaymentProcessor
Strategy contract
  • + ~PaymentProcessor() = default
  • + process(amountRupees: int): bool = 0
Used by:OrderService(◆)UpiProcessor(△)
UpiProcessor
△ implementsPaymentProcessor
  • - vpa_: string
  • + process(amountRupees: int): bool override
  • + vpa(): string
UserSession
  • - userId_: string
  • + userId(): string
Used by:OrderService(◇)AuditObserver(⇢)
AuditObserver
Weak observer breaking lifetime cycle
⇢ depends onUserSession
  • - sessionWeak_: weak_ptr<UserSession>
  • + auditEvent(event: string_view): void
  • + isSessionAlive(): bool
«interface»
ILogger
Non-owning borrowed observer
  • + ~ILogger() = default
  • + log(msg: string_view): void = 0
Used by:OrderService(⇢)
Relationships (UML arrows)
OrderService◆──▶PaymentProcessorowns unique_ptr
UpiProcessor╌╌▷PaymentProcessorimplements
OrderService◇──▶UserSessionshares shared_ptr
AuditObserver╌╌▶UserSessionobserves weak_ptr
OrderService╌╌▶ILoggerborrows non-owning ref
04

Runnable C++23 example

What the demo shows
  1. 1.

    Exclusive ownership via std::make_unique: We create a UpiProcessor on the heap and pass it into OrderService using std::move. In the caller, upi becomes nullptr, demonstrating that sole ownership transferred into OrderService.

  2. 2.

    Shared ownership with atomic tracking: UserSession is wrapped in std::shared_ptr. When shared with OrderService, the reference count increments from 1 to 2. main drops its handle with .reset(), leaving OrderService as the lone owner with use_count == 1.

  3. 3.

    Weak observation without extending lifetime: AuditObserver holds a std::weak_ptr<UserSession>. It audits events by calling .lock() to obtain a temporary shared_ptr. Once OrderService goes out of scope, the session is immediately destroyed; subsequent calls to auditor.auditEvent gracefully detect the expired session instead of accessing dangling memory.

  4. 4.

    Non-owning borrowing: ConsoleLogger implements ILogger on the stack. OrderService::checkout borrows it by const ILogger&, decoupling the business logic from how the logger's memory is allocated.

Foundation Demo · demo.cpp
#include <iostream>
#include <memory>
#include <print>
#include <string>
#include <string_view>
#include <utility>

// Non-owning observer interface
class ILogger {
public:
    virtual ~ILogger() = default;
    virtual void log(std::string_view msg) const = 0;
};

class ConsoleLogger final : public ILogger {
public:
    void log(std::string_view msg) const override {
        std::println("[LOG] {}", msg);
    }
};

// Pure virtual interface for payment strategy
class PaymentProcessor {
public:
    virtual ~PaymentProcessor() = default;
    virtual bool process(int amountRupees) = 0;
};

class UpiProcessor final : public PaymentProcessor {
    std::string vpa_;
public:
    explicit UpiProcessor(std::string vpa) : vpa_(std::move(vpa)) {}
    bool process(int amountRupees) override {
        std::println("[UPI] Debited Rs.{} via {}", amountRupees, vpa_);
        return true;
    }
    [[nodiscard]] const std::string& vpa() const noexcept { return vpa_; }
};

// Shared domain entity: UserSession
class UserSession {
    std::string userId_;
public:
    explicit UserSession(std::string userId) : userId_(std::move(userId)) {
        std::println("UserSession created for {}", userId_);
    }
    ~UserSession() {
        std::println("UserSession destroyed for {}", userId_);
    }
    [[nodiscard]] const std::string& userId() const noexcept { return userId_; }
};

// Audit observer holding weak_ptr to prevent cycle and avoid extending lifetime
class AuditObserver {
    std::weak_ptr<UserSession> sessionWeak_;
public:
    explicit AuditObserver(std::weak_ptr<UserSession> session)
        : sessionWeak_(std::move(session)) {}

    void auditEvent(std::string_view event) const {
        if (auto session = sessionWeak_.lock()) {
            std::println("[AUDIT] User '{}' performed: {}", session->userId(), event);
        } else {
            std::println("[AUDIT] (Session expired) Unauthenticated event: {}", event);
        }
    }

    [[nodiscard]] bool isSessionAlive() const noexcept {
        return !sessionWeak_.expired();
    }
};

// Order domain entity
struct Order {
    std::string orderId;
    int amountRupees;
};

// OrderService:
// 1. Exclusively owns PaymentProcessor via std::unique_ptr
// 2. Shares UserSession via std::shared_ptr
// 3. Borrows ILogger via non-owning reference ILogger&
// 4. Copies are explicitly deleted (= delete)
class OrderService {
    std::unique_ptr<PaymentProcessor> processor_;
    std::shared_ptr<UserSession> session_;

public:
    OrderService(std::unique_ptr<PaymentProcessor> processor,
                 std::shared_ptr<UserSession> session)
        : processor_(std::move(processor)), session_(std::move(session)) {}

    // Rule of 5: Prevent accidental slicing and duplicated ownership
    OrderService(const OrderService&) = delete;
    OrderService& operator=(const OrderService&) = delete;

    OrderService(OrderService&&) noexcept = default;
    OrderService& operator=(OrderService&&) noexcept = default;

    bool checkout(const Order& order, const ILogger& logger) {
        logger.log(std::format("Checking out order {} for Rs.{}", order.orderId, order.amountRupees));
        if (!processor_) {
            logger.log("Error: No payment processor attached");
            return false;
        }
        return processor_->process(order.amountRupees);
    }

    [[nodiscard]] const std::shared_ptr<UserSession>& session() const noexcept {
        return session_;
    }

    [[nodiscard]] bool hasProcessor() const noexcept {
        return processor_ != nullptr;
    }
};

int main() {
    ConsoleLogger logger;

    // 1. Exclusive ownership: std::make_unique transferred via std::move
    auto upi = std::make_unique<UpiProcessor>("naman@oksbi");

    // 2. Shared ownership: session shared between OrderService and AuditObserver
    auto session = std::make_shared<UserSession>("USR-404");
    AuditObserver auditor(session);

    std::println("Initial session use_count: {}", session.use_count());

    // 3. Move unique_ptr into OrderService (ownership transfer)
    OrderService service(std::move(upi), session);
    std::println("Session use_count after service creation: {}", session.use_count());

    // 4. Execute checkout borrowing logger as non-owning observer
    Order order{"ORD-901", 1250};
    service.checkout(order, logger);
    auditor.auditEvent("PaymentCompleted");

    // 5. Release main's shared ownership
    session.reset();
    std::println("Session use_count after main reset: {}", service.session().use_count());

    // 6. Demonstrate weak_ptr expiration when all shared_ptrs are destroyed
    {
        OrderService tempService = std::move(service);
        // tempService goes out of scope here
    }

    std::println("Checking auditor after session destruction:");
    auditor.auditEvent("PostLogoutAction");

    return 0;
}
Program output (stdout)
UserSession created for USR-404
Initial session use_count: 1
Session use_count after service creation: 2
[LOG] Checking out order ORD-901 for Rs.1250
[UPI] Debited Rs.1250 via naman@oksbi
[AUDIT] User 'USR-404' performed: PaymentCompleted
Session use_count after main reset: 1
UserSession destroyed for USR-404
Checking auditor after session destruction:
[AUDIT] (Session expired) Unauthenticated event: PostLogoutAction
05

Modern C++ note

Deleted Copy Operations and std::make_uniqueC++20
What it replaces

Manual new/delete pairs, and C++98 un-implemented private copy constructors.

Why it is better

Compile-time error diagnostics are immediate and obvious when an engineer attempts to copy a non-copyable resource owner. Combining = delete with std::make_unique guarantees exception safety and prevents object slicing across polymorphic interfaces.

When classic virtual wins

Custom memory allocators (e.g. fixed-size pool or arena allocators in high-frequency trading) or interfacing with C APIs requiring raw pointer handles and custom free callbacks.

06

Rules of thumb

  • Default to std::unique_ptr for composition: Represent 1-to-1 or 1-to-N parent-child relationships with exclusive ownership. If a class creates or manages a collaborator, own it via unique_ptr.

  • Reserve std::shared_ptr for truly shared lifecycles: If two objects have clear parent-child or caller-callee relationships, do not use shared_ptr. Only use shared ownership for async task graphs, cache nodes, or multi-threaded pipelines.

  • Pass raw references T& or pointers T* to observe: Never write void process(const std::unique_ptr<T>& item). Write void process(const T& item). This lets callers pass stack objects, vector elements, or smart pointers without coupling to the storage mechanism.

  • Break circular references with std::weak_ptr: In bidirectional relationships (e.g., Parent ↔ Child, Publisher ↔ Subscriber), the back-pointer must be a weak_ptr or raw reference; otherwise, neither object's destructor will ever run.

  • Delete copy constructors on polymorphic types: Always declare ClassName(const ClassName&) = delete; on polymorphic base classes and service owners to catch accidental slicing bugs at compile time.

07

Don't make this mistake

Circular reference memory leaks with std::shared_ptr
✗ class Node { std::shared_ptr<Node> next_; std::shared_ptr<Node> prev_; // Cycle: neither node will ever be deleted! };
✓ class Node { std::shared_ptr<Node> next_; std::weak_ptr<Node> prev_; // Weak back-pointer breaks the cycle };

When two objects point to each other with std::shared_ptr, their reference counts never drop to zero even after all outside references are destroyed. The entire graph leaks permanently.

Passing std::unique_ptr by value when not sinking ownership
✗ void displayOrder(std::unique_ptr<Order> order) { // Steals and destroys order! std::println("ID: {}", order->id); }
✓ void displayOrder(const Order& order) { // Borrows non-owning view std::println("ID: {}", order.id); }

Passing std::unique_ptr by value forces the caller to call std::move(ptr), emptying their pointer. At the end of displayOrder, the object is destroyed. Only pass unique_ptr by value when the function's explicit purpose is to consume or store ownership.

Using raw new instead of std::make_unique
✗ std::unique_ptr<PaymentProcessor> p(new UpiProcessor("naman@oksbi"));
✓ auto p = std::make_unique<UpiProcessor>("naman@oksbi");

std::make_unique avoids spelling the type twice, is exception-safe if multiple arguments are evaluated before constructor invocation, and avoids naked new keywords in application code.

08

Quick interview check

09

Mark your progress

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers