unique_ptr - A custom implementation

C++
Author

Quasar

Published

April 21, 2025

Introduction

In this post, I try to write a simple homegrown version of std::unique_ptr<T>. This post is partly inspired by the fantastic book C++ Memory Management by Patrice Roy. Tghe toy examples in this book are very instructive and I highly reckon you order a copy. Our goal is just to build intuition for the kind of code required to write such a type, and not to try and replace the standard library facilities.

The std::unique_ptr<T> smart pointer type models unqiue(sole) ownership of the resource semantics.

struct X{};

std::unique_ptr<X> p1 = std::make_unique<X>();  
//std::unique_ptr<X> p2(p1);      // Error when calling copy constructor, 
                                  // p1 is the exclusive owner

std::unique_ptr enforces exclusive ownership using the fact, that it is not copy-constructible or copy-assignable. Note however, that it doesn’t prevent you from writing deliberately hostile code. The below code is compiles perfectly well and is valid C++.

int* p = new int(10);

std::unique_ptr<int> p1(p);  
std::unique_ptr<int> p2(p);      

The copy constructor and the copy assignment operator of std::unique_ptr<T> are marked delete. It is however, move constructible and move-assignable.

Basic functionalities to expect out of std::unique_ptr<T>

I skimmed through the documentation for std::unique_ptr on cppreference.com. A basic implementation of unique_ptr in less than 200 lines of code should pass the following unit tests:

#include <gtest/gtest.h>
#include "unique_ptr.h"

TEST(UniquePtrTest, CreateAndAccessTest)
{
    int* raw_ptr = new int(42);
    dev::unique_ptr<int> p1(raw_ptr);
    
    EXPECT_EQ(*p1 == 42,true);
    EXPECT_EQ(p1.get(), raw_ptr);

    dev::unique_ptr<int> p2 = new int(17);
    EXPECT_EQ(*p2 == 17,true);
    EXPECT_EQ(p2.get() != nullptr, true);
}

/* Move constructor - Transfer of ownership */
TEST(UniquePtrTest, MoveConstructorTest)
{
    dev::unique_ptr p {dev::unique_ptr(new int(17))};

    EXPECT_EQ(*p, 17);
    EXPECT_EQ(p!=nullptr, true);
}

/* Move assignment */
TEST(UniquePtrTest, MoveAssignmentTest)
{
    dev::unique_ptr<int> p1(new int(42));
    p1 = dev::unique_ptr<int>(new int(17));

    EXPECT_EQ(p1!=nullptr, true);
    EXPECT_EQ(*p1 == 17, true);
}

// Modifiers
/* release() : Returns the pointer to resource and releases ownership*/
TEST(UniquePtrTest, ReleaseTest){
    dev::unique_ptr<double> ptr(new double(3.14));
    double* rawPtr = ptr.release();

    EXPECT_EQ(ptr == nullptr, true);
    EXPECT_EQ(rawPtr != nullptr, true);
    EXPECT_EQ(*rawPtr == 3.14, true);
}

/* reset() :  replaces the managed object */
TEST(UniquePtrTest, ResetUniquePtr) {
    dev::unique_ptr<int> ptr(new int(10));
    ptr.reset(new int(20));
    EXPECT_EQ(ptr != nullptr, true);
    EXPECT_EQ(*ptr == 20, true);

    // Self-reset test
    ptr.reset(ptr.get());
}

/* swap() : swap the managed objects */
TEST(UniquePtrTest, SwapTest){
    int* first = new int(42);
    int* second = new int(17);

    dev::unique_ptr<int> p1(first);
    dev::unique_ptr<int> p2(second);

    swap(p1, p2);

    EXPECT_EQ(p2.get() == first && p1.get() == second, true);
    EXPECT_EQ(((*p1) == 17) && ((*p2) == 42), true);
}

// Observers
/* get() : Returns a pointer to the managed object or nullptr*/
TEST(UniquePtrTest, GetTest){
    double* resource = new double(0.50);
    dev::unique_ptr p(resource);

    EXPECT_EQ(p.get() == resource, true);
    EXPECT_EQ(*(p.get()) == 0.50, true);
}

/* operator bool() : Checks whether *this owns an object*/
/*TEST(UniquePtrTest, OperatorBoolTest){
    int* resource = new int(28);
    dev::unique_ptr<int> p1;
    dev::unique_ptr<int> p2(resource);

    EXPECT_EQ(p1, false);
    EXPECT_EQ(p2, true);
}*/

// Pointer-like functions
TEST(UniquePtrTest, IndirectionOperatorTest) {
    /* indirection operator* to dereference pointer to managed object,
       member access operator -> to call member function*/
    struct X {
        int _n;

        X() = default;
        X(int n) : _n{n} {}
        ~X() = default;
        int foo() { return _n; }
    };

    dev::unique_ptr<X> ptr(new X(10));
    EXPECT_EQ((*ptr)._n == 10, true);
    EXPECT_EQ(ptr->foo() == 10, true);
}

TEST(UniquePtrTest, PointerToArrayOfTConstructionAndAccess){
    /* Constructing unique_ptr<T[]> and access */
    dev::unique_ptr<int[]> p;
    EXPECT_EQ(p == nullptr, true);
    {
        p = new int[5]{1, 2, 3, 4, 5};
        EXPECT_EQ(p !=nullptr, true);
        EXPECT_EQ(*p == 1, true);
        EXPECT_EQ(p[2] == 3, true);
        p.release();
        EXPECT_EQ(p ==nullptr, true);
    }
}

A custom implementation

Most of the code is self-evident. The special case, where the resource(pointee) is an array of elements of type T, we write a partial specialization of unique_ptr<T[],D>.

#include<format>
#include<concepts>

namespace dev{

    /* Default deleter - single object version */
    template<typename T>
    struct default_deleter{
        void operator()(T* raw_ptr){
            delete raw_ptr;
        }
    };

    /* Default deleter - pointee is an array of objects version */
    template<typename T>
    struct default_deleter<T[]>{
        void operator()(T* raw_ptr){
            delete[] raw_ptr;
        }
    };

    /* Single object version */
    template<typename T, typename D = default_deleter<T>>
    class unique_ptr : public D{
        public:
        using deleter_type = D;

        /* Default c'tor */
        unique_ptr() = default;

        unique_ptr(const unique_ptr& ) = delete;
        unique_ptr& operator=(const unique_ptr& ) = delete;

        /* Parameteric constructor */
        unique_ptr(T* ptr) : m_underlying_ptr{ptr}
        {}

        /* Swap function */
        void swap(unique_ptr& other) noexcept{
            std::swap(m_underlying_ptr, other.m_underlying_ptr);
        }

        /* Move constructor */
        unique_ptr(unique_ptr&& other)
        : m_underlying_ptr{ std::move(other.m_underlying_ptr)}
        {}

        /* Move assignment */
        unique_ptr& operator=(unique_ptr&& other){
            std::swap(m_underlying_ptr, other.m_underlying_ptr);
            return *this;
        }

        /* Destructor */
        ~unique_ptr(){
            deleter_type* deleter = static_cast<deleter_type*>(this);
            (*deleter)(m_underlying_ptr);
        }

        // Pointer-like functions
        /* Dereferencing operator */
        T operator*(){
            return *m_underlying_ptr;
        }

        /* Indirection operator*/
        T* operator->(){
            return m_underlying_ptr;
        }

        /* get() - get the raw underlying pointer*/
        T* get() const{
            return m_underlying_ptr;
        }

        // Modifiers
        /* Release - returns a pointer to the managed object
           and releases the ownership*/
        T* release(){
            return std::exchange(m_underlying_ptr, nullptr);
        }

        /* Reset - Replaces the managed object */
        void reset(T* other){
            if(m_underlying_ptr != other)
            {
                deleter_type* deleter = static_cast<deleter_type*>(this);
                (*deleter)(m_underlying_ptr);
    
                m_underlying_ptr = other;
            }
        }

        void reset(std::nullptr_t){
            deleter_type* deleter = static_cast<deleter_type*>(this);
            (*deleter)(m_underlying_ptr);
            m_underlying_ptr = nullptr;
        }

        explicit operator bool() const{
            return (m_underlying_ptr == nullptr);
        }

        friend bool operator==(const unique_ptr& lhs, const unique_ptr& rhs){
            return (lhs.get() == rhs.get());
        }

        friend bool operator==(const unique_ptr& lhs, std::nullptr_t rhs){
            return (lhs.m_underlying_ptr == nullptr);
        }

        friend bool operator!=(unique_ptr& lhs, unique_ptr& rhs){
            return !(lhs == rhs);
        }

        friend bool operator!=(unique_ptr& lhs, std::nullptr_t){
            return !(lhs == nullptr);
        }

        friend void swap(unique_ptr& lhs, unique_ptr& rhs){
            lhs.swap(rhs);
        }

        private:
        T* m_underlying_ptr{nullptr};
    };

    /* Array version*/
    template<typename T, typename D>
    class unique_ptr<T[], D> : public D{
        public:
        using deleter_type = D;

        /* Default c'tor */
        unique_ptr() = default;

        unique_ptr(const unique_ptr& ) = delete;
        unique_ptr& operator=(const unique_ptr& ) = delete;

        /* Parameteric constructor */
        unique_ptr(T* ptr) : m_underlying_ptr{ptr}
        {}

        /* Swap function */
        void swap(unique_ptr& other) noexcept{
            std::swap(m_underlying_ptr, other.m_underlying_ptr);
        }

        /* Move constructor */
        unique_ptr(unique_ptr&& other)
        : m_underlying_ptr{ std::move(other.m_underlying_ptr)}
        {}

        /* Move assignment */
        unique_ptr& operator=(unique_ptr&& other){
            std::swap(m_underlying_ptr, other.m_underlying_ptr);
            return *this;
        }

        /* Destructor */
        ~unique_ptr(){
            deleter_type* deleter = static_cast<deleter_type*>(this);
            (*deleter)(m_underlying_ptr);
        }

        // Pointer-like functions
        /* Dereferencing operator */
        T operator*(){
            return *m_underlying_ptr;
        }

        /* Indirection operator*/
        T* operator->(){
            return m_underlying_ptr;
        }

        /* IndexOf operator - provides indexed access 
           to the managed array.*/
        T operator[](std::size_t index){
            return m_underlying_ptr[index];
        }

        /* get() - get the raw underlying pointer*/
        T* get() const{
            return m_underlying_ptr;
        }

        // Modifiers
        /* Release - returns a pointer to the managed object
           and releases the ownership*/
        T* release(){
            return std::exchange(m_underlying_ptr, nullptr);
        }

        /* Reset - Replaces the managed object */
        void reset(T* other){
            if(m_underlying_ptr != other)
            {
                deleter_type* deleter = static_cast<deleter_type*>(this);
                (*deleter)(m_underlying_ptr);
    
                m_underlying_ptr = other;
            }
        }

        void reset(std::nullptr_t){
            deleter_type* deleter = static_cast<deleter_type*>(this);
            (*deleter)(m_underlying_ptr);
            m_underlying_ptr = nullptr;
        }

        explicit operator bool() const{
            return (m_underlying_ptr == nullptr);
        }

        friend bool operator==(const unique_ptr& lhs, const unique_ptr& rhs){
            return (lhs.get() == rhs.get());
        }

        friend bool operator==(const unique_ptr& lhs, std::nullptr_t rhs){
            return (lhs.m_underlying_ptr == nullptr);
        }

        friend bool operator!=(unique_ptr& lhs, unique_ptr& rhs){
            return !(lhs == rhs);
        }

        friend bool operator!=(unique_ptr& lhs, std::nullptr_t){
            return !(lhs == nullptr);
        }

        friend void swap(unique_ptr& lhs, unique_ptr& rhs){
            lhs.swap(rhs);
        }

        private:
        T* m_underlying_ptr{nullptr};
    };
}

You can play around the code files, build the project and run unit tests for this (naive) toy-implementation of unique_ptr by cloning my GitHub repo github.com/quasar-chunawala/interview_data_structures.

References