The Palos Publishing Company

Follow Us On The X Platform @PalosPublishing
Categories We Write About

How to Implement a Smart Pointer Library in C++

Smart pointers in C++ are powerful tools for automatic memory management and help prevent common issues like memory leaks and dangling pointers. Implementing a basic smart pointer library from scratch deepens understanding of resource management in modern C++. This article details how to implement a simple smart pointer library, focusing on three types of smart pointers: UniquePointer, SharedPointer, and WeakPointer.

Understanding the Concept of Smart Pointers

Smart pointers are class templates that manage the lifetime of dynamically allocated objects. Instead of manually calling delete, smart pointers automatically deallocate memory when the object is no longer needed.

There are three main types of smart pointers in C++:

  • unique_ptr: Owns a resource exclusively.

  • shared_ptr: Allows multiple smart pointers to share ownership.

  • weak_ptr: Observes an object managed by shared_ptr without affecting its lifetime.

Step 1: Basic Structure for Smart Pointers

Start by creating a base class template to serve as the foundation:

cpp
template <typename T> class SmartPointerBase { protected: T* ptr; public: SmartPointerBase(T* p = nullptr) : ptr(p) {} virtual ~SmartPointerBase() {} T* get() const { return ptr; } T& operator*() const { return *ptr; } T* operator->() const { return ptr; } };

This base provides basic pointer functionality, which will be extended in specific smart pointer types.

Step 2: Implementing UniquePointer

The UniquePointer is the simplest smart pointer. It holds sole ownership and deletes the object when it goes out of scope.

cpp
template <typename T> class UniquePointer { private: T* ptr; public: explicit UniquePointer(T* p = nullptr) : ptr(p) {} ~UniquePointer() { delete ptr; } // Disable copy semantics UniquePointer(const UniquePointer&) = delete; UniquePointer& operator=(const UniquePointer&) = delete; // Enable move semantics UniquePointer(UniquePointer&& other) noexcept : ptr(other.ptr) { other.ptr = nullptr; } UniquePointer& operator=(UniquePointer&& other) noexcept { if (this != &other) { delete ptr; ptr = other.ptr; other.ptr = nullptr; } return *this; } T* get() const { return ptr; } T& operator*() const { return *ptr; } T* operator->() const { return ptr; } void reset(T* p = nullptr) { delete ptr; ptr = p; } T* release() { T* temp = ptr; ptr = nullptr; return temp; } };

This UniquePointer enforces exclusive ownership by deleting the copy constructor and assignment operator. Move semantics allow transfer of ownership.

Step 3: Implementing SharedPointer

SharedPointer uses reference counting to allow multiple owners of the same resource. The resource is deleted only when the last SharedPointer is destroyed.

cpp
template <typename T> class SharedPointer { private: T* ptr; size_t* ref_count; public: explicit SharedPointer(T* p = nullptr) : ptr(p), ref_count(new size_t(1)) {} ~SharedPointer() { release(); } SharedPointer(const SharedPointer& other) : ptr(other.ptr), ref_count(other.ref_count) { ++(*ref_count); } SharedPointer& operator=(const SharedPointer& other) { if (this != &other) { release(); ptr = other.ptr; ref_count = other.ref_count; ++(*ref_count); } return *this; } void release() { if (--(*ref_count) == 0) { delete ptr; delete ref_count; } } T* get() const { return ptr; } T& operator*() const { return *ptr; } T* operator->() const { return ptr; } size_t use_count() const { return *ref_count; } };

This implementation uses a simple reference counter. When a SharedPointer is copied, the counter increments. When it is destroyed, the counter decrements and deletes the resource only when the count reaches zero.

Step 4: Implementing WeakPointer

A WeakPointer does not own the object and does not affect the reference count. It can be used to break cyclic dependencies in shared pointers.

cpp
template <typename T> class WeakPointer { private: T* ptr; size_t* ref_count; public: WeakPointer() : ptr(nullptr), ref_count(nullptr) {} WeakPointer(const SharedPointer<T>& shared) : ptr(shared.get()), ref_count(new size_t(*shared.ref_count)) {} ~WeakPointer() {} T* get() const { return ptr; } bool expired() const { return *ref_count == 0; } SharedPointer<T> lock() const { return expired() ? SharedPointer<T>(nullptr) : SharedPointer<T>(ptr); } };

This simplified WeakPointer assumes external management of reference count. In a full implementation, shared and weak reference counts should be separated and coordinated via a control block.

Step 5: Control Block for Shared and Weak Pointers

To implement a robust version of SharedPointer and WeakPointer, a control block structure is required. This block manages shared and weak counts separately and centralizes deletion logic.

cpp
template <typename T> class ControlBlock { public: T* ptr; size_t shared_count; size_t weak_count; explicit ControlBlock(T* p) : ptr(p), shared_count(1), weak_count(0) {} ~ControlBlock() { delete ptr; } };

Refactor SharedPointer and WeakPointer to use ControlBlock:

Updated SharedPointer

cpp
template <typename T> class SharedPointer { private: ControlBlock<T>* control; public: explicit SharedPointer(T* p = nullptr) : control(new ControlBlock<T>(p)) {} ~SharedPointer() { if (--control->shared_count == 0) { delete control->ptr; if (control->weak_count == 0) { delete control; } } } SharedPointer(const SharedPointer& other) : control(other.control) { ++control->shared_count; } SharedPointer& operator=(const SharedPointer& other) { if (this != &other) { if (--control->shared_count == 0 && control->weak_count == 0) { delete control; } control = other.control; ++control->shared_count; } return *this; } T* get() const { return control->ptr; } T& operator*() const { return *control->ptr; } T* operator->() const { return control->ptr; } size_t use_count() const { return control->shared_count; } };

Updated WeakPointer

cpp
template <typename T> class WeakPointer { private: ControlBlock<T>* control; public: WeakPointer() : control(nullptr) {} WeakPointer(const SharedPointer<T>& shared) : control(shared.control) { ++control->weak_count; } ~WeakPointer() { if (control && --control->weak_count == 0 && control->shared_count == 0) { delete control; } } bool expired() const { return control->shared_count == 0; } SharedPointer<T> lock() const { return expired() ? SharedPointer<T>(nullptr) : SharedPointer<T>(*this); } };

Benefits and Limitations

Benefits:

  • Prevents memory leaks through automatic deletion.

  • Avoids dangling pointers.

  • Facilitates safer object sharing and ownership semantics.

Limitations of Simple Implementation:

  • Not thread-safe.

  • Missing features like custom deleters.

  • WeakPointer does not handle all edge cases unless implemented with proper control block management.

Conclusion

Implementing a smart pointer library in C++ from scratch provides deep insight into memory management and resource ownership models. The UniquePointer ensures exclusive ownership, SharedPointer provides shared access through reference counting, and WeakPointer helps avoid circular references. While the standard library implementations offer robust, efficient, and thread-safe smart pointers, building your own version is an excellent exercise in understanding the mechanics behind them.

Share this Page your favorite way: Click any app below to share.

Enter your email below to join The Palos Publishing Company Email List

We respect your email privacy

Categories We Write About