KoreanFoodie's Study

C++ 기초 개념 13-2 : shared_ptr 와 weak_ptr 본문

Tutorials/C++ : Beginner

C++ 기초 개념 13-2 : shared_ptr 와 weak_ptr

GoldGiver 2022. 4. 19. 10:23

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

shared_ptr

어떤 객체의 경우, 여러 포인터에서 참조를 하여 사용하는 경우가 있다. 이때, 해당 객체를 참조하는 포인터의 수가 0이 되었을 때 해당 객체를 메모리로부터 해제해주는 포인터가 필요한데, 이것이 바로 shared_ptr이다.

std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1);

// unique_ptr 일 경우 소유권 문제로 컴파일 오류가 발생한다.
std::unique_ptr<A> p3(new A());
std::shared_ptr<A> p4(p3); // 컴파일 오류

 

위 사진처럼, shared_ptr 는 같은 객체를 가리킬 수 있고, 이때 참조 개수(reference count)를 카운팅한다. 예제 코드를 보자.

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

class A {

	int *data;

public:
	A() {	
		std::cout << "메모리 획득!" << std::endl;
		data = new int[100];	
	}
	~A() { 
		std::cout << "메모리 해제!" << std::endl;
		delete[] data; 
	}
};

int main() {

	std::vector<std::shared_ptr<A>> vec;

	vec.push_back(std::shared_ptr<A>(new A()));
	vec.push_back(std::shared_ptr<A>(vec[0]));
	vec.push_back(std::shared_ptr<A>(vec[1]));

	std::cout << "===첫번째 원소 소멸!===" << std::endl;
	vec.erase(vec.begin());

	std::cout << "===두번째 원소 소멸!===" << std::endl;
	vec.erase(vec.begin());

	std::cout << "===세번째 원소 소멸!===" << std::endl;
	vec.erase(vec.begin());
}

 

위의 코드를 돌리면 다음과 같은 결과가 나오는데, 이를 통해 참조 개수가 0일때 소멸자가 호출됨을 알 수 있다!

메모리 획득!
===첫번째 원소 소멸!===
===두번째 원소 소멸!===
===세번째 원소 소멸!===
메모리 해제!

 

참조 갯수는 다음과 같이 체크해 볼 수 있다.

std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1);

std::cout << p1.use_count(); // 2
std::cout << p2.use_count(); // 2

 

shared_ptr 는 어떻게 동기화를 할까?

참조 개수는 어디에 저장될까? 만약 p1, p2 가 같은 객체를 가리키는 상황에서 p3 가 추가된다고 해 보자. 만약 참조 개수가 p1에 저장되어 있으면, p1에 접근해야 하는 위험성이 생기며, 동시에 p1이 해제될 때 참조 개수 접근이 불가능하다.

따라서 위의 그림과 같이, 제어 블록(control block)을 따로 동적으로 할당하여(최조 shared_ptr가 생성) shared_ptr 가 생성될 시 주소값을 전달한다.

 

 

make_shared 사용하기

std::shared_ptr<A> p1(new A());

이전에 shared_ptr 를 할당할 때는 다음과 같은 구문을 이용했다. 하지만 이는 A를 먼저 동적할당 하고, 다시 제어 블록(control block)을 동적할당 해야하므로 두 번의 동적할당이 발생한다.

 

만약 make_shared 를 이용하면, 객체 A와 제어 블록을 동시에 동적할당할 수 있다!

std::shared_ptr<A> p1 = std::make_shared<A>();
// A의 생성자에 인자를 전달하고 싶으면..
std::shared_ptr<A> p2 = std::make_shared<A>(1);

 

 

shared_ptr 생성 시 주의할 점

shared_pointer 는 인자로 주소값이 전달되면, 마치 자기가 해당 객체를 첫 번째로 소유하는 shared_ptr 인 것마냥 행동한다. 예를 들어,

class A {};

int main() {
    A* a = new A();

    std::shared_ptr<A> p1(a);
    std::shared_ptr<A> p2(a);
}

 

다음과 같은 코드는 p1, p2 에 대해 두 개의 제어 블록을 각기 생성한다.

 

따라서 아래와 같은 경우, 참조 카운트가 제대로 카운팅이 되지 않아 해제된 객체를 다시 해제하는 오류가 발생할 수 있다. 다음 코드를 보자.

#include <iostream>
#include <memory>

class A {

int* data;

public:
    A() {
        data = new int[100];
        std::cout << "Resource Aqusition!" << std::endl;
    }

    ~A() {
        delete[] data;
        std::cout << "Resource Freed!" << std::endl;
    }
};

int main() {
    A* a = new A();

    std::shared_ptr<A> p1(a);
    std::shared_ptr<A> p2(a);

    std::cout << p1.use_count() << std::endl;
    std::cout << p2.use_count() << std::endl;
}

p1과 p2 모두 use_count( )는 1을 리턴한다.

주소값을 통해 shared_pointer 를 생성하는 것은 지양해야 하지만, 다음과 같은 어쩔 수 없는 경우가 있을 수도 있다.

 

    // class A 에 다음과 같은 멤버 함수 추가
    std::shared_ptr<A> get_shared_ptr() { return std::shared_ptr<A>(this); }

int main() {
    std::shared_ptr<A> p1 = std::make_shared<A>();
    std::shared_ptr<A> p2 = a->get_shared_ptr();

    std::cout << p1.use_count() << std::endl;
    std::cout << p2.use_count() << std::endl;
}

위와 같은 코드도, 마찬가지로 use_count( )는 모두 1을 리턴한다.

 

enable_shared_from this

이 경우, 클래스가 enable_shared_from this 를 상속받게 만듦으로써 참조 카운트 문제를 해결할 수 있다.

class A : public std::enable_shared_from_this<A> {
  int *data;

 public:
  A() {
    data = new int[100];
    std::cout << "Resource Obtained!" << std::endl;
  }

  ~A() {
    std::cout << "Resource Freed!" << std::endl;
    delete[] data;
  }

  std::shared_ptr<A> get_shared_ptr() { return shared_from_this(); }
};

int main() {
  std::shared_ptr<A> pa1 = std::make_shared<A>();
  std::shared_ptr<A> pa2 = pa1->get_shared_ptr();

  std::cout << pa1.use_count() << std::endl;
  std::cout << pa2.use_count() << std::endl; 
}

주의할 점은, shared_from_this( ) 함수의 경우 이미 생성된 제어 블록을 이용해 새로운  shared_ptr 를 만들어내는 방식이기 때문에, 제어 블록이 생성되지 않은 상태일 경우 shared_from_this( ) 를 사용하면 에러가 발생한다.

 

  A* a = new A();
  std::shared_ptr<A> p1 = a->get_shared_ptr();
  
  /* 에러 발생
  terminate called after throwing an instance of 'std::bad_weak_ptr'
  what():  bad_weak_ptr
  */

 

 

서로 참조하는 shared_ptr 

만약 각 객체의 shared_ptr 가 서로의 객체를 참조한다면 어떨까?

class A : public std::enable_shared_from_this<A> {
  int *data;
  std::shared_ptr<A> other;

 public:
  A() {
    data = new int[100];
    std::cout << "Resource Obtained!" << std::endl;
  }

  ~A() {
    std::cout << "Resource Freed!" << std::endl;
    delete[] data;
  }

  void set_other(std::shared_ptr<A> o) { other = o; }
};

int main() {

  std::shared_ptr<A> p1 = std::make_shared<A>();
  std::shared_ptr<A> p2 = std::make_shared<A>();
  
  p1->set_other(p2);
  p2->set_other(p1);

}

 

위와 같은 경우, 소멸자가 제대로 호출되지 않는다!

Resource Obtained!
Resource Obtained!

 

 

weak_ptr

트리 구조를 지원하는 클래스를 만들어 보자.

class Node {
std::vector<std::shared_ptr<Node>> children;
/* 어떤 타입이 와야 할까? */ parent;

public:
  Node() {};
  void AddChild(std::shared_ptr<Node> node) {
    children.push_back(node);
  }
};

만약 parent 타입을 Node* 로 하게 된다면, 메모리가 해제되지 않을 위험성이 있다. 그리고 shared_ptr 로 하게 된다면 부모와 자식이 서로를 순환참조하므로, 메모리가 절대 해제되지 않는다.

이럴때 사용하는 것이 바로 weak_ptr 이다. weak_ptr 는 자체로는 참조 개수를 늘리지 않는다. 그래서 실제로 사용할 때는 shared_ptr 로 변환하여 사용해야 하는데, 만약 해당 객체가 이미 소멸되었으면 빈 shared_ptr 로 변환되고, 아닐 경우 해당 객체를 가리키는 shared_ptr 로 변환된다.

 

코드를 통해 작동 원리를 이해해 보자.

#include <windows.h>
#include <iostream>
#include <memory>
#include <vector>
#include <string>


class A {

  std::string s;
  std::weak_ptr<A> other;

public:

  A(const std::string& s) : s(s) { 
    std::cout << "자원을 획득함!" << std::endl;
  }

  ~A() {
    std::cout << "소멸자 호출!" << std::endl;
  }

  void set_other(std::weak_ptr<A> o) {
    other = o;
  }
  void access_other() {
    std::shared_ptr<A> o = other.lock();
    if (o) {
      std::cout << "접근 : " << o->name() << std::endl;
    } else {
      std::cout << "이미 소멸됨 ㅠ" << std::endl;
    }
  }

  std::string name() { return s; }
};

int main() {
  system("chcp 65001"); // g++ 한글 깨짐

  std::vector<std::shared_ptr<A>> vec;
  vec.push_back(std::make_shared<A>("자원 1"));
  vec.push_back(std::make_shared<A>("자원 2"));
  
  vec[0]->set_other(vec[1]);
  vec[1]->set_other(vec[0]);

  // vec[0]과 vec[1]의 ref count는 그대로다.
  std::cout << "vec[0] ref count : " << vec[0].use_count() << std::endl;
  std::cout << "vec[1] ref count : " << vec[1].use_count() << std::endl;
  
  // weak_ptr 로 해당 객체 접근
  vec[0]->access_other();

  // 벡터 마지막 원소 제거 (vec[1] 소멸)
  vec.pop_back();
  vec[0]->access_other(); // 접근 실패!

}

 

결과는 다음과 같다.

자원을 획득함!
자원을 획득함!
vec[0] ref count : 1
vec[1] ref count : 1
접근 : 자원 2
소멸자 호출!
이미 소멸됨 ㅠ
소멸자 호출!

 

먼저 set_other 함수의 인자는 weak_ptr 를 인자로 받고 있었는데, 여기에 shared_ptr 을 전달했다. weak_ptr 는 생성자로 shared_ptr 나 weak_ptr 를 받는데, 제어 블록을 생성할 수 없어 이미 제어 블록이 만들어진 객체를 전달 받아야 한다.

 

void access_other() {
  std::shared_ptr<A> o = other.lock();
  if (o) {
    std::cout << "접근 : " << o->name() << std::endl;
  } else {
    std::cout << "이미 소멸됨 ㅠ" << std::endl;
  }
}

wak_ptr 그 자체로는 원소를 참조할 수 없고, shared_ptr 로 변환해야 하는데, 이 과정은 lock 함수를 통해 수행된다.

weak_ptr 의 lock 함수는 weak_ptr 가 가리키는 객체가 아직 메모리에서 살아 있다면 (참조 개수가 0이 아니라면) 해당 객체를 가리키는 shared_ptr 를 반환하고, 이미 해제가 되었다면 아무것도 가리키지 않는 shared_ptr 를 반환한다.

참고로 아무것도 가리키지 않는 shared_ptr 는 false 로 형변환 되므로 위와 같이 if 로 체크할 수 있다.

 

참조 개수가 0이 되면 객체는 메모리에서 해제된다. 그렇다면 제어 블록도 해제가 되는 걸까?

해당 객체를 가리키는 shared_ptr 가 없으면 객체는 이제 자유이지만, 해당 객체를 가리키는 weak_ptr 가 남아있을 수 있다. 만약 제어 블록이 해제되면 제어 블록의 참조 카운트가 0이라는 것을 알 수 없게 된다!

실제로 메로리가 해제된 이후에 같은 자리가 다른 용도로 할당될 수 있다. 따라서 참조 카운트 위치에 있는 메모리가 다른 값으로 덮어 씌어질 수도 있다.

즉, 제어 블록을 해제하기 위해서는 이를 가리키는 weak_ptr 역시 0개여야 한다. 따라서 제어 블록에는 참조 개수와 더불어 약한 참조 개수 (weak count) 를 기록하게 된다.

 

 

문제 풀이 :

더보기

모두의 코드 13-2 강좌 하단에 있는 문제에 대해 간단한 코드를 짜 보았다. (미완성, 가계도 관리 라이브러리 코드를 짜려고 했는데, 명세대로 맞추어 짜려니 생각보다 쉽지가 않음... 일부일처제로 가정을 하고, 최대한 명세에 맞게 타입을 맞추려고 했는데 잘 짜신 분들의 코드가 궁금하다!) 

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

class Member {
private :
	std::vector<std::shared_ptr<Member>> children;
	std::vector<std::weak_ptr<Member>> parents;
	std::vector<std::weak_ptr<Member>> spouses;

public:

	friend class FamilyTree;

	static int cnt;
	std::string idx;

	Member() : idx(std::to_string(cnt)) {
		//std::cout << "Member Number : " << idx << std::endl;
		++cnt;
	}

	void AddParent(const std::shared_ptr<Member>& parent);
	void AddSpouse(const std::shared_ptr<Member>& spouse);
	void AddChild(const std::shared_ptr<Member>& child);

	std::vector<std::shared_ptr<Member>> GetChildren() const { return children;	}
	std::vector<std::weak_ptr<Member>> GetParents() const { return parents;	}
	std::vector<std::weak_ptr<Member>> GetSpouses() const {	return spouses;	}
	
};

void Member::AddParent(const std::shared_ptr<Member>& parent) {
	parents.push_back(parent);
}

void Member::AddSpouse(const std::shared_ptr<Member>& spouse) {
	spouses.push_back(spouse);
}

void Member::AddChild(const std::shared_ptr<Member>& child) {
	child->AddParent(std::shared_ptr<Member>(this));
	children.push_back(child);
}

// initialize static variable of Member class
int Member::cnt = 0;



class FamilyTree {
public:
	std::vector<std::shared_ptr<Member>> entire_family;
	// 두 사람 사이의 촌수를 계산
	int CalculateChon(Member* mem1, Member* mem2);
	void addFamily(const std::shared_ptr<Member>& member) {
		entire_family.push_back(member);
	}
};

int FamilyTree::CalculateChon(Member* mem1, Member* mem2) {

	if (mem1 == nullptr || mem2 == nullptr)
		return 0;

	if (mem1 == mem2)
		return 0;	

	// Find LCA - O(n^2)
	Member* lca1 = mem1;
	Member* lca2 = mem2;

	int cnt1 = 0;
	int cnt2 = 0;

	bool exit_flag = false;
	int spouse_cnt = 0;
	Member* ori_mem(mem2);

	while (!(mem1->GetParents()).empty()) {
		
		// mem2 초기화
		mem2 = ori_mem;
		cnt2 = 0;

		while (!(mem2->GetParents()).empty()) {

			if (mem1 == mem2) exit_flag = true;
			if (mem1->GetSpouses()[0].lock() == mem2->GetSpouses()[0].lock()) {
				++spouse_cnt;
				exit_flag = true;
			}

			if (exit_flag) break;

			// mem2 부모 찾기			
			std::vector<std::weak_ptr<Member>> pVec = mem1->GetParents();
			if (!pVec[0].lock()->GetParents().empty()) {
				mem1 = &(*((pVec[1].lock()->GetParents())[0].lock()));
			} else mem1 = &(*((pVec[1].lock()->GetParents())[1].lock()));
			++cnt2;

		}

		if (exit_flag) break;

		// mem1 부모 찾기
		std::vector<std::weak_ptr<Member>> pVec = mem1->GetParents();
		if (!pVec[0].lock()->GetParents().empty()) {
			mem1 = &(*((pVec[1].lock()->GetParents())[0].lock()));
		} else mem1 = &(*((pVec[1].lock()->GetParents())[1].lock()));
		++cnt1;
	}

	return cnt1 + cnt2 + spouse_cnt;
}


int main() {

	std::shared_ptr<Member> m0 = std::make_shared<Member>();
	std::shared_ptr<Member> m1 = std::make_shared<Member>();
	std::shared_ptr<Member> m2 = std::make_shared<Member>();
	std::shared_ptr<Member> m3 = std::make_shared<Member>();
	std::shared_ptr<Member> m4 = std::make_shared<Member>();
	std::shared_ptr<Member> m5 = std::make_shared<Member>();
	std::shared_ptr<Member> m6 = std::make_shared<Member>();

	m0->AddSpouse(m1);
	m0->AddChild(m2);
	m0->AddChild(m3);

	m1->AddSpouse(m0);
	m1->AddChild(m2);
	m1->AddChild(m3);

	m3->AddSpouse(m4);
	m3->AddChild(m5);
	m3->AddChild(m6);

	m4->AddSpouse(m3);
	m4->AddChild(m5);
	m4->AddChild(m6);

	FamilyTree ft;
	ft.addFamily(m0);
	ft.addFamily(m1);
	ft.addFamily(m2);
	ft.addFamily(m3);
	ft.addFamily(m4);
	ft.addFamily(m5);
	ft.addFamily(m6);

	std::cout << "CalculateChon between 0 and 0 : " << ft.CalculateChon(&(*ft.entire_family[0]), &(*ft.entire_family[0])) << std::endl;

}
 
Comments