14
Oct
2021

Smart Pointers (3/6): std::shared_ptr

Automatic memory management alias garbage collector provides release of memory which was allocated by the program, but, then, is no longer referenced. C++ has no explicit garbage collector mechanism.
Quotation from Bjarne Strousb:
“I don’t like garbage. I don’t like littering. My ideal is to eliminate the need for a garbage colletor by not producting any garbage. That is now possible. Tools supporting and enforcing the programming techniques that achieves that are being produced.”
https://www.stroustrup.com/bs_faq.html#garbage-collection

Since C++11, we have a strong resource-owning and resource-sharing library to manage the lifetime of object: std::shared_ptr.
Shared Smart Pointer provides more than one ownership during the lifetime of the object.
Shared pointer class instances may own the same object. Sharing mechanism is working on copying, moving and assigning over the share pointer objects.
Shared pointer class holds two pointers contrast unique pointer class. These pointers are the owned object pointer and control block pointer.
What is the control block?
Unique pointers don’t share any object. It only holds the owns object. But, If we want to share this owns an object, we need a bigger mechanism. We need to keep more data-related sharing such as counters, the deleter. That’s why the size of the shared pointer is bigger than the unique pointer.

As we said, the shared pointer contains two pointers, one of them is owned object pointer, the other is the control block pointer. The control block’s memory is not part of the shared pointer class. The shared pointer object only keeps its pointer.
Suppose we have a class named embeddedWorld. We create an embeddedWorld class object with a shared pointer. And totally, three shared_pointer points to the same object.
Every new shared pointer increments the reference count from the control block. If the shared pointer goes out of scope or resets the reference count decrements. When the reference count reaches zero the owned object is deleted by the control block.
The diagram of this example and the object is pointed in three-way code are below.

 

The diagram of three shared pointer instances

 

#include <iostream>
#include <memory>

using namespace std;
class embeddedWorld
{
	public:
		embeddedWorld(int p_no): planetNo(p_no) {}

	int getPlanetNo() const
	{
		return planetNo;
	};

	private:
		int planetNo = 0;
};

int main()
{

	std::shared_ptr<embeddedWorld> e1(make_shared<embeddedWorld> (10));

	std::shared_ptr<embeddedWorld> e2;

	e2 = e1;	//copy assignment

	std::shared_ptr<embeddedWorld> e3(e2);	//copy constructor

	cout << "e3.use_count(): " << e3.use_count() << endl;

	return 0;
}

The output is the code above: “e3.use_count(): 3”


The Aliasing Constructor: shared_ptr( const shared_ptr& r, element_type* ptr ) noexcept

A shared pointer instance can be constructed with another shared pointer (r) to share ownership but it can hold a different pointer (ptr). When we call the pointer always return ptr. When all shared pointers go out of scope or reset, the unmanaged pointer ptr remains. The responsibilities of unmanaged pointer ptr belong to us.
Here is a little example of using the aliasing constructor.

#include <iostream>
#include <memory>
using namespace std;
class embeddedWorld
{
	public:
		embeddedWorld(int p_no): planetNo(p_no) {}
	~embeddedWorld()
		{
			cout << "called Embedded World Destructor, planet No: " << planetNo << endl;
		};

	int getPlanetNo() const
	{
		return planetNo;
	};

	private:
		int planetNo = 0;
};

int main()
{

	{
		std::shared_ptr<embeddedWorld> e1(make_shared<embeddedWorld> (10));

		std::shared_ptr<embeddedWorld> e2 = e1;

		embeddedWorld *myEmbeddedWorld = new embeddedWorld(5);
		std::shared_ptr<embeddedWorld> e3(e2, myEmbeddedWorld);

		cout << "e1 or e2 planet No: " << e1->getPlanetNo() << endl;
		cout << "e3 planet No: " << e3->getPlanetNo() << endl;
	}

	return 0;
}

Output:

“e1 or e2 planet No: 10
e3 planet No: 5
called Embedded World Destructor, planet No: 10″

If we take a look at the code above:

We create two instances with shared ownership and the pointer. The pointer of the class embeddedWorld’s planet number is 10; afterward, we create an instance named e3, sharing ownerships with e1 and e2 but it holds the new embeddedWorld object pointer named myEmbeddedWorld. Its planet number is also 5.

All instances are localized with braces in the main function due to observing all instances going out of scope.

When all of them go out of scope, the object pointer myEmbeddedWorld remains but we have no any way to access it. That’s why we need to make sure the unmanaged pointer, myEmbeddedWorld remains valid as long as that owned object exists.


Using std::shared_ptr<T>(this)

Using this inside the managed object may result in memory crash problems. Because when we use this, std::shared_ptr<T>(this) creates a new shared pointer that has a new control block and ownership. The usage in the managed object already has ownership.

Let’s take a look at this example:

 

#include <iostream>
#include <memory>
#include <vector>

using namespace std;
class embeddedWorld;

std::vector<std::shared_ptr<embeddedWorld> unionWorlds;

 class embeddedWorld
 {
	public:
		embeddedWorld(int p_no): planetNo(p_no) {}

	int getPlanetNo() const
	{
		return planetNo;
	};

	void saveToUnion()
	{
		unionWorlds.push_back(std::shared_ptr<embeddedWorld> (this));
	}

	private:
		int planetNo = 0;
 };

 int main()
  {

	{
		std::shared_ptr<embeddedWorld> e1(make_shared<embeddedWorld> (10));

		e1->saveToUnion();
	}
	cout << unionWorlds.at(0).use_count() << endl;
	cout << unionWorlds.at(0)->getPlanetNo();

	return 0;
  }

Our managed class is embeddedWorld and it has a function to save itself to unionWorld collect class such as std::vector. When it saves itself in the vector with std::shared_ptr<embeddedWorld>(this), then, the new shared pointer instance is created with the new control block. There will be two shared smart pointers with different ownership but the same object (this).

When the e1 instance goes out of scope, and we write it in localized block to see this risk, the managed object (this) will be deleted with its deleter. Afterward, when we want to reach the object via the vector, unionWorld, we will get the different instances with the deleted resources (this).

For preventing this issue, the library: memory has a class we can publicly inherit: std::enable_shared_from_this. It provides a function named shared_from_this to return itself with the same ownership. This technic is also called CRTP 

When we change the example above, with these technics:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;
class embeddedWorld;

std::vector<std::shared_ptr<embeddedWorld> unionWorlds;

class embeddedWorld: public std::enable_shared_from_this < embeddedWorld>
{
	public: embeddedWorld(int p_no): planetNo(p_no) {}

	int getPlanetNo() const
	{
		return planetNo;
	};

	void saveToUnion()
	{
		unionWorlds.emplace_back(shared_from_this());
	}

	private: int planetNo = 0;
};

int main()
{

	{
	std::shared_ptr<embeddedWorld> e1(make_shared<embeddedWorld> (10));

	e1->saveToUnion();
	cout << e1.use_count() << endl;	// we see: 2

	}

	cout << unionWorlds.at(0).use_count() << endl;	// we see: 2 also
	cout << unionWorlds.at(0)->getPlanetNo();	// There is no risk anymore
	return 0;
}

The function, std::make_shared provides advantages for performance and safety.

We talked about the advantage of using a kind of make smart pointers functions at unique pointer side. Using the make_share function prevents memory leakage.

If we want to use a custom deleter, we need to know the make_shared function doesn’t allow a custom deleter.

Now, we will create our own custom shared pointer class in the next article.

Smart Pointers (4/6): std::weak_ptr

Resources:
std::shared_ptr
std::make_shared
Effective Modern C++ by Scott Meyers