Null vs. Empty shared_ptr

Initializing a shared_ptr with nullptr is as straightforward as anyone would expect it to be. There is absolutely no difference between the two shared_ptr instances shown below. Although they are created with different constructors, they are both holding nullptr , and therefore, both of them can be treated as null pointers:

//Uses default constructor: shared_ptr(); std::shared_ptr<int> p1; //Uses constructor: shared_ptr(std::nullptr_t); std::shared_ptr<int> p2(nullptr); //They are both null std::cout << std::boolalpha << !p1 << " " << !p2 << "

"; //true true

And, the below shared_ptr initialized with a nullptr of the exact type ( int *) also holds a nullptr . This shared_ptr is created using a different constructor, and it is same as p1 and p2 for the most apparent purposes:

//Uses constructor: template<class Y> // explicit shared_ptr(Y* ptr); int* iptr{nullptr}; std::shared_ptr<int> p3(iptr); //null check std::cout << std::boolalpha << !p3 << "

"; //true

However, p3 is different from p1 and p2. Both p1 and p2 are null, but they are empty too because they don't have any control block associated with them. On the other hand, p3 is null but not empty because it has a control block with a managed nullptr and a reference count of 1. In brief, a control block is a data structure through which several shared_ptr instances share ownership of a managed object. Simply speaking, a control block keeps a pointer to the managed object and a reference counter, among other bookkeeping data. The nextptr article "shared_ptr - basics and internals with examples" has in-depth coverage of the control block.

It is easy to verify the reference count of these shared_ptr instances:

//ref count: 0 std::cout << p1.use_count() << "

"; //0 //ref count: 0 std::cout << p2.use_count() << "

"; //0 //ref count: 1 std::cout << p3.use_count() << "

"; //1

Here is a pictorial representation of the memory layout of all the three shared_ptr instances:

So, what happens when we copy these shared_ptr instances? Well, copying p1 (or p2) creates another shared_ptr that shares nothing with it because there is nothing to share. Whereas, copying p3 creates a shared_ptr that shares the ownership of the managed nullptr with it:

//p1's (or p2's) copy is null and empty auto p1c = p1; //ref count: 0 std::cout << p1c.use_count() << "

"; //0 //p3 is different. ref count increases with copy. auto p3c = p3; //ref count: 2 std::cout << p3c.use_count() << "

"; //2

Interestingly, it is also possible for a shared_ptr to be non-null but still be empty. That is achieved through the aliasing constructor. With aliasing constructor, we can create a shared_ptr that points to an object but shares the ownership of a completely unrelated object. Here is an example of how we create a shared_ptr that is empty but still pointing to an object:

int x = 100; //'px' holds &x, but is empty. //A null and empty shared_ptr<void> is passed //to aliasing constructor to initialize px std::shared_ptr<int> px(std::shared_ptr<void>(), &x);

Having established the difference between null and empty shared_ptr, let's look at an example where this knowledge is put to use.

Executing code on block exit

A null shared_ptr does serve the same purpose as a raw null pointer. It might indicate the non-availability of data. However, for the most part, there is no reason for a null shared_ptr to possess a control block or a managed nullptr . But we might utilize a non-empty shared_ptr's deleter to execute arbitrary cleanup code on block exit. In the following code, the function spam() acquires a few resources (an open file and a connection to some service) that it has to free on return. There are many conditional returns and the possibility of exceptions in the function. We utilize a null shared_ptr's deleter to clean up the resources in all conditions, as shown:

struct Connection { std::string read(); //Can throw void write(const std::string&); //Can throw //..more interface }; struct Service { static Connection* getConnection(); static void freeConnection(Connection* cp); //more... }; void spam() { //Resource handles std::FILE* fp = nullptr; Connection* cp = nullptr; //The guard's deleter always executes on return/exit //The shared_ptr is null but not empty std::shared_ptr<void> guard(nullptr, [&fp, &cp](void*){ //Always runs. Releases resources. if(fp) std::fclose(fp); if(cp) Service::freeConnection(cp); }); /* There are conditional returns and the possibility of exceptions */ //Open a log file fp = std::fopen("test.log","a"); if(!fp) return; //Get a connection cp = Service::getConnection(); if(!cp) return; //Read from connection auto data = cp->read(); if(data.empty()) return; //Process data... //Write some data to file std::fputs("Some Data", fp); //Write to connection cp->write("Some Data"); //... }

A Question

Let's look at an easy question related to this subject. A shared_ptr, p4, is initialized with nullptr , as shown below. Later, p4 is copied to p4c, and then reset to nullptr again. You have to tell the reference count of the p4 and p4c:

std::shared_ptr<int> p4(static_cast<int*>(nullptr)); //make a copy auto p4c = p4; p4.reset<int>(nullptr); //Print ref counts std::cout << "p4: " << p4.use_count() << " p4c: " << p4c.use_count() << "

";