unique_ptr - A custom implementation

C++
Author

Quasar

Published

December 20, 2024

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

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);      

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 "pch.h"
#include "gtest/gtest.h"
#include "../UniquePointer/UniquePointer.h"

TEST(ComparisonOperator, Test) {
    /* 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));
    EXPECT_EQ(ptr1 == ptr1, true);
    EXPECT_EQ(ptr1 != ptr2, true);
}

TEST(CreateAndAccess, Test) {
    /* Create and access */
    dev::unique_ptr<int> ptr(new int(10));
    EXPECT_EQ(ptr != nullptr, true);
    EXPECT_EQ(*ptr == 10, true);

}


TEST(ResetUniquePtr, Test) {
    /* Reset unique_ptr
       Replaces the managed object with a new one*/
    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());
}

TEST(ReleaseUniquePtr, Test) {
    /* 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();
    EXPECT_EQ(ptr == nullptr, true);
    EXPECT_EQ(rawPtr != nullptr, true);
    EXPECT_EQ(*rawPtr == 3.14, true);

}

TEST(SwapFunction, Test) {
    /* 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);
    EXPECT_EQ(*ptr1 == 20, true);
    EXPECT_EQ(*ptr2 == 10, true);

}

TEST(GetRawUnderlyingPtr, Test) {
    /* Get the raw underlying pointer */
    int* x = new int(10);
    dev::unique_ptr<int> ptr(x);
    int* rawPtr = ptr.get();
    EXPECT_EQ(*rawPtr == 10, true);
    EXPECT_EQ(rawPtr == x, true);

}

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

}

TEST(IndirectionOperator, Test) {
    /* 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));
    EXPECT_EQ((*ptr).n == 10, true);
    EXPECT_EQ(ptr->foo() == 10, true);

}

TEST(MakeUnique, Test) {
    /* 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});

}

A custom implementation

// UniquePointer.cpp : This file contains the 'main' function. Program execution begins and ends there.

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


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);
        }

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

        // get the raw pointer
        T* get() const
        {
            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>
    dev::unique_ptr<T> make_unique(Args&&... args) {
        return dev::unique_ptr(new T(std::forward<Args>(args)...));
    }

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

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

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

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

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

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

Compiler Explorer