C++11's New Features: shared_ptr

Overview

In the C++11 standard, smart pointers were introduced as an automatic resource management tool, greatly improving the robustness and security of code. Among them, std::shared_ptr, as a multi-ownership smart pointer, has become an important component in modern C++ development thanks to its unique reference counting mechanism and automatic memory deallocation function.

std::shared_ptr is one of the smart pointer types provided by the C++ standard library. It uses reference counting technology to track the number of owners of dynamically allocated memory objects. When the last owner (that is, the last std::shared_ptr instance) is destroyed, the dynamically allocated memory it points to is also released, effectively avoiding memory leak problems.

Core Features of shared_ptr

1. Shared ownership.

A std::shared_ptr instance can be copied or moved to another std::shared_ptr instance. After copying, the two share the same resource and both participate in maintaining the reference count.

2. Weak reference.

The C++ standard library also provides std::weak_ptr, which is a type of weak reference that does not increase the reference count. It is used to solve the problem of objects not being released due to circular references.

3. Custom deleter.

std::shared_ptr allows specifying a custom deleter to perform specific cleanup operations when the resource is no longer needed.

4. Atomicity guarantee.

In a multithreaded environment, increment and decrement operations on the reference count are atomic, ensuring thread safety.

Interface of shared_ptr

shared_ptr is a reference-counted smart pointer used to share ownership of objects. It can be constructed from a raw pointer, another shared_ptr, an auto_ptr, or a weak_ptr. You can also pass a second parameter to the shared_ptr constructor, which is called a deleter. The deleter handles the release of shared resources, which is very useful for managing resources that are not allocated with new or released with delete. After shared_ptr is created, it can be used just like a regular pointer, with the only exception that it cannot be explicitly deleted.

Some important interfaces of shared_ptr are listed below.

template 
explicit shared_ptr(T* p);

This constructor takes ownership of the given pointer p, and parameter p must be a valid pointer to T. After construction, the reference count is set to 1. The only exception thrown by this constructor is std::bad_alloc, which only occurs in very rare cases when sufficient memory cannot be allocated for the reference counter.

template 
shared_ptr(T* p,D d);

This constructor takes two parameters. The first is the resource that shared_ptr will take ownership of, and the second is an object responsible for releasing the resource when shared_ptr is destroyed. The stored resource is passed to this object in the form of d(p). If the reference counter cannot be allocated successfully, shared_ptr throws an exception of type std::bad_alloc.

shared_ptr(const shared_ptr & r);

The resource stored in r is shared by the newly constructed shared_ptr, and the reference count is incremented by one. This constructor does not throw exceptions.

template 
explicit shared_ptr(const weak_ptr & r);

Constructs a shared_ptr from a weak_ptr. This makes the use of weak_ptr thread-safe, because the reference count of the shared resource pointed to by the weak_ptr parameter will be incremented (weak_ptr does not affect the reference count of the shared resource). If the weak_ptr is empty (r.use_count() == 0), shared_ptr throws an exception of type bad_weak_ptr.

template 
shared_ptr(auto_ptr & r);

This constructor acquires ownership of the pointer stored in r from an auto_ptr by saving a copy of the pointer and calling release on the auto_ptr. After construction, the reference count is 1, and r becomes empty. If the reference counter cannot be allocated successfully, std::bad_alloc is thrown.

~shared_ptr();

Destructor of shared_ptr, decrements the reference count by one. If the count reaches zero, the stored pointer is deleted. The pointer is deleted by calling operator delete, or if a deleter object was specified for deletion, the object is called with the stored pointer as the only parameter. The destructor does not throw exceptions.

shared_ptr & operator=(const shared_ptr & r);

The assignment operation shares the resource in r and stops sharing the original resource. The assignment operation does not throw exceptions.

void reset();

The reset function is used to stop sharing ownership of the stored pointer. The reference count of the shared resource is decremented by one.

T & operator*() const;

This operator returns a reference to the object pointed to by the stored pointer. If the pointer is null, calling operator* results in undefined behavior. This operator does not throw exceptions.

T* operator->() const;

This operator returns the stored pointer. Together with operator*, this operator makes the smart pointer behave like a regular pointer. This operator does not throw exceptions.

T* get() const;

The get function is the best way to obtain the stored pointer when it may be null (in which case both operator* and operator-> will result in undefined behavior). Note that you can also use implicit boolean conversion to test whether shared_ptr contains a valid pointer. This function does not throw exceptions.

bool unique() const;

This function returns true if shared_ptr is the sole owner of the stored pointer; otherwise it returns false. unique does not throw exceptions.

long use_count() const;

The use_count function returns the reference count of the pointer. It is particularly useful during debugging because it allows you to get a snapshot of the reference count at key points during program execution. Use it with caution, because in some possible shared_ptr implementations, calculating the reference count may be expensive or even not supported. This function does not throw exceptions.

void swap(shared_ptr & b);

This allows you to conveniently swap two shared_ptr instances. The swap function swaps the stored pointers (and their reference counts). This function does not throw exceptions.

template   shared_ptr static_pointer_cast(const shared_ptr & r);

To perform a static_cast on the pointer stored in shared_ptr, you can extract the pointer and cast it, but you cannot store it in another shared_ptr; the new shared_ptr will assume that it is the first to manage this resource. The solution is to use static_pointer_cast, which ensures that the reference count of the pointed object remains correct. static_pointer_cast does not throw exceptions.

Usage of shared_ptr

Refer to the following sample code for using shared_ptr.

#include 
#include 
using namespace std;

int main()
{
shared_ptr pInt1;
// No pointer is referenced yet
assert(pInt1.use_count() == 0);

{

    shared_ptr<int> pInt2(new int(66));
    // The pointer new int(66) is referenced once
    assert(pInt2.use_count() == 1);

    pInt1 = pInt2;

    // The pointer new int(66) is referenced twice
    assert(pInt2.use_count() == 2);
    assert(pInt1.use_count() == 2);

}

// pInt2 goes out of scope, so the reference count of new int(66) is decremented by 1
assert(pInt1.use_count() == 1);
return 0;

}
</assert.h>

What if the resource is not created and destroyed using new and delete? As we can see from the interfaces introduced earlier, you can specify a deleter in the shared_ptr constructor. Refer to the following sample code for how to use it.

#include 
using namespace std;

class CFileCloser
{
public:
void operator()(FILE *pFile)
{
if (pFile != NULL)
{
fclose(pFile);
pFile = NULL;
}
}
};

int main()
{
shared_ptr fp(fopen(“C:\1.txt”, “r”), CFileCloser());
return 0;
}

When using shared_ptr, you need to avoid passing the same object pointer as a parameter to the shared_ptr constructor twice. Consider the following sample code.

    {
        int *pInt = new int(66);
        shared_ptr pTemp1(pInt);
        assert(pTemp1.use_count() == 1);
    shared_ptr<int> pTemp2(pInt);
    assert(pTemp2.use_count() == 1);
}
// pTemp1 and pTemp2 go out of scope, both destroy pInt, which will cause the same block of memory to be deallocated twice

The correct approach is: after assigning the raw pointer to the smart pointer, all subsequent operations should be performed on the smart pointer.

    {
        shared_ptr pTemp1(new int(66));
        assert(pTemp1.use_count() == 1);
    shared_ptr<int> pTemp2(pTemp1);
    assert(pTemp2.use_count() == 2);
}
// pTemp1 and pTemp2 go out of scope, the reference count becomes 0, and the pointer is destroyed.

In addition, when using shared_ptr in multithreading, if there are copy or assignment operations, concurrent access to the reference count may lead to an invalid count. The solution is to pass the shared weak_ptr to each thread, and convert the weak_ptr to shared_ptr when the thread needs to use it.

Notes

1. Initialization and assignment.

You can perform direct initialization through the constructor, or change the pointed-to object through the member function reset() or the assignment operator.

2. Circular references.

It should be noted that two std::shared_ptr referencing each other may form a circular reference, resulting in the reference count never reaching zero and the resource never being released. In this case, you should properly use std::weak_ptr to break the circular reference.

3. Performance overhead.

Although std::shared_ptr brings convenience, every addition or removal of a reference requires updating the reference count, which brings a certain performance overhead. Therefore, for small objects that are frequently created and destroyed or scenarios with a single owner, std::unique_ptr may be more suitable.

4. Exception safety.

std::shared_ptr provides an exception safety guarantee: even if an exception is thrown during construction or assignment, no memory leak will occur.

Summary

std::shared_ptr plays a critical role in C++ programming. Its introduction has greatly simplified memory management tasks and improved the security and reliability of code. However, like any tool, std::shared_ptr can only deliver maximum value when understood and used correctly. Developers must always pay attention to potential circular reference problems and performance overhead to properly apply this powerful tool in practical projects.


This is a discussion topic separated from the original thread at https://juejin.cn/post/7368855076130553875