shared_ptr step-by-step implementation (C++)

shard_ptr step-by-step implementation (C++)

  • shared_ptr step-by-step implementation (C++)
    • Step1 define a class
    • Step2 define member variables
    • Step3 constructor
    • Step4 copy constructor
    • Step5 Destructor
    • Step6 assignment operator
    • Step7 dereference operator
    • Step8 Implement `use_count()`
    • Step 9 Implement `reset()`
    • Thread Safety and Testing
      • testing
      • thread safe code

shared_ptr step-by-step implementation (C++)

There are several kinds of smart pointers in C++, the most common of which are std::shared_ptr and std::unique_ptr.

std::shared_ptr is a shared pointer that allows multiple pointers to share the same memory. It increments and decrements an internally maintained counter to ensure that the object is not freed until all shared_ptr pointing to the same object are destroyed.

I give you some hints to help you implement your own shared_ptr class in C++.

Step1 define a class

Step 1: Define a class template shared_ptr, with only one template parameter T representing the type of the managed object.

template<typename T>
class shared_ptr {<!-- -->
    // Your code here
};

The purpose of this class is to manage the lifetime of dynamically allocated objects of type T. The shared_ptr class should allow multiple shared_ptr objects to share ownership of the same object and automatically delete the object when the last shared_ptr object goes out of scope .

Step2 define member variables

Tip 2: You can add two private member variables in the shared_ptr class: a pointer to a managed object, and a pointer to an integer representing a reference count.

template<typename T>
class shared_ptr {<!-- -->
private:
    T* m_ptr;
    int* m_ref_count;
    
public:
    // Your code here
};

The m_ptr member variable is a raw pointer to an object managed by the shared_ptr class, while the m_ref_count member variable is a pointer to an integer that keeps track of The number of shared_ptr objects currently managing this object. The purpose of reference counting is to allow multiple shared_ptr objects to share ownership of the same object, and to ensure that the object is not deleted until the last shared_ptr object goes out of scope.

Question: Why does m_ref_count need to be a pointer type?

m_ref_count is an integer pointer type because multiple shared_ptr objects can share ownership of the same object and keep track of the shared_ptr currently managing the object > number of objects.

When creating a shared_ptr object, it uses a raw pointer (m_ptr) to point to the dynamically allocated object and creates a reference count variable (m_ref_count), initially The value is 1. Whenever a new shared_ptr object is created pointing to the same object, the reference count is incremented by 1. When the shared_ptr object is destroyed, the reference count is decremented by 1. When the reference count reaches 0, it means that there are no more shared_ptr objects pointing to it and the object can be safely deleted.

By making m_ref_count a pointer to an integer, we ensure that all shared_ptr objects pointing to the same object share the same reference count variable. This is necessary because if each shared_ptr object had its own reference counted variable, we would not be able to know when all the shared_ptr objects went out of scope and thus could not be safely deleted Administered objects.

Step3 Constructor

Tip 3: Implement a constructor that takes a raw pointer to the object and initializes the m_ptr and m_ref_count member variables.

template<typename T>
class shared_ptr {<!-- -->
public:
    shared_ptr(T* ptr) {<!-- -->
        m_ptr = ptr;
        m_ref_count = new int(1);
    }
    
    // Your code here
};

In the constructor, we initialize the m_ptr member variable to point to the object managed by the shared_ptr object and use the new operator to create a new integer and initialize it to 1. This integer will be used as the reference count of the managed object.

Note that we use new to dynamically allocate memory to store the reference count because we want to ensure that the reference count is initialized to 1 and that it is not shared with any other shared_ptr objects. We need to manually delete this memory in the destructor of the shared_ptr object.

Step4 copy constructor

Tip 4: Implement a copy constructor that takes another shared_ptr object, increments its reference count, and copies its m_ptr and m_ref_count member variables .

template<typename T>
class shared_ptr {<!-- -->
public:
    shared_ptr(T* ptr) {<!-- -->
        m_ptr = ptr;
        m_ref_count = new int(1);
    }
    
    shared_ptr(const shared_ptr<T> & amp; other) {<!-- -->
        m_ptr = other.m_ptr;
        m_ref_count = other.m_ref_count;
        (*m_ref_count) + + ;
    }
    
    // Your code here
};

In the copy constructor, we initialize the m_ptr and m_ref_count member variables of the new shared_ptr object to the existing shared_ptr respectively code> object’s m_ptr and m_ref_count member variables, and increase the reference count of the managed object by 1.

By increasing the reference count, we indicate that there is one more shared_ptr object managing the same object as the original shared_ptr object. This is important because it allows multiple shared_ptr objects to share ownership of the same object and ensures that the object is not deleted until all shared_ptr objects managing it go out of scope.

Step5 Destructor

Tip 5: Implement a destructor that, when the reference count reaches zero, decrements the reference count by one and deletes the managed object.

template<typename T>
class shared_ptr {<!-- -->
public:
    shared_ptr(T* ptr) {<!-- -->
        m_ptr = ptr;
        m_ref_count = new int(1);
    }
    
    shared_ptr(const shared_ptr<T> & amp; other) {<!-- -->
        m_ptr = other.m_ptr;
        m_ref_count = other.m_ref_count;
        (*m_ref_count) + + ;
    }
    
    ~shared_ptr() {<!-- -->
        (*m_ref_count)--;
        if (*m_ref_count == 0) {<!-- -->
            delete m_ptr;
            delete m_ref_count;
        }
    }
    
    // Your code here
};

In the destructor, we first decrement the reference count of the managed object by one. If the reference count reaches 0, it means that there are no more shared_ptr objects pointing to it and we can safely delete it. In this case we use the delete operator to delete managed objects and reference counts.

Note that we use delete to free the memory associated with managed objects and reference counts, since we use new to dynamically allocate them. If we use malloc or calloc to allocate memory for these objects, we need to use free to free the memory instead of delete.

Step6 assignment operator

Tip 6: Implement an assignment operator that decrements the reference count of the old shared_ptr object by one and increases the reference count of the new object by one.

template<typename T>
class shared_ptr {<!-- -->
public:
    shared_ptr(T* ptr) {<!-- -->
        m_ptr = ptr;
        m_ref_count = new int(1);
    }
    
    shared_ptr(const shared_ptr<T> & amp; other) {<!-- -->
        m_ptr = other.m_ptr;
        m_ref_count = other.m_ref_count;
        (*m_ref_count) + + ;
    }
    
    ~shared_ptr() {<!-- -->
        (*m_ref_count)--;
        if (*m_ref_count == 0) {<!-- -->
            delete m_ptr;
            delete m_ref_count;
        }
    }
    
    shared_ptr<T> & amp; operator=(const shared_ptr<T> & amp; other) {<!-- -->
        if (this != &other) {<!-- -->
            (*m_ref_count)--;
            if (*m_ref_count == 0) {<!-- -->
                delete m_ptr;
                delete m_ref_count;
            }
            m_ptr = other.m_ptr;
            m_ref_count = other.m_ref_count;
            (*m_ref_count) + + ;
        }
        return *this;
    }
};

In the assignment operator, we first check if the new shared_ptr object is the same as the old one. If they are the same, we do nothing and return a reference to the current shared_ptr object. Otherwise, we decrement the reference count of the old shared_ptr object by one and check whether the reference count has reached 0. If it has been reached, delete the managed object and reference count. Then, we copy the m_ptr and m_ref_count member variables of the new shared_ptr object to the old shared_ptr object, And increase the reference count of the managed object by 1.

By copying the m_ptr and m_ref_count member variables of the new shared_ptr object into the old one, we effectively transfer the ownership of the managed The old shared_ptr object is transferred to the new shared_ptr object. By increasing the managed object’s reference count by 1, we indicate that there is one more shared_ptr object managing the same object as the new shared_ptr object.

Question: What is the difference between return this and return *this?

The difference between return this and return *this is that return this returns the pointer of the object, while return *this returns the object references.

When using return this, what is returned is a pointer to the object on which the current method was invoked. This is useful if you want to chain multiple method calls or use the arrow (->) operator to directly access member variables or methods of an object.

On the other hand, when using return *this , what is returned is a reference to the object on which the current method was invoked. This is useful if you want to chain multiple method calls or pass an object to a function that requires a reference as an argument.

In general, using return *this is preferred to return this because it is safer and more convenient. When using return *this , you can treat the returned value as a regular object and use the dot (.) operator to directly access its member variables and methods. When using return this , you need to use the arrow (->) operator to access member variables and methods of the object, which can be more cumbersome and error-prone.

Step7 dereference operator

Tip 7: Implement a dereference operator (OPERATOR*) and a member access operator (OPERATOR->) to allow access to member variables and member functions of managed objects .

template<typename T>
class shared_ptr {<!-- -->
public:
    shared_ptr(T* ptr) {<!-- -->
        m_ptr = ptr;
        m_ref_count = new int(1);
    }
    
    shared_ptr(const shared_ptr<T> & amp; other) {<!-- -->
        m_ptr = other.m_ptr;
        m_ref_count = other.m_ref_count;
        (*m_ref_count) + + ;
    }
    
    ~shared_ptr() {<!-- -->
        (*m_ref_count)--;
        if (*m_ref_count == 0) {<!-- -->
            delete m_ptr;
            delete m_ref_count;
        }
    }
    
    shared_ptr<T> & amp; operator=(const shared_ptr<T> & amp; other) {<!-- -->
        if (this != &other) {<!-- -->
            (*m_ref_count)--;
            if (*m_ref_count == 0) {<!-- -->
                delete m_ptr;
                delete m_ref_count;
            }
            m_ptr = other.m_ptr;
            m_ref_count = other.m_ref_count;
            (*m_ref_count) + + ;
        }
        return *this;
    }
    
    T & amp; operator*() const {<!-- -->
        return *m_ptr;
    }
    
    T* operator->() const {<!-- -->
        return m_ptr;
    }
    
private:
    T* m_ptr;
    int* m_ref_count;
};

In the dereference operator (operator*), we return a reference to the managed object, which allows us to access its member variables and member functions using the dot operator (.) . In the member access operator (operator->), we return a pointer to the managed object, which allows us to use the arrow operator (->) to access its member variables and member function.

By implementing these operators, we can treat shared_ptr objects as regular pointers to objects. This allows us to access member variables and member functions of managed objects in a more natural and intuitive way.

Step8 implement use_count()

Tip 8: Implement a use_count() member function that returns the current reference count of the managed object.

template<typename T>
class shared_ptr {<!-- -->
public:
    shared_ptr(T* ptr) {<!-- -->
        m_ptr = ptr;
        m_ref_count = new int(1);
    }
    
    shared_ptr(const shared_ptr<T> & amp; other) {<!-- -->
        m_ptr = other.m_ptr;
        m_ref_count = other.m_ref_count;
        (*m_ref_count) + + ;
    }
    
    ~shared_ptr() {<!-- -->
        (*m_ref_count)--;
        if (*m_ref_count == 0) {<!-- -->
            delete m_ptr;
            delete m_ref_count;
        }
    }
    
    shared_ptr<T> & amp; operator=(const shared_ptr<T> & amp; other) {<!-- -->
        if (this != &other) {<!-- -->
            (*m_ref_count)--;
            if (*m_ref_count == 0) {<!-- -->
                delete m_ptr;
                delete m_ref_count;
            }
            m_ptr = other.m_ptr;
            m_ref_count = other.m_ref_count;
            (*m_ref_count) + + ;
        }
        return *this;
    }
    
    T & amp; operator*() const {<!-- -->
        return *m_ptr;
    }
    
    T* operator->() const {<!-- -->
        return m_ptr;
    }
    
    int use_count() const {<!-- -->
        return *m_ref_count;
    }
    
private:
    T* m_ptr;
    int* m_ref_count;
};

In the use_count() member function, we simply return the value of the m_ref_count member variable. This way we can check how many shared_ptr objects are managing the same object.

By implementing this function, we can monitor the reference count of the managed object, verifying that it is correctly shared among multiple shared_ptr objects. This is important because memory leaks or undefined behavior can result if reference counts are not maintained properly.

Step 9 implement reset()

Tip 9: Implement a reset() member function that releases the managed object and sets the shared_ptr object to null.

template<typename T>
class shared_ptr {<!-- -->
public:
    shared_ptr(T* ptr) {<!-- -->
        m_ptr = ptr;
        m_ref_count = new int(1);
    }
    
    shared_ptr(const shared_ptr<T> & amp; other) {<!-- -->
        m_ptr = other.m_ptr;
        m_ref_count = other.m_ref_count;
        (*m_ref_count) + + ;
    }
    
    ~shared_ptr() {<!-- -->
        (*m_ref_count)--;
        if (*m_ref_count == 0) {<!-- -->
            delete m_ptr;
            delete m_ref_count;
        }
    }
    
    shared_ptr<T> & amp; operator=(const shared_ptr<T> & amp; other) {<!-- -->
        if (this != &other) {<!-- -->
            (*m_ref_count)--;
            if (*m_ref_count == 0) {<!-- -->
                delete m_ptr;
                delete m_ref_count;
            }
            m_ptr = other.m_ptr;
            m_ref_count = other.m_ref_count;
            (*m_ref_count) + + ;
        }
        return *this;
    }
    
    T & amp; operator*() const {<!-- -->
        return *m_ptr;
    }
    
    T* operator->() const {<!-- -->
        return m_ptr;
    }
    
    int use_count() const {<!-- -->
        return *m_ref_count;
    }
    
    void reset() {<!-- -->
        (*m_ref_count)--;
        if (*m_ref_count == 0) {<!-- -->
            delete m_ptr;
            delete m_ref_count;
        }
        m_ptr = nullptr;
        m_ref_count = nullptr;
    }
    
private:
    T* m_ptr;
    int* m_ref_count;
};

In the reset() member function, we first decrement the reference count of the managed object by 1, and delete the managed object if the reference count drops to 0. We then set the m_ptr and m_ref_count member variables to null, thus freeing the managed object and setting the shared_ptr object to null.

By implementing this function, we can release the managed object when it is no longer needed, which can help prevent memory leaks and improve performance. This function is also useful when we want to reuse a shared_ptr object to manage another object.

Question: Why do I need to add const after some functions?

In C++, the const keyword is used to indicate that a function does not modify the state of the object it is called from. This is called a “const member function”.

When you declare a member function as const, you are telling the compiler that this function will not modify any member variables of the object it is called on. This allows the function to be called on const objects, as well as non-const objects. (doesn’t call on const objects without declaring const )

Below is an example:

class MyClass {<!-- -->
public:
 void foo() {<!-- -->
     // This function can modify member variables
 }

 void bar() const {<!-- -->
     // This function cannot modify member variables
 }

private:
 int m_value;
};

In this example, foo() is a non-const member function that modifies the value of the m_value member variable. bar() is a const member function, and cannot modify the value of the m_value member variable.

If a member function does not modify any member variables of the object that calls it, the const keyword should be added to the function declaration. This is good programming practice because it allows writing more general and flexible code that can be used for both const and non-const objects.

For example, in the shared_ptr class, the use_count() member function will not modify any member variables of the shared_ptr object. Therefore, it should be declared const:

int use_count() const {<!-- -->
 return *m_ref_count;
}

By declaring this function as const, we allow it to be called on const shared_ptr objects, which is useful in some cases to ensure that shared_ptr objects are not modified.

Thread safety and testing

Finally, if shared_ptr is used in multithreading, we may need to consider the locking of m_ref_count to ensure thread safety. At the same time, we also give a set of tests, and my final code.

Testing

void test1(){<!-- -->
    std::cout << "Tes`t 1:" << std::endl;
    shared_ptr<int> p1(new int(42));
    shared_ptr<int> p2(p1);
    shared_ptr<int> p3 = p1;
    std::cout << *p1 << std::endl; // Output: 42
    std::cout << *p2 << std::endl; // Output: 42
    std::cout << *p3 << std::endl; // Output: 42
    std::cout << p1. use_count() << std::endl; // Output: 3
    std::cout << p2. use_count() << std::endl; // Output: 3
    std::cout << p3. use_count() << std::endl; // Output: 3
}

void test2(){<!-- -->
    std::cout << "Test 2:" << std::endl;
    shared_ptr<int> p1(new int(42));
    shared_ptr<int> p2(p1);
    p1. reset();
    std::cout << p1. use_count() << std::endl; // Output: 0
    std::cout << p2. use_count() << std::endl; // Output: 1
}

void test3(){<!-- -->
    std::cout << "Test 3:" << std::endl;
    struct MyClass {<!-- -->
        int m_value;
        MyClass(int value) : m_value(value) {<!-- -->}
        void foo() {<!-- --> std::cout << "foo() called with value " << m_value << std::endl; }
    };
    shared_ptr<MyClass> p1(new MyClass(42));
    (*p1).foo(); // Call the foo() member function of MyClass
    p1->foo(); // Same as above
}

void test4(){<!-- -->
    std::cout << "Test 4:" << std::endl;
    shared_ptr<int> p1(nullptr);
    std::cout << p1. use_count() << std::endl; // Output: 0
}

void test5(){<!-- -->
    std::cout << "Test 5:" << std::endl;
    shared_ptr<int> p1(new int(42));
    p1 = p1;
    std::cout << p1. use_count() << std::endl; // Output: 1
}

int main(){<!-- -->
    test1();
    test2();
    test3();
    test4();
    test5();
    return 0;
}

Thread-safe code

template <typename T>
class shared_ptr{<!-- -->
private:
    T *m_ptr;
    std::mutex *m_mutex;
    int *count_reference;

    void release(){<!-- -->
        if (m_ptr == nullptr){<!-- --> // check if the pointer is null
            return;
        }
        bool delete_flag = false;
        m_mutex->lock();
        --(*count_reference);
        if (*count_reference == 0){<!-- -->
            delete m_ptr;
            delete count_reference;
            delete_flag = true;
        }
        m_mutex->unlock();
        if (delete_flag){<!-- -->
            delete m_mutex;
        }
    }
public:
    // Does it have to be explicitly initialized? Yes
    shared_ptr(T *ptr){<!-- -->
        m_ptr = ptr;
        m_mutex = new std::mutex();
        count_reference = new int(1);
    }

    shared_ptr(const shared_ptr<T> & amp;other){<!-- -->
        m_ptr = other.m_ptr;
        m_mutex = other.m_mutex;
        count_reference = other. count_reference;
        m_mutex->lock(); // lock
         + + (*count_reference);
        m_mutex->unlock();
    }

    shared_ptr<T> & amp;operator=(const shared_ptr<T> & amp;other){<!-- -->
        if (other.m_ptr != m_ptr){<!-- --> // object is a private variable that can access other objects of the same class
            release();
            m_ptr = other.m_ptr;
            m_mutex = other.m_mutex;
            count_reference = other. count_reference;
        }
        return *this;
    }

    ~shared_ptr(){<!-- -->
        release();
    }

    T & amp; operator*() const{<!-- -->
        // const: const member function
        return *m_ptr;
    }

    T *operator->() const{<!-- -->
        return m_ptr;
    }

    T *get() const {<!-- -->
        return m_ptr;
    }


    int use_count() const{<!-- -->
        if (m_ptr == nullptr){<!-- --> // check if the pointer is null
            return 0;
        }
        return *count_reference;
    }

    void reset(){<!-- -->
        release();
        m_ptr = nullptr;
        m_mutex = nullptr;
        count_reference = nullptr;
    }
};