KoreanFoodie's Study
C++ 기초 개념 13-2 : shared_ptr 와 weak_ptr 본문
모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 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;
}
'Tutorials > C++ : Beginner' 카테고리의 다른 글
C++ 기초 개념 15-1 : 쓰레드(thread)의 기초와 실습 (2) | 2022.04.19 |
---|---|
C++ 기초 개념 14 : 함수를 객체로 사용하기 (std::function, std::mem_fn, std::bind) (0) | 2022.04.19 |
C++ 기초 개념 13-1 : RAII 패턴과 unique_ptr (0) | 2022.04.19 |
C++ 기초 개념 12-2 : Move 문법, 완벽한 전달과 레퍼런스 겹침 (0) | 2022.04.19 |
C++ 기초 개념 12-1 : 우측값, 이동 생성자와 우측값 레퍼런스 (0) | 2022.04.19 |