In-Depth Guide to C++ Smart Pointers

Introduction

There are four smart pointers in C++: auto_ptr, unique_ptr, shared_ptr, and weak_ptr. The last three are supported in C++11, while the first one has been deprecated in C++11.

Introduction to C++11 Smart Pointers

Smart pointers are mainly used to manage memory allocated on the heap, encapsulating regular pointers as stack objects. When the lifespan of the stack object ends, the memory allocated will be released in the destructor, thus preventing memory leaks. The most commonly used smart pointer in C++11 is shared_ptr, which uses reference counting to keep track of how many smart pointers are referencing the current memory resource. The memory allocated for this reference counting is on the heap. When a new reference is added, the reference count increases by one, and when it expires, the count decreases. Only when the reference count reaches zero will the smart pointer automatically release the referenced memory resource. When initializing a shared_ptr, a regular pointer cannot be directly assigned to it, as one is a pointer and the other is a class. It can be initialized using the make_shared function or by passing a regular pointer through the constructor. The regular pointer can also be obtained using the get function.

Why Use Smart Pointers

The role of smart pointers is to manage a pointer, as there are situations where allocated memory is forgotten to be released at the end of a function, leading to memory leaks. Using smart pointers can significantly avoid this problem because a smart pointer is a class that automatically calls the destructor when the instance of the class goes out of scope, releasing resources automatically. Therefore, the principle of smart pointers is to automatically release memory space at the end of a function without needing to manually release memory.

auto_ptr

(C++98 solution, deprecated in C++11) adopts an ownership model.

cpp

auto_ptr<string> p1 (new string ("I reigned lonely as a cloud.")); auto_ptr<string> p2; p2 = p1; //auto_ptr will not report an error.

At this point, no error will occur, as p2 has taken ownership from p1. However, accessing p1 during program execution will cause an error. Therefore, the drawback of auto_ptr is the potential for memory crash issues!

unique_ptr

(Replaces auto_ptr) unique_ptr implements the concept of exclusive ownership, ensuring that only one smart pointer can point to an object at a time. It is particularly useful in preventing resource leaks (for example, forgetting to call delete after creating an object with new due to an exception).

It adopts an ownership model, still using the previous example.

cpp

unique_ptr<string> p3 (new string ("auto"));   //#4unique_ptr<string> p4;//#5p4 = p3;//this will cause an error!!

The compiler considers p4=p3 illegal, preventing p3 from pointing to invalid data.Attempting to copy p3 will result in a compile-time error, while auto_ptr can compile, leading to potential runtime errors.Thus, unique_ptr is safer than auto_ptr.

Additionally, unique_ptr has a clever feature: when the program attempts to assign one unique_ptr to another, if the source unique_ptr is a temporary rvalue, the compiler allows it; if the source unique_ptr will exist for a while, the compiler prohibits it, as shown:

cpp

unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1;                                      // #1 not allowedunique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

In #1, a dangling unique_ptr (pu1) is left, which can be hazardous.While #2 does not leave a dangling unique_ptr, as it calls the constructor of unique_ptr, which destroys the temporary object once ownership is transferred to pu3.This behavior indicates that unique_ptr is superior to auto_ptr, which allows both types of assignments.

Note: If you really want to perform an operation similar to #1, to safely reuse such a pointer, you can assign it a new value. C++ has a standard library function std::move() that allows you to transfer a unique_ptr to another. Although there may still be cases where the original pointer is called (leading to crashes), this syntax emphasizes that you are transferring ownership, making it clear what you are doing and preventing accidental calls to the original pointer.

(Additional: The boost library’s boost::scoped_ptr is also an exclusive smart pointer, but it does not allow ownership transfer, being responsible for only one resource from start to finish, making it safer but with a narrower application range.)

For example:

cpp

unique_ptr<string> ps1, ps2;ps1 = demo("hello");ps2 = move(ps1);ps1 = demo("alexia");cout << *ps2 << *ps1 << endl;

shared_ptr

shared_ptr implements the concept of shared ownership. Multiple smart pointers can point to the same object, and the object and its associated resources will be released when “the last reference is destroyed”. As the name share indicates, resources can be shared by multiple pointers, using a counting mechanism to indicate how many pointers share the resource. The number of owners can be checked using the member function use_count(). In addition to being constructed using new, it can also be constructed by passing in auto_ptr, unique_ptr, or weak_ptr. When we call release(), the current pointer releases resource ownership, and the count decreases by one. When the count equals zero, the resource will be released.

shared_ptr was created to solve the limitations of auto_ptr in object ownership (auto_ptr is exclusive), providing a smart pointer that can share ownership using a reference counting mechanism.

Member functions:

use_count returns the number of references

unique returns whether it is exclusive ownership (use_count is 1)

swap swaps two shared_ptr objects (i.e., swaps the owned objects)

reset relinquishes ownership of the internal object or changes the owned object, which will reduce the reference count of the original object

get returns the internal object (pointer), as it has overloaded the () method, it behaves the same as directly using the object. For example:

cpp

shared_ptr<int> sp(new int(1));

sp and sp.get() are equivalent.

A simple example of share_ptr:

cpp

int main(){  string *s1 = new string("s1");  shared_ptr<string> ps1(s1);  shared_ptr<string> ps2;  ps2 = ps1;  cout << ps1.use_count()<<endl;  //2  cout<<ps2.use_count()<<endl;  //2  cout << ps1.unique()<<endl;  //0  string *s3 = new string("s3");  shared_ptr<string> ps3(s3);  cout << (ps1.get()) << endl;  //033AEB48  cout << ps3.get() << endl;  //033B2C50  swap(ps1, ps3);  //swap owned objects  cout << (ps1.get())<<endl;  //033B2C50  cout << ps3.get() << endl;  //033AEB48  cout << ps1.use_count()<<endl;  //1  cout << ps2.use_count() << endl;  //2  ps2 = ps1;  cout << ps1.use_count()<<endl;  //2  cout << ps2.use_count() << endl;  //2  ps1.reset();  //relinquish ps1's ownership, reducing reference count  cout << ps1.use_count()<<endl;  //0  cout << ps2.use_count()<<endl;  //1}

weak_ptr

While shared_ptr is very useful, there is still a risk of memory leaks when two objects mutually reference a shared_ptr member variable pointing to each other, causing circular references and rendering the reference count ineffective.

weak_ptr is a smart pointer that does not control the object’s lifecycle; it points to an object managed by a shared_ptr. The memory management of the object is handled by the strong reference shared_ptr, while weak_ptr only provides an access method to the managed object. The purpose of weak_ptr is to assist shared_ptr, and it can only be constructed from a shared_ptr or another weak_ptr object. Its construction and destruction do not increase or decrease the reference count. weak_ptr is used to solve the deadlock problem when shared_ptr references each other; if two shared_ptrs reference each other, their reference counts can never decrease to zero, and the resources will never be released. It is a weak reference to the object and does not increase the reference count. It can be converted back and forth with shared_ptr, allowing a shared_ptr to be directly assigned to it, and it can obtain a shared_ptr by calling the lock function.

cpp

class B;  //declarationclass A{public:  shared_ptr<B> pb_;  ~A()  {    cout << "A delete\n";  }};class B{public:  shared_ptr<A> pa_;  ~B()  {    cout << "B delete\n";  }};void fun(){  shared_ptr<B> pb(new B());  shared_ptr<A> pa(new A());  cout << pb.use_count() << endl;  //1  cout << pa.use_count() << endl;  //1  pb->pa_ = pa;  pa->pb_ = pb;  cout << pb.use_count() << endl;  //2  cout << pa.use_count() << endl;  //2}int main(){  fun();  return 0;}

In the fun function, pa and pb mutually reference each other, resulting in both resources having a reference count of 2. When exiting the function, the smart pointers pa and pb destruct, their reference counts decrease by one, but both reference counts remain at 1, preventing the resources from being released (the destructors of A and B are not called), leading to a memory leak.If one of them is changed to weak_ptr, it will work. We can change the shared_ptr pb_ in class A to weak_ptr pb_, and the output will be as follows:

cpp

1112B deleteA delete

In this case, resource B’s reference count starts at 1. When pb destructs, B’s count becomes 0 and is released, which in turn reduces A’s count by one. When pa destructs, A’s count also becomes 0, releasing A.

Note: We cannot directly access the object’s methods through weak_ptr, for example, if the B object has a method print(), we cannot access it like this: pa->pb_->print(); because pb_ is a weak_ptr; we should first convert it to a shared_ptr, like:

cpp

shared_ptr<B> p = pa->pb_.lock();p->print();

weak_ptr does not overload * and -> but can use lock to obtain a usable shared_ptr object. Note, weak_ptr needs to check its validity before use.

expired checks whether the managed object has been released; if it has been released, it returns true; otherwise, it returns false.

lock is used to obtain a strong reference (shared_ptr) to the managed object. If expired is true, it returns an empty shared_ptr; otherwise, it returns a shared_ptr whose internal object points to the same as weak_ptr.

use_count returns the reference count of the object shared with shared_ptr.

reset sets weak_ptr to null.

weak_ptr supports copying or assignment but does not affect the reference count of the corresponding shared_ptr internal object.

Core Implementation of shared_ptr and weak_ptr

As a weak reference pointer, weak_ptr’s implementation relies on the counter class and shared_ptr’s assignment and construction, so let’s briefly implement the counter and shared_ptr.

Simple Implementation of Counter

cpp

class Counter{public:    Counter() : s(0), w(0){};    int s;  //reference count of shared_ptr    int w;  //reference count of weak_ptr};

The purpose of the counter object is to allocate a block of memory to store the reference count, where s is the reference count of shared_ptr and w is the reference count of weak_ptr. When w is 0, the Counter object is deleted.

Simple Implementation of shared_ptr

cpp

template <class T>class WeakPtr; //needed for weak_ptr's lock() to generate shared_ptr, requires copy constructortemplate <class T>class SharePtr{public:    SharePtr(T *p = 0) : _ptr(p)    {        cnt = new Counter();        if (p)            cnt->s = 1;        cout << "in construct " << cnt->s << endl;    }    ~SharePtr()    {        release();    }    SharePtr(SharePtr<T> const &s)    {        cout << "in copy con" << endl;        _ptr = s._ptr;        (s.cnt)->s++;        cout << "copy construct" << (s.cnt)->s << endl;        cnt = s.cnt;    }    SharePtr(WeakPtr<T> const &w) //needed for weak_ptr's lock() to generate shared_ptr, requires copy construct    {        cout << "in w copy con " << endl;        _ptr = w._ptr;        (w.cnt)->s++;        cout << "copy w  construct" << (w.cnt)->s << endl;        cnt = w.cnt;    }    SharePtr<T> &operator=(SharePtr<T> &s)    {        if (this != &s)        {            release();            (s.cnt)->s++;            cout << "assign construct " << (s.cnt)->s << endl;            cnt = s.cnt;            _ptr = s._ptr;        }        return *this;    }    T &operator*()    {        return *_ptr;    }    T *operator->()    {        return _ptr;    }    friend class WeakPtr<T>; //allows weak_ptr to set reference counts and assignmentsprotected:    void release(){        cnt->s--;        cout << "release " << cnt->s << endl;        if (cnt->s < 1)        {            delete _ptr;            if (cnt->w < 1)            {                delete cnt;                cnt = NULL;            }        }    }private:    T *_ptr;    Counter *cnt;};

The provided function interfaces for shared_ptr include:construction, copy construction, assignment, dereferencing, and releasing memory through release when the reference count reaches 0.

Simple Implementation of weak_ptr

cpp

template <class T>class WeakPtr{public: //provides default and copy constructors, where the copy constructor cannot be constructed from the original pointer    WeakPtr()    {        _ptr = 0;        cnt = 0;    }    WeakPtr(SharePtr<T> &s) : _ptr(s._ptr), cnt(s.cnt)    {        cout << "w con s" << endl;        cnt->w++;    }    WeakPtr(WeakPtr<T> &w) : _ptr(w._ptr), cnt(w.cnt)    {        cnt->w++;    }    ~WeakPtr()    {        release();    }    WeakPtr<T> &operator=(WeakPtr<T> &w)    {        if (this != &w)        {            release();            cnt = w.cnt;            cnt->w++;            _ptr = w._ptr;        }        return *this;    }    WeakPtr<T> &operator=(SharePtr<T> &s)    {        cout << "w = s" << endl;        release();        cnt = s.cnt;        cnt->w++;        _ptr = s._ptr;        return *this;    }    SharePtr<T> lock()    {        return SharePtr<T>(*this);    }    bool expired(){        if (cnt)        {            if (cnt->s > 0)            {                cout << "empty" << cnt->s << endl;                return false;            }        }        return true;    }    friend class SharePtr<T>; //allows weak_ptr to set reference counts and assignments    protected:    void release(){        if (cnt)        {            cnt->w--;            cout << "weakptr release" << cnt->w << endl;            if (cnt->w < 1 && cnt->s < 1)            {                //delete cnt;                cnt = NULL;            }        }    }private:    T *_ptr;    Counter *cnt;};

weak_ptr is generally constructed through shared_ptr, checks if the original pointer is null using the expired function, and converts to shared_ptr using lock.

Original article: https://www.cnblogs.com/WindSun/p/11444429.html

Recommendations
1. Are these C/C++ questions within your knowledge gap?
2. Master C++ smart pointer usage in one article
3. TIOBE September latest index: C first, what about C++?
4. Recently, four departments of the state jointly guided companies such as Alibaba and Meituan
5. 67% of college students believe: ten years after graduation, they can earn a million annual salary!

6. Why are outsourcing companies so unpopular?

2T Programmer gift package giveaway, including C/C++, Linux, Python, Java, PHP, artificial intelligence, microcontrollers, Raspberry Pi, etc.
Follow our public account👇 and reply “1024” to get it for free!

Leave a Comment

×