I skimmed through the documentation on cppreference.com and put down the basic functionalities expected out of a unique_ptr<T> implementation. I coded up a basic implementation std::unique_ptr<T> under 250 lines. This version is not meant to deal with arrays and custom deleters.
I would like to ask someone for a thorough code review? Does my code look like modern C++20/23? Are there any bugs in the code?
Also, how do I support a curly-braced initializer list? I am thinking to code an explicit overload for such cases.
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);
}
Test Cases.
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});
        
    }
}



