C++ Interview Weekly (7): Implementation Principles of unique_ptr and shared_ptr

In C++, manual resource management (<span>new/delete</span>) is prone to errors, leading to:

Memory leaksDouble freeException safety issues

To address these problems, C++11 introduced smart pointers:

<span>std::unique_ptr</span>: Exclusive ownership<span>std::shared_ptr</span>: Shared ownership, managed through reference counting

We will analyze from three dimensions: principles, performance, and application scenarios.

1. Technical Background and Design Intent

1unique_ptr

Goal: Exclusive resource ownership, non-copyable, can only be moved (move)

Usage scenario: Unique ownership of resources, RAII style automatic release

Core issues:

Avoid memory leaksSupport move semantics1shared_ptr

Goal: Multiple parties share resources, reference counting controls lifecycle

Usage scenario: Multi-threaded shared objects, cache management

Core issues:

Automatic lifecycle managementHandle reference counting in multi-threaded environments (atomic)Avoid circular reference issues

2. Implementation Principles of unique_ptr

1. Internal Structure

template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
    T* ptr; // Raw pointer
    Deleter deleter; // Deleter
public:
    explicit unique_ptr(T* p = nullptr) noexcept : ptr(p) {}
    ~unique_ptr() { if(ptr) deleter(ptr); }

    // Non-copyable
    unique_ptr(const unique_ptr&amp;) = delete;
    unique_ptr&amp; operator=(const unique_ptr&amp;) = delete;

    // Movable
    unique_ptr(unique_ptr&amp;&amp; u) noexcept : ptr(u.ptr) { u.ptr = nullptr; }
    unique_ptr&amp; operator=(unique_ptr&amp;&amp; u) noexcept {
        if(this != &amp;u) {
            reset(u.ptr);
            u.ptr = nullptr;
        }
        return *this;
    }

    T* get() const noexcept { return ptr; }
    T&amp; operator*() const noexcept { return *ptr; }
    T* operator->() const noexcept { return ptr; }

    void reset(T* p = nullptr) {
        if(ptr) deleter(ptr);
        ptr = p;
    }
};

2. Core Principles

Exclusive ownership: The unique pointer owns the resource, copying is disabledMove semantics: Resource transfer is achieved through movingRAII: Automatic release at the end of the lifecycle

3. Performance Characteristics

Small memory footprint (only stores raw pointer + deleter)No additional reference counting overheadLow compile-time overheadException safe, avoids leaks

3. Implementation Principles of shared_ptr

1. Internal Structure

The core of shared_ptr lies in reference counting and control block.

shared_ptr<T>
├── T* ptr           // Actual object pointer
└── ControlBlock* cb // Reference count and deleter

Example of control block structure:

struct ControlBlock {
    std::atomic<size_t> use_count;   // Strong reference count
    std::atomic<size_t> weak_count;  // Weak reference count
    Deleter deleter;                 // Deleter
    T* ptr;                          // Pointer to object
};

2. Core Logic

Constructing shared_ptr: Initializes <span>use_count = 1</span>Copying shared_ptr: <span>use_count++</span> (atomic operation)Destructing shared_ptr: <span>use_count--</span>, if it is 0, call deleter to destroy the objectweak_ptr: Does not increase <span>use_count</span>, only increases <span>weak_count</span>, used to resolve circular references

3. Key Implementation Details

Thread safety: <span>std::atomic</span> ensures reference counting is correct in multi-threading

Control block separation:

Object pointer and control block are separated, allowing support for <span>make_shared</span> inline allocation (reducing memory fragmentation)

Circular reference issues:

Two shared_ptr referencing each other will cause the reference count not to be 0Requires weak_ptr to resolve

4. Performance Analysis

Feature unique_ptr shared_ptr
Memory usage Small (pointer + deleter) Additional overhead for control block, atomic counting
Copy/Move overhead Copy is prohibited, move overhead is small Copy requires atomic operation, move overhead is small
Multi-thread safety Depends on external protection Atomic operations on reference counting ensure safety
Circular reference risk None Exists, requires weak_ptr
Applicable scenarios Exclusive resources, RAII Multi-party shared object management, caching, asynchronous tasks

5. Applications in Distributed Systems

1unique_ptrIn Ceph’s object cache (ObjectCacher), each cache entry exclusively owns a memory blockTiDB Executor manages local temporary objects1shared_ptrCeph multi-threaded I/O callback objects are sharedTiDB RPC/asynchronous tasks share context

The core idea: Use unique_ptr whenever possible to avoid atomic overhead; use shared_ptr only when multiple parties need to share, and combine with weak_ptr to avoid circular references.

6. Conclusion

unique_ptr

Exclusive resource ownership, low overhead, RAII automatic releaseOwnership transfer achieved through move semantics

shared_ptr

Shared resources, managed through reference counting + control blocklifecycleSupports multi-threading, but with performance overheadCircular references need to be resolved with weak_ptr

In Distributed Systems

Use unique_ptr for hotspot objectsUse shared_ptr for asynchronous callbacks and shared context

In summary:“Use unique_ptr for unique ownership, use shared_ptr when sharing is necessary, and controlling reference counting is controlling your performance.”

Leave a Comment