C++ Shared_Ptr implementation I reinvented a c++ smart pointer, shared_ptr to be precise. It is meant for practice purpose and does not attempt to replace the standard implementation. To the best of my knowledge, the code works as expected. I decided to skip the custom deleter because I want to keep things simple for now. I would love feedbacks and constructive criticism.
Here is the implementation
shared.hpp
#ifndef SHARED_PTR_H_
#define SHARED_PTR_H_

#include &lt;cstddef&gt;

namespace smp
{
    template&lt;typename T&gt;
    class shared_ptr 
    {
        public:
            explicit shared_ptr(T* ptr = nullptr);
            ~shared_ptr();
            shared_ptr(shared_ptr&amp; ptr);
            shared_ptr operator=(shared_ptr&amp; ptr);
            shared_ptr(shared_ptr&amp;&amp; ptr) noexcept;
            shared_ptr operator=(shared_ptr&amp;&amp; ptr) noexcept;

            T operator*();
            T* operator-&gt;() const;

            size_t use_count() const;
            void reset();
            T* get();
            bool unique() const;
            operator bool() const;
            
        private:
            T* obj_ptr;
            struct RefCount {
                size_t use_count_;
                T* object_;
            }* ref_cnt;

    };

    template&lt;typename T&gt;
    shared_ptr&lt;T&gt; make_shared(T object);
}

#endif /* SHARED_PTR_H_ */

shared.cpp
#include &quot;shared.hpp&quot;

#include &lt;algorithm&gt;
#include &lt;iostream&gt;

namespace smp
{
    template&lt;typename T&gt;
    shared_ptr&lt;T&gt;::shared_ptr(T* ptr)
        : obj_ptr{ptr}, ref_cnt{new RefCount()}
    {
        ref_cnt-&gt;object_ = ptr;
        if(ptr)
            ++ref_cnt-&gt;use_count_;
    }

    template&lt;typename T&gt;
    shared_ptr&lt;T&gt;::shared_ptr(shared_ptr&amp; ptr)
        : ref_cnt{new RefCount()}  
    {
        obj_ptr = ptr.obj_ptr;
        ref_cnt = ptr.ref_cnt;

        ++ref_cnt-&gt;use_count_;
    }

    template&lt;typename T&gt;
    shared_ptr&lt;T&gt; shared_ptr&lt;T&gt;::operator=(shared_ptr&amp; ptr)
    {
        obj_ptr = ptr.obj_ptr;
        ref_cnt = ptr.ref_cnt;
        ++ref_cnt-&gt;use_count_;

        return *this;
    }

    template&lt;typename T&gt;
    shared_ptr&lt;T&gt;::shared_ptr(shared_ptr&amp;&amp; ptr) noexcept
        : obj_ptr{nullptr}, ref_cnt{new RefCount()}
    {
        obj_ptr = std::move(ptr.obj_ptr);
        ref_cnt = std::move(ptr.ref_cnt);

        ptr.reset();
    }

    template&lt;typename T&gt;
    shared_ptr&lt;T&gt; shared_ptr&lt;T&gt;::operator=(shared_ptr&amp;&amp; ptr) noexcept
    {
        if(obj_ptr)
            reset();
        obj_ptr = std::move(ptr.obj_ptr);
        ref_cnt = std::move(ptr.ref_cnt);

        ptr.obj_ptr = nullptr;

        return *this;
    }

    template&lt;typename T&gt;
    shared_ptr&lt;T&gt; make_shared(T object)
    {
        return shared_ptr&lt;T&gt;(new T{object});
    }

    template&lt;typename T&gt;
    T shared_ptr&lt;T&gt;::operator*()
    {
        return *obj_ptr;
    }

    template&lt;typename T&gt;
    T* shared_ptr&lt;T&gt;::operator-&gt;() const
    {
        return &amp;*obj_ptr;
    }

    template&lt;typename T&gt;
    size_t shared_ptr&lt;T&gt;::use_count() const
    {
        return ref_cnt-&gt;use_count_;
    }

    template&lt;typename T&gt;
    void shared_ptr&lt;T&gt;::reset()
    {
        obj_ptr = nullptr;

        if(ref_cnt)
            --ref_cnt-&gt;use_count_;

        if(ref_cnt-&gt;use_count_ == 0)
            ref_cnt-&gt;object_-&gt;~T();
    }

    template&lt;typename T&gt;
    T* shared_ptr&lt;T&gt;::get()
    {
        return obj_ptr;
    }

    template&lt;typename T&gt;
    bool shared_ptr&lt;T&gt;::unique() const
    {
        return (ref_cnt-&gt;use_count_ == 1);
    }

    template&lt;typename T&gt;
    shared_ptr&lt;T&gt;::operator bool() const 
    {
        return (obj_ptr != nullptr);
    }
    
    template&lt;typename T&gt;
    shared_ptr&lt;T&gt;::~shared_ptr()
    {
        if(obj_ptr)
            reset();
    }
}
Overview
Templated classes &quot;normally&quot; should provide all the code at the point of usage. As a result you should probably not have a separate shared.cpp. Though some people put the definitions in a separate shared.tpp file that is included by the header file.
Code Review:
You are missing a constructor for nullptr.
Though nullptr can be converted to any other type the compiler can not know what type you want in this situation so you would need to be explicit. But it would be nice to allow auto conversion from nullptr to the correct shared pointer type.
 void myFunc(smp::shared_ptr&lt;int&gt;&amp;&amp; ptr) { /* Stuff */}

 int main()
 {
     myFunc(nullptr); // This fails.
                      // as you defined an explicit constructor.
                      // But this is totaly safe so it would be nice if it
                      // simply worked rather than forcing the long hand.
 }

So I would add a constructor for this situation.
 smp::shared_ptr::shared_ptr(std::nullptr_t);


Non standard copy constructor and move constructor:
            shared_ptr(shared_ptr&amp; ptr);           // Would expect const ref
            shared_ptr operator=(shared_ptr&amp; ptr); // 

In the current state these can not catch temporary values.

Most people forget the noexcept. So well done.
            shared_ptr(shared_ptr&amp;&amp; ptr) noexcept;
            shared_ptr operator=(shared_ptr&amp;&amp; ptr) noexcept;


You should probably return a reference here:
            T operator*();

Otherwise you are going to force a copy of the internal object as it is returned. Just like the operator-&gt; this does not affect the state of the shared pointer so this is const.
In std::shared_ptr this is a noexcept operation. I could personally argue over that. But smarter people than me wrote the standard so I would go with them.
            T&amp;       operator*() const noexcept;


Good.
T* operator-&gt;() const;
But like the operator* can be noexcept.
            T* operator-&gt;() const noexcept;


You don't want this accidentally converting in non boolean contexts.
            operator bool() const;

In this situation:
      smp::shared_ptr&lt;int&gt;   x(new int{6});
      smp::shared_ptr&lt;float&gt; y(new float{5});

      if (x == y) {
          // Because of your bool operator
          // they are converted to bool (for both this is true)
          // resulting in this code saying they are equal.
          std::cout &lt;&lt; &quot;Equal\n&quot;;
      }

Use the explicit here:
      explicit operator bool() const;


The issue I have hear is that if the new fails (for RefCount) then you leak the pointer.
Once you had the pointer to the constructor of a shared pointer you are seeding all responsibility of the pointer. That mean if the object fails to correctly construct you need to make sure the passed pointer is deleted. So in the situation of new failing you need to call delete on the pointer.
template&lt;typename T&gt;
shared_ptr&lt;T&gt;::shared_ptr(T* ptr)
    : obj_ptr{ptr}, ref_cnt{new RefCount()}
{
    ref_cnt-&gt;object_ = ptr;
    if(ptr)
        ++ref_cnt-&gt;use_count_;
}

Add a try catch block to the initializer list:
template&lt;typename T&gt;
shared_ptr&lt;T&gt;::shared_ptr(T* ptr)
try
    : obj_ptr{ptr}
    , ref_cnt{new RefCount()}
{
    ref_cnt-&gt;object_ = ptr;
    if(ptr)
        ++ref_cnt-&gt;use_count_;
}
catch(...)
{
    delete ptr;
}


If you are copying a shared_ptr then you should be using the same ref_cnt not a brand new one.
You should prefer to do as much work in the initializer list as possible to prevent multiple initializations.
    shared_ptr&lt;T&gt;::shared_ptr(shared_ptr&amp; ptr)
        : ref_cnt{new RefCount()}  
    {
        obj_ptr = ptr.obj_ptr;
        ref_cnt = ptr.ref_cnt;

        ++ref_cnt-&gt;use_count_;
    }

// Fixed:
    shared_ptr&lt;T&gt;::shared_ptr(shared_ptr&amp; ptr)
        : obj_ptr(ptr.obj_ptr)
        , ref_cnt{ptr.ref_cnt}  
    {
        if (obj_ptr) {
            ++ref_cnt-&gt;use_count_;
        }
    }


Here you forget to decrement the ref_counter of the previous object (so potentially allowing a leak). I would use the copy and swap idiom to provide the strong exception guarantee (it also covers self assignment).
    template&lt;typename T&gt;
    shared_ptr&lt;T&gt; shared_ptr&lt;T&gt;::operator=(shared_ptr&amp; ptr)
    {
        obj_ptr = ptr.obj_ptr;  // Old object potentially leaked.
        ref_cnt = ptr.ref_cnt;  // Old ref count potentially leaked.
        ++ref_cnt-&gt;use_count_;

        return *this;
    }


Again incorrectly increment the counter.
    template&lt;typename T&gt;
    shared_ptr&lt;T&gt;::shared_ptr(shared_ptr&amp;&amp; ptr) noexcept
        : obj_ptr{nullptr}, ref_cnt{new RefCount()}
    {
        obj_ptr = std::move(ptr.obj_ptr);
        ref_cnt = std::move(ptr.ref_cnt);

        ptr.reset();
    }

The reset will decrement the counter. Since you have moved the object there has been no decrement.
Simpler to simply set this one up as null and then swap.

This can fail.
    template&lt;typename T&gt;
    shared_ptr&lt;T&gt; shared_ptr&lt;T&gt;::operator=(shared_ptr&amp;&amp; ptr) noexcept
    {
        if(obj_ptr)
            reset();

If ptr is the only copy. Then calling reset is going to free the object. Thus any other code is going to now manipulate a freeed object.
        obj_ptr = std::move(ptr.obj_ptr);
        ref_cnt = std::move(ptr.ref_cnt);


        ptr.obj_ptr = nullptr;
        // If you don't reset the ref count object then
        // eventually `ptr` is going to go out of scope and
        // its destructor is going to mess with the ref count object.

Simpler to simply set this one up as null and then swap.

Why does the shared object have to be copied constructed?
    template&lt;typename T&gt;
    shared_ptr&lt;T&gt; make_shared(T object)
    {
        return shared_ptr&lt;T&gt;(new T{object});
    }

Would be nive to allow the type T to be constructed using any of its normal constructors that take parameters.
    template&lt;typename T, typename... Args&gt;
    shared_ptr&lt;T&gt; make_shared(Args...&amp;&amp; args)
    {
        // Though this is the simplist way to do it.
        // The standard version tries to make the whole thing more efficient.
        // by allocating the space for the object T and the ref-count
        // in a single allocation (thus removing the need for an extra
        // call to new).
        return shared_ptr&lt;T&gt;(new T{std::forward&lt;Args&gt;(args)...});
    }


    template&lt;typename T&gt;
    T* shared_ptr&lt;T&gt;::operator-&gt;() const
    {
        return &amp;*obj_ptr; // Not sure why that is rewquired.
    }


This implies you don't keep a reference count object for nullptr. But your object always has a reference counter and you will need to make sure it is updated correctly.
template&lt;typename T&gt;
shared_ptr&lt;T&gt;::~shared_ptr()
{
    if(obj_ptr) // Always call reset.
                // Otherwise you are potentially leaking
                // the reference count object.
        reset();
}


Which leads us to the reference count object handling. In reset you need to call delete on the object managed and the reference count object if the count reaches zero.
template&lt;typename T&gt;
void shared_ptr&lt;T&gt;::reset()
{
    obj_ptr = nullptr;

    if(ref_cnt)
        --ref_cnt-&gt;use_count_;

    if(ref_cnt-&gt;use_count_ == 0)
        // This is not correct.
        // You have called the destructor but leaked the object space.
        ref_cnt-&gt;object_-&gt;~T();

        // You need to delete the object and the memory used by the
        // reference count object
        // delete ref_cnt-&gt;object_;
        // delete ref_cnt;
}


Self Plug: Wrote a lot about shared pointers here:
https://lokiastari.com/series/
Read three articles on shared pointers.