17
Oct
2021

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

Before talking about the std::weak_ptr, it would be nice to talk about weak and strong type pointers.

As an example of a strong type pointer, std::shared_ptr holds the owner and increases its reference counter. It means every strong type pointer keeps alive its resource individually. This feature causes a big problem sometimes. We will talk about this problem below.
The weak type pointer doesn’t hold the owners and doesn’t increase the strong reference counter. When the weak type pointer needs to access the resource it must be converted to a shared pointer with controlling if it exists.

Here is the basic example of using std::weak_ptr

The function, lock returns this expression: expired() ? shared_ptr() : shared_ptr(*this).

#include <iostream>
#include "memory"
using namespace std;

std::weak_ptr<int> wp;

int main()
{
	auto sp = make_shared<int> (10);
	cout << "sp use_count(): " << sp.use_count() << endl;

	wp = sp;
	cout << "sp use_count(): " << sp.use_count() << endl;

	if (auto sp2 = wp.lock())	//or std::shared_ptr<int> sp2
	{
	  cout << "wp is not expired " << endl;
	  cout << "sp use_count(): " << sp.use_count() << endl;
	}
	else
	{
	  cout << "wp is expired " << endl;
	}

	cout << "after the if block, sp use_count(): " << sp.use_count() << endl;

	return 0;
}

The function expired, returns the state of the object’s existence. If the managed object has been deleted or the use_count function returns 0, the expired function returns true otherwise returns false. In this way, the lock functions check the object indirectly. It creates a new shared_ptr that shares the ownership. If there is no object to create shared_ptr, the returned shared_ptr will be empty. That’s why we have to check if the object was already deleted with the expire or the lock function inside if block. And if the weak pointer is expired, std::bad_weak_ptr is thrown as an exception.

The example above doesn’t explain why we need a weak type pointer. Only, it shows weak pointer’s usage. We can give two reasons with an example to understand why the weak pointer is useful.

The first is the Reference Cycle.

When the objects refer to each other, the reference cycle is created. Especially, if the objects refer to each other with shared_ptr, and, even if all objects go out of scope, the shared_ptr reference counter never reaches 0, hereby, the memory leakage occurs. Here is the reference cycle example code.

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class embeddedWorld
{

	public:
	embeddedWorld(int p_no): planetNo(p_no) {}
	~embeddedWorld()
	{
         cout << "has been destroyed - planet no:" << planetNo << endl;
	}

	void assignNeighbor(const std::shared_ptr<embeddedWorld> &np)
	{
		neighboringWorld = np;
	}

	private:
		int planetNo = 0;
	std::shared_ptr<embeddedWorld> neighboringWorld;
	//std::weak_ptr<embeddedWorld> neighboringWorld;
};

void assignTest()
{

	auto e1 = make_shared<embeddedWorld> (1);

	auto e2 = make_shared<embeddedWorld> (2);

	e1->assignNeighbor(e2);
	e2->assignNeighbor(e1);

	cout << "e1 or e2 use_count(): " << e1.use_count() << endl;	// we see 2 becase of the reference cycle

}

int main()
{
	assignTest();

	return 0;
}

The output:
“e1 or e2 use_count(): 2”

In the code above, two embeddedWorld objects are created as shared_ptr instances inside the assignTest function. They assign themselves with the assignNeighbor function. Each embeddedWorld shared_ptr’s reference counter value is 2. The illustration shows that below.  They never delete the resource even though they go out of scope because of the reference counter’s value. So, the reference cycle is created. And we can’t access them anymore out of the function. It causes memory leakage.

Reference Cycle

 

To break the cycle, we need to use a std::weak_ptr to hold a neighbor object. Using the std::weak_ptr doesn’t increase the reference counter. When each one goes out of scope its resource will be deleted.

We need to change only the neighboringWorld member as a weak_ptr.

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class embeddedWorld
{

	public:
	embeddedWorld(int p_no): planetNo(p_no) {}
	~embeddedWorld()
	{
         cout << "has been destroyed - planet no:" << planetNo << endl;
	}

	void assignNeighbor(const std::shared_ptr<embeddedWorld> &np)
	{
		neighboringWorld = np;
	}

	private:

	int planetNo = 0;
	std::weak_ptr<embeddedWorld> neighboringWorld;
};

void assignTest()
{

	auto e1 = make_shared<embeddedWorld> (1);

	auto e2 = make_shared<embeddedWorld> (2);

	e1->assignNeighbor(e2);
	e2->assignNeighbor(e1);

	cout << "e1 or e2 use_count(): " << e1.use_count() << endl;	// we see 2 becase of the reference cycle

}

int main()
{
	assignTest();

	return 0;
}

The output:
“e1 or e2 use_count(): 1
has been destroyed – planet no:2
has been destroyed – planet no:1 “

The second useful reason is using a design pattern like the observer design pattern with smart pointers. Assume that, we have two classes, one of them is the embeddedWorld as a subject class and the other is council class for observation.

Here is the example using a raw pointer for this approach:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Subject;
class Observer
{
	public:
	virtual void update() const = 0;
	virtual~Observer() = default;
};

class Subject
{

	public:

	virtual~Subject() = default;

	void attach(Observer & o)
	{
		observers.emplace_back(&o);
	}

	void detach(Observer & o)
	{
		observers.erase(std::remove(this->observers.begin(), this->observers.end(), &o));
	}

	void notify()
	{
		cout << "inside notify function" << endl;
		for (auto o: observers)
		{
			o->update();
		}
	}
	private:
		std::vector<Observer*> observers;
};

class embeddedWorld: public Subject
{

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

      ~embeddedWorld()
       {
	cout << "has been destroyed - planet no:" << planetNo << endl;
        }

	int getPlanetNo() const
	{
		return planetNo;
	}

	void doSomeThing()
	{
		this->notify();
	}

   private: int planetNo = 0;

};

class council: public Observer
{
   public:

	council(embeddedWorld &e, int no): couincilNo(no),ewSubject(e)
	{
		ewSubject.attach(*this);
	};

        ~council()
	{
		ewSubject.detach(*this);
	}

	void update() const override
	{
		std::cout << "council number: " << couincilNo << " embeddedWorld's planet no:" << ewSubject.getPlanetNo() << endl;
	}

    private: int couincilNo = 0;
	embeddedWorld &ewSubject;
};

int main()
{

    embeddedWorld e(10);

    {
	  council c1(e, 1);
	  {
	   council c2(e, 2);
           e.doSomeThing();	// two council objects exist!
	  }
	 e.doSomeThing();	// when c2 went out of scope
    }

    e.doSomeThing();	// when two council objects went out of scope!

	return 0;
}

The output of the code above:
“inside notify function
council number: 1 embeddedWorld’s planet no:10
council number: 2 embeddedWorld’s planet no:10
inside notify function
council number: 1 embeddedWorld’s planet no:10
inside notify function
has been destroyed – planet no:10”

The code above is a very common approach for the observer design pattern. What if we want to use smart pointers instead of raw pointers? It’s clear to see we have to use the shared_ptr but if we use a shared_ptr for every raw pointer we can create reference cycles. That’s why we need weak type smart pointers to hold the other pointers. Using the std::weak_ptr with observer design pattern is needed to consider somewhere such as deleting the observer from the container. We have to use the function, std::remove_if there.

Here is the dirty and non-multi thread example:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Subject;
class Observer
{
	public:
	virtual void update() const = 0;
	virtual~Observer() = default;
};

class Subject
{

	public:

	void attach(const std::shared_ptr<Observer> o)
	{
	  observers.emplace_back(o);
	}

	void notify()
	{
	   std::vector<std::shared_ptr < Observer>> validObservers;

	    observers.erase(std::remove_if(this->observers.begin(), this->observers.end(),
		[ &](const std::weak_ptr<Observer> &ow)
			{
				auto os = ow.lock();
				if (os)
				{
					validObservers.push_back(os);
					return false;
				}
				else return true;
                }), this->observers.end());

	      for (auto o: validObservers)
		    o->update();
	}
	private:
		std::vector<std::weak_ptr < Observer>> observers;
};

class embeddedWorld: public Subject
{

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

		~embeddedWorld()
		{
			cout << "has been destroyed - planet no:" << planetNo << endl;
		}

	int getPlanetNo() const
	{
		return planetNo;
	}

	void doSomeThing()
	{
		this->notify();
	}

	private: int planetNo = 0;

};

class council: public Observer
{
	public: static int i;
	council(const shared_ptr<embeddedWorld> s, int no): couincilNo(no),
	wp(s) {};

	void update() const override
	{
		auto sp = wp.lock();

		if (sp)
			std::cout << "council number: " << couincilNo << " embeddedWorld's planet no:" << sp->getPlanetNo() << endl;
	}

	private: int couincilNo = 0;
	std::weak_ptr<embeddedWorld> wp;
};

int main()
{

	auto e = make_shared<embeddedWorld> (11);
	{
		auto c1 = std::make_shared<council> (e, 1);	//council c(e);
		{
			auto c2 = std::make_shared<council> (e, 2);

			e->attach(c1);
			e->attach(c2);
			e->doSomeThing();
		}
		e->doSomeThing();
	}
	e->doSomeThing();

	{
	  auto e2 = make_shared<embeddedWorld> (19);
	}
	cout << "Hello World!" << endl;
	return 0;
}

 

The output:
“council number: 1 embeddedWorld’s planet no:11
council number: 2 embeddedWorld’s planet no:11
council number: 1 embeddedWorld’s planet no:11
has been destroyed – planet no:11″

The Next Article: Smart Pointers (6/6): Final: Custom Deleters, Passing Smart Pointers…

Resources:

std::weak_ptr
std::shared_ptr
Effective Modern C++ by Scott Meyers