M.8 Circular dependency issues with std::shared_ptr, and std::weak_ptr

주홍영·2022년 3월 24일
0

Learncpp.com

목록 보기
198/199

https://www.learncpp.com/cpp-tutorial/circular-dependency-issues-with-stdshared_ptr-and-stdweak_ptr/

이전 레슨에서 우리는 std::shared_ptr이 same resource에 대한 co-owning을 어떻게 가능하게 했는지 살펴보았다. 그러나 특정한 케이스에서 이는 문제가 될 수 있다
다음의 케이스를 살펴보자.
shared pointer가 두 개의 object를 각각 pointing하고 있는 경우이다.

#include <iostream>
#include <memory> // for std::shared_ptr
#include <string>

class Person
{
	std::string m_name;
	std::shared_ptr<Person> m_partner; // initially created empty

public:

	Person(const std::string &name): m_name(name)
	{
		std::cout << m_name << " created\n";
	}
	~Person()
	{
		std::cout << m_name << " destroyed\n";
	}

	friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
	{
		if (!p1 || !p2)
			return false;

		p1->m_partner = p2;
		p2->m_partner = p1;

		std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";

		return true;
	}
};

int main()
{
	auto lucy { std::make_shared<Person>("Lucy") }; // create a Person named "Lucy"
	auto ricky { std::make_shared<Person>("Ricky") }; // create a Person named "Ricky"

	partnerUp(lucy, ricky); // Make "Lucy" point to "Ricky" and vice-versa

	return 0;
}

위의 예시에서 dynamically 두 Person object를 allocate하고 있다
그리고 우리는 두 object를 parterUp 이라는 function을 이용해 내부의 member variable에
서로의 shared_ptr을 partner로 할당해주고 있다

그리고 해당 프로그램을 실행해보면 다음과 같은 결과가 나온다

Lucy created
Ricky created
Lucy is now partnered with Ricky

출력을 보면 deallocation이 발생하지 않고 있음을 알 수 있다

partnerUp()함수를 실행하고 "Rickey"를 pointing하고 있는것은 두개의 shared_ptr이다
하나는 ricky object, 또하나는 lucy object 내부의 m_partner

main() 함수의 끝에서 ricky shared_ptr은 scope out하게 된다
이때 ricky는 Person object를 co-own하고 있는 shared_ptr이 있는지 check 하게된다
그런데 Lucy의 m_partner가 존재하므로 Ricky는 deallocate 되지 않는다
이 지점에서 Ricky object는 Lucy의 m_partner가 pointing하고 있고
Lucy object는 lucy와 Ricky의 m_partner가 pointing하고 있다

그리고 lucy share_ptr이 scope를 벗어나면 destory되면서 co-own을 체크하는데
앞서 말했뜻 Lucy object는 lucy와 Ricky's m_partner가 pointing하고 있으므로
Lucy object는 destructor가 작동하지 않는다

프로그램이 끝나고 "Lucy", "Ricky" 라는 Person class object는 모두 deallocate이 되지 않았다. 따라서 memory leak이 발생한다.

이것은 shared ptr이 (circular reference)순환 참조를 형성할 때마다 발생할 수 있습니다.

Circular references

순환 참조는 일련의 참조가 다음을 가르키고 마지막 참조가 처음을 가리키는 고리 형태가 되는 경우를 뜻한다. 이때 참조가 c++에서 의미하는 reference일 필요는 없다

shared ptr의 경우 pointer가 참조의 의미를 가지고 있다

A reductive case

이는 하나의 shared_ptr 만으로도 발생할 수 있다

#include <iostream>
#include <memory> // for std::shared_ptr

class Resource
{
public:
	std::shared_ptr<Resource> m_ptr; // initially created empty

	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
	auto ptr1 { std::make_shared<Resource>() };

	ptr1->m_ptr = ptr1; // m_ptr is now sharing the Resource that contains it

	return 0;
}

위와 같은 상황에서 m_ptr이 class 객체 스스로를 shared_ptr로 가지고 있으므로
circular ref의 상황이 만들어져 deallocate이 불가능한 상황이다
이처럼 shared_ptr이 하나만 있어도 circular ref는 발생할 수 있다

So what is std::weak_ptr for anyway?

std::weak_prt은 이런 "cyclical ownership"을 해결하기 위해 설계된 스마트 포인터 클래스다
std::weak_ptr은 observer(옵져버)로 shared_ptr이 관리하고 있는 object 혹은 다른 weak_ptr이 관측하고 있는 object를 같이 관측할 수 있다
참고로 weak_ptr은 owner로 간주되지 않는 다는 것을 기억하자

그럼 다음의 예시에서 순환 참조 문제를 어떻게 해결하고 있는지 살펴보자

#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>

class Person
{
	std::string m_name;
	std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr

public:

	Person(const std::string &name): m_name(name)
	{
		std::cout << m_name << " created\n";
	}
	~Person()
	{
		std::cout << m_name << " destroyed\n";
	}

	friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
	{
		if (!p1 || !p2)
			return false;

		p1->m_partner = p2;
		p2->m_partner = p1;

		std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";

		return true;
	}
};

int main()
{
	auto lucy { std::make_shared<Person>("Lucy") };
	auto ricky { std::make_shared<Person>("Ricky") };

	partnerUp(lucy, ricky);

	return 0;
}

위 프로그램의 출력은 다음과 같다

Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky destroyed
Lucy destroyed

의도한 functionality가 실현됐음을 알 수 있다
주목할 점은 m_partner가 더 이상 shared_ptr이 아닌 weak_ptr이라는 점을 알 수 있다
따라서 두 shared_ptr이 각자 destroy되면서 object가 deallocate된 것이다
weak_ptr은 owner로 count하지 않기 때문에 순환 참조 문제를 해결할 수 있었다

Using std::weak_ptr

std::weak_ptr의 단점은 directly 사용 가능하지 않다는 점이다
왜냐하면 overload된 operator 중에 ->가 없다
std::weak_ptr을 사용하기 위해서는 먼저 std::shared_ptr로 converting을 해야 한다

예시를 살펴보자

#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>

class Person
{
	std::string m_name;
	std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr

public:

	Person(const std::string &name) : m_name(name)
	{
		std::cout << m_name << " created\n";
	}
	~Person()
	{
		std::cout << m_name << " destroyed\n";
	}

	friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
	{
		if (!p1 || !p2)
			return false;

		p1->m_partner = p2;
		p2->m_partner = p1;

		std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";

		return true;
	}

	const std::shared_ptr<Person> getPartner() const { return m_partner.lock(); } // use lock() to convert weak_ptr to shared_ptr
	const std::string& getName() const { return m_name; }
};

int main()
{
	auto lucy { std::make_shared<Person>("Lucy") };
	auto ricky { std::make_shared<Person>("Ricky") };

	partnerUp(lucy, ricky);

	auto partner = ricky->getPartner(); // get shared_ptr to Ricky's partner
	std::cout << ricky->getName() << "'s partner is: " << partner->getName() << '\n';

	return 0;
}

위의 예시에서 getPartner함수를 이용해서 weak_ptr을 이용한다
주목 할 점은 m_partner.lock()로 weak_ptr은 직접 사용할 수 없기에
std::weak_ptr의 member function인 .lock()를 이용해서 shared_ptr로 convert 하여 사용한다
partner는 main을 벗어나면 해제가 되므로 circular dependency problem은 일어나지 않는다

Conclusion

std::shared_ptr은 복수의 스마트 포인터가 한 객체를 co-own 할 때 사용한다
만약 circular ref문제가 발생하는 경우라면 std::weak_ptr을 이용해 문제를 해결할 수 있다
참고로 weak_ptr은 direct로 사용이 불가능하고 .lock()라는 member function을 이용해
shared_ptr로 convert를 시켜서 사용할 수 있다

profile
청룡동거주민

0개의 댓글