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 multipleshared_ptr
objects can share ownership of the same object and keep track of theshared_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 newshared_ptr
object is created pointing to the same object, the reference count is incremented by 1. When theshared_ptr
object is destroyed, the reference count is decremented by 1. When the reference count reaches 0, it means that there are no moreshared_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 allshared_ptr
objects pointing to the same object share the same reference count variable. This is necessary because if eachshared_ptr
object had its own reference counted variable, we would not be able to know when all theshared_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
andreturn *this
?The difference between
return this
andreturn *this
is thatreturn this
returns the pointer of the object, whilereturn *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 toreturn this
because it is safer and more convenient. When usingreturn *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 usingreturn 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 them_value
member variable.bar()
is a const member function, and cannot modify the value of them_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, theuse_count()
member function will not modify any member variables of theshared_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 thatshared_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; } };