What is std::shared_ptr? Examples

std::shared_ptr is a smart pointer that manages shared ownership of an object through pointer. Multiple std::shared_ptr (s) can hold the same object pointer. The actual object gets deleted when all the std::shared_ptr (s) go out of scope or get deleted/reset. It was introduced in C++11.

It helps C++ programmers cleaning up memory in very convenient way. Programmers often forget to delete or clean up the allocated objects. Sometimes, it is difficult to decide when or where to delete the objects if we pass the pointers to multiple functions or threads.

If we pass a pointer to multiple threads and delete the object from one thread, the other threads will have problem in accessing the object through the pointer. Deleting the object after determining when all threads finish their tasks is a complex task. Here we’ll see how shared pointers make our job easy in this scenario.

We’ll use this class in our examples.

class C {
public:
  C() {
    std::cout << "Constructor called." << std::endl;
  }

  ~C() {
    std::cout << "Destructor called." << std::endl;
  }

  void func() {
    std::cout << "Func called." << std::endl;
  }

};

Creating the std::shared_ptr

We can create and use a shared pointer of the above class like this.

std::shared_ptr<C> p = std::make_shared<C>();
p->func();
std::cout << "Use count: " << p.use_count() << ", p.get(): " << p.get() << std::endl;
p.reset();
std::cout << "Use count: " << p.use_count() << ", p.get(): " << p.get() << std::endl;

Output of the above code snippet will be like this.

Constructor called.
Func called.
Use count: 1, p.get(): 0x1b2c2b8
Destructor called.
Use count: 0, p.get(): 0

We can use this output to understand the code.

“Constructor called.”

When we create the smart pointer from “std::shared_ptr p = std::make_shared();“, the object is created. The std::make_shared() function allocates the object of type C. So the constructor was called and above line got printed.

Now we can use the shared pointer, p, like a normal pointer of C.

“Func called.”

In the next line, we called the function “a->func()” that printed the above line.

Use count: 1, p.get(): 0x1b2c2b8

This line shows some information about the shared pointer. Use count suggests how many shared pointers are sharing the object at that moment. So far only shared pointer has the ownership of the object, so the counter is 1. “p.get()” simply returns the address (pointer) of the actual object.

“Destructor called.”

By the next line “p->reset()“, shared pointer, p, releases the ownership of the object. As no other shared pointer has the ownership of the object, the object gets deleted. So the above line printed from the destructor.

If we print the use count and the address of the object again, we’ll get this print.

“Use count: 0, p.get(): 0”

As no shared pointer is now owing the object, the use count is 0. The p.get() is 0 suggests that the shared pointer, p, released the ownership of the object.

Multiple Ownership

Unlike unique pointers, multiple shared pointers can own a particular object. Lets consider this code snippet.

std::shared_ptr<C> p = std::make_shared<C>();
std::shared_ptr<C> q = p;
p->func();
std::cout << "Use count (p): " << p.use_count() << ", p.get(): " << p.get() << std::endl;
p.reset();
std::cout << "Use count (p): " << p.use_count() << ", p.get(): " << p.get() << std::endl;

std::cout << "Use count (q): " << q.use_count() << ", q.get(): " << q.get() << std::endl;
q.reset();
std::cout << "Use count (q): " << q.use_count() << ", q.get(): " << q.get() << std::endl;

Here we created the shared pointer, p, and then assigned that to q. Here is the output of the above code.

Constructor called.
Func called.
Use count (p): 2, p.get(): 0x1c022b8
Use count (p): 0, p.get(): 0
Use count (q): 1, q.get(): 0x1c022b8
Destructor called.
Use count (q): 0, q.get(): 0

Lets walk through this output.

“Constructor called.”

When we created the shared pointer, p, the actual object is created. So the above line came from the constructor. But when we assigned p to q, no constructor called. That means no new object was created. At this moment, both p and q own the object.

We can call the func() function either using p or q.

“Func called.”

This line came from the “p->func()” call.

“Use count (p): 2, p.get(): 0x1c022b8”

Here use count is 2. That suggests that two shared pointers own the object. “p->get()” returns the address of the object, 0x1c022b8.

After “p.reset()” call, p will release the ownership of the object. The following line suggests that.

“Use count (p): 0, p.get(): 0”

We used shared pointer p, which is already reset, to print the above line. That’s why the use count is 0. This is not the actual use count. If the shared pointer is reset, then it releases the ownership of the object and does not maintain any information about the object. It will always show use count 0. To get the actual use count, we have the use the other shared pointer q.

Here we did not see the print from the destructor yet. That means the object is still alive.

“Use count (q): 1, q.get(): 0x1c022b8”

This line suggests that one shared pointer is still owning the object. Shared pointer p released the ownership but q is still holding that. We can see that both “p->get()” and “q->get()” returned the same address 0x1c022b8 .

“Destructor called.”

After we called “q.reset()“, the destructor called and the line above got printed.

“Use count (q): 0, q.get(): 0”

This line suggests that q also released the ownership.

Passing Shared Pointer to Function

This code snippet shows how to pass shared pointer to a function.

void f1(std::shared_ptr<C> in) {
  std::cout << "Calling in.func() from f1()..." << std::endl;
  in->func();
  std::cout << "Returning from f1()..." << std::endl;
}

int main() {
  std::shared_ptr<C> p = std::make_shared<C>();
  f1(p);
  std::cout << "Returning from main()" << std::endl;
  return 0;
}

Here is the output of this code.

Constructor called.
Calling in.func() from f1()...
Func called.
Returning from f1()...
Returning from main()
Destructor called.

If a shared pointer passed to a function, the shared pointer in the function will also own the object. The use count will be incremented. This example shows that when both f1() and main() returned, the object got deleted.

Passing Shared Pointer to Threads

The real benefit we get from shared pointers, when we pass them to multiple threads. If we use normal pointer, then it is very difficult to delete the pointer as every thread runs sort of independently. Deleting the pointer from one thread is really risky. Other threads might access that pointer at that time. If you use shared pointers, you don’t to worry about this at all.

Lets see this code snippet.

int main() {
  std::shared_ptr<C> p = std::make_shared<C>();

  std::thread t1([](std::shared_ptr<C> a) {
      std::cout << "Waiting for 1 second..." << std::endl;
      std::this_thread::sleep_for (std::chrono::seconds(1));
      std::cout << "Calling a->func() from thread 1: ";
      a->func();
      std::cout << "Returning from thread 1..." << std::endl;
    }, p);

  std::thread t2([](std::shared_ptr<C> a) {
      std::cout << "Waiting for 2 second..." << std::endl;
      std::this_thread::sleep_for (std::chrono::seconds(2));
      std::cout << "Calling a->func() from thread 2: ";
      a->func();
      std::cout << "Returning from thread 2..." << std::endl;
    }, p);

  std::cout << "Resetting p..." << std::endl;
  p.reset();
  std::cout << "Use count: " << p.use_count() << ", p.get(): " << p.get() << std::endl;
  t1.join();
  t2.join();

  return 0;
}

Here is the output of this code.

Constructor called.
Resetting p...
Use count: 0, p.get(): 0
Waiting for 1 second...
Waiting for 2 second...
Calling a->func() from thread 1: Func called.
Returning from thread 1...
Calling a->func() from thread 2: Func called.
Returning from thread 2...
Destructor called.

Lets use this output to understand what really happened.

“Constructor called.”

Constructor is called when we created the shared pointer, p.

We passed this shared pointer to two threads. Then we reset the original shared pointer.

“Resetting p…
Use count: 0, p.get(): 0″

After reset, p becomes unusable as “p.get()” returns 0. But the object is not deleted yet because shared pointers in two child threads are still owing that. So, no destructor is called.

“Waiting for 1 second…
Waiting for 2 second…”

First thread waits for 1 second and second thread waits for 2 second.

“Calling a->func() from thread 1: Func called.
Returning from thread 1…”

After 1 second, we called “a->func()” from thread 1 and returned from thread function. The object is not yet deleted because the shared pointer in thread 2 is still owning the object.

“Calling a->func() from thread 2: Func called.
Returning from thread 2…
Destructor called.”

After 2 seconds, we called “a->func()” and returned from thread 2. When we returned from thread 2, all shared pointers is referring to the object. So the object is deleted and the destructor is called.

In this example we can see that, if we create a shared pointer, we can assign that to other variables, we can pass that to other functions or threads. But we don’t need to worry about when and where to delete the object. System will delete the object automatically when there will be no reference to the object. This is a great relief to programmers in a complex code structure.

Author: Srikanta

I write here to help the readers learn and understand computer programing, algorithms, networking, OS concepts etc. in a simple way. I have 20 years of working experience in computer networking and industrial automation.


If you also want to contribute, click here.

Leave a Reply

Your email address will not be published. Required fields are marked *

1
1
0
3
1
0