DEV Community

Quasar Chunawala
Quasar Chunawala

Posted on

unique_ptr<T> - A minimal implementation

Introduction

Smart pointers are a fancy wrapper over raw pointers. Smart pointers are RAII compliant. RAII(Resource Acquisition Is Initialization) is a design paradigm and its a concept very emblematic in C++. It's a simple idea - all resources used by a class should be acquired in the constructor. All resources used by a class must be released in the destructor! It turns out that by abiding by the RAII policy, we avoid half-valid states.

The std::unique_ptr in the <memory> header is a smart pointer that excusively owns and manages the objects created dynamically on the heap through an underlying raw pointer. It subsequently disposes off the object, when it goes out of scope and is destroyed.

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
Enter fullscreen mode Exit fullscreen mode

std::unique_ptr enforces exclusive ownership using the fact, that it is not copy-constructible or copy-assignable. Note however, that the below code compiles successfully and is valid code.

int* p = new int(10);

std::unique_ptr<int> p1(p);  
std::unique_ptr<int> p2(p);      
Enter fullscreen mode Exit fullscreen mode

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:

int main(){
    /* Test Cases*/

    {
        /* Non member function operator== 
           Comparison with another unique_ptr or nullptr 
        dev::unique_ptr<int> ptr1(new int(10));
        dev::unique_ptr<int> ptr2(new int(10));
        assert(ptr1 == ptr1);   
        assert(!(ptr1 == ptr2));
        */ 
    }

    {
        /* Create and access 
        dev::unique_ptr<int> ptr(new int(10));
        assert(!(ptr == nullptr));
        assert(*ptr == 10);
        */
    }

    {
        /* Reset unique_ptr 
           Replaces the managed object with a new one
        dev::unique_ptr<int> ptr(new int(10));
        ptr.reset(new int(20));
        assert(!(ptr == nullptr));
        assert(*ptr == 20);
        */
    }

    {
        /* Release unique_ptr ownership - 
           Returns a pointer to the managed object and releases ownership 
        dev::unique_ptr<double> ptr(new double(3.14));
        double* rawPtr = ptr.release();
        assert(ptr == nullptr);
        assert(rawPtr != nullptr);
        assert(*rawPtr == 3.14);
        */
    }

    {
        /* Non-member function swap
           Swap the managed objects
        dev::unique_ptr<int> ptr1(new int(10));
        dev::unique_ptr<int> ptr2(new int (20));
        swap(ptr1, ptr2);
        assert(*ptr1 == 20);
        assert(*ptr2 == 10);
         */
    }

    {
        /* Get the raw underlying pointer 
        int* x = new int(10);
        dev::unique_ptr<int> ptr(x);
        int* rawPtr = ptr.get();
        assert(*rawPtr == 10);
        assert(rawPtr == x);
        */
    }

    {
        /* operator bool to test if the unique pointer owns an object 
        dev::unique_ptr<int> ptr(new int(42));
        assert(ptr);
        ptr.reset(nullptr);
        assert(!ptr);
        */

    {
        /* indirection operator* to dereference pointer to managed object,
           member access operator -> to call member function
        struct X{
            int n;
            int foo(){ return n; }
        };


        dev::unique_ptr<X> ptr(new X(10));
        assert((*ptr).n == 10);
        assert(ptr->foo() == 10);
        */ 
    }

    {
        /* Constructs an object of type T and wraps it in a unique_ptr.
        struct Point3D{
            double x, y, z;

            Point3D() {}

            Point3D(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {}

            Point3D(const Point3D& other) : x(other.x), y(other.y), z(other.z) {}

            Point3D(Point3D&& other) 
            : x(std::move(other.x))
            , y(std::move(other.y))
            , z(std::move(other.z))
            {}
        };

        // Use the default constructor. 
        dev::unique_ptr<Point3D> v1 = dev::make_unique<Point3D>();
        // Use the constructor that matches these arguments.
        dev::unique_ptr<Point3D> v2 = dev::make_unique<Point3D>(0, 1, 2);
        // Create a unique_ptr to a vector of 5 elements.
        //dev::unique_ptr<std::vector<int>> v3 = dev::make_unique<std::vector<int>>({1,2,3,4,5});
        */

    }
}
Enter fullscreen mode Exit fullscreen mode

A custom implementation

#include <iostream>
#include <memory>
#include <cassert>
#include <utility>
#include <vector>

namespace dev{
    // Implement unique_ptr here
    template<typename T>
    class unique_ptr{
        public:
        // Default c'tor 
        unique_ptr() : ptr{nullptr} {}

        // Copy c'tor should be deleted to enforce the concept that this is 
        // an owning pointer
        unique_ptr(const unique_ptr& u) = delete;

        // Copy assignment should also be deleted to enforce the ownership of the 
        // managed object
        unique_ptr& operator=(const unique_ptr& ) = delete;

        // Move constructor
        unique_ptr(unique_ptr&& other) : ptr{nullptr}
        {
            std::swap(ptr, other.ptr);
        }

        // Move assignment operator
        unique_ptr& operator=(unique_ptr&& other){
            if(ptr == other)
                return (*this);

            delete_underlying_ptr();
            ptr = std::exchange(other.ptr,nullptr);
            return (*this);
        }

        // Parameterized construtor
        unique_ptr(T* p) 
        : ptr{p} 
        {}

        // Overload deferencing operator *
        T& operator*(){
            return (*ptr);
        }

        // get the raw pointer
        T* get()
        {
            return ptr;
        }

        /* Reset the unique_ptr */
        void reset(T* other){
            if(ptr == other)
                return;

            delete_underlying_ptr();
            std::swap(ptr, other);
        }

        /* Release unique_ptr ownership */
        T* release(){
            return std::exchange(ptr,nullptr);
        }

        /* Destructor */
        ~unique_ptr(){
            delete_underlying_ptr();
        }

        /* swap - Exchange the contents of ptr1 and ptr2 member-by-member */
        friend void swap(dev::unique_ptr<T>& ptr1, dev::unique_ptr<T>& ptr2) noexcept
        {
            //Re-wire the raw pointers
            std::swap(ptr1.ptr, ptr2.ptr);
        }

        /* operator bool */
        constexpr operator bool(){
            return (ptr != nullptr);
        }

        /* Member access operator - return the underlying pointer */
        T* operator->(){
            return ptr;
        }

        /* Helper function to invoke delete on the underlying raw pointer */
        void delete_underlying_ptr(){
            if(ptr != nullptr){
                delete ptr;
                ptr = nullptr;
            }
        }
        private:
        T* ptr;
    };

    template<typename T, typename... Args>
    T* make_unique(Args&&... args){
        return new T(std::forward<Args>(args)...);
    }
}

/* Non-member functions */
//Overload operator==
template<typename T1, typename T2>
bool operator==(dev::unique_ptr<T1>& lhs, dev::unique_ptr<T2>& rhs){
    return lhs.get() == rhs.get();
}

/*template<typename T1> */
template<typename T1>
bool operator==(dev::unique_ptr<T1>& lhs, std::nullptr_t rhs){
    return lhs.get() == nullptr;
}

//Overload operator!=
template<typename T1, typename T2>
bool operator!=(dev::unique_ptr<T1>& lhs, dev::unique_ptr<T2>& rhs){
    return !(lhs == rhs);
}

template<typename T1>
bool operator!=(dev::unique_ptr<T1>& lhs, std::nullopt_t& rhs){
    return !(lhs == rhs);
}

int main(){
    /* Test Cases*/

    {
        /* Non member function operator== 
           Comparison with another unique_ptr or nullptr */ 
        dev::unique_ptr<int> ptr1(new int(10));
        dev::unique_ptr<int> ptr2(new int(10));
        assert(ptr1 == ptr1);   
        assert(ptr1 != ptr2);
    }

    {
        /* Create and access */
        dev::unique_ptr<int> ptr(new int(10));
        assert(ptr != nullptr);
        assert(*ptr == 10);

    }

    {
        /* Reset unique_ptr 
           Replaces the managed object with a new one*/
        dev::unique_ptr<int> ptr(new int(10));
        ptr.reset(new int(20));
        assert(ptr != nullptr);
        assert(*ptr == 20);

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

    {
        /* Release unique_ptr ownership - 
           Returns a pointer to the managed object and releases ownership */
        dev::unique_ptr<double> ptr(new double(3.14));
        double* rawPtr = ptr.release();
        assert(ptr == nullptr);
        assert(rawPtr != nullptr);
        assert(*rawPtr == 3.14);

    }

    {
        /* Non-member function swap
           Swap the managed objects */
        dev::unique_ptr<int> ptr1(new int(10));
        dev::unique_ptr<int> ptr2(new int (20));
        swap(ptr1, ptr2);
        assert(*ptr1 == 20);
        assert(*ptr2 == 10);

    }

    {
        /* Get the raw underlying pointer */
        int* x = new int(10);
        dev::unique_ptr<int> ptr(x);
        int* rawPtr = ptr.get();
        assert(*rawPtr == 10);
        assert(rawPtr == x);

    }

    {
        /* operator bool to test if the unique pointer owns an object */
        dev::unique_ptr<int> ptr(new int(42));
        assert(ptr);
        ptr.reset(nullptr);
        assert(!ptr);

    }

    {
        /* indirection operator* to dereference pointer to managed object,
           member access operator -> to call member function*/ 
        struct X{
            int n;
            int foo(){ return n; }
        };

        dev::unique_ptr<X> ptr(new X(10));
        assert((*ptr).n == 10);
        assert(ptr->foo() == 10);

    }

    {
        /* operator[] is used to access a managed array

        const int size = 5; 
        dev::unique_ptr<int[]> fact(new int[size]);

        for (int i = 0; i < size; ++i)
            fact[i] = (i == 0) ? 1 : i * fact[i - 1];

        assert(fact[0] == 1);
        assert(fact[1] == 2);
        assert(fact[3] == 6);
        assert(fact[4] == 24);
        assert(fact[5] == 120);
        */
    }

    {
        /* Constructs an object of type T and wraps it in a unique_ptr.*/
        struct Point3D{
            double x, y, z;

            Point3D() {}

            Point3D(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {}

            Point3D(const Point3D& other) : x(other.x), y(other.y), z(other.z) {}

            Point3D(Point3D&& other) 
            : x(std::move(other.x))
            , y(std::move(other.y))
            , z(std::move(other.z))
            {}
        };

        // Use the default constructor. 
        dev::unique_ptr<Point3D> v1 = dev::make_unique<Point3D>();
        // Use the constructor that matches these arguments.
        dev::unique_ptr<Point3D> v2 = dev::make_unique<Point3D>(0, 1, 2);
        // Create a unique_ptr to a vector of 5 elements.
        //dev::unique_ptr<std::vector<int>> v3 = dev::make_unique<std::vector<int>>({1,2,3,4,5});

    }
}
Enter fullscreen mode Exit fullscreen mode

Compiler Explorer

Top comments (0)