C++ weak_ptr | "약한 포인터" 가이드
이 글의 핵심
std::weak_ptr 은 C++11에서 도입된 스마트 포인터로, shared_ptr가 관리하는 객체를 "관찰"만 하고 참조 카운트를 올리지 않습니다. 순환 참조 방지와 캐시·옵저버 패턴에 쓰이며, 스마트 포인터 weak_ptr에서 더 자세히 다룹니다.
weak_ptr이란?
std::weak_ptr 은 C++11에서 도입된 스마트 포인터로, shared_ptr가 관리하는 객체를 “관찰”만 하고 참조 카운트를 올리지 않습니다. 순환 참조 방지와 캐시·옵저버 패턴에 쓰이며, 스마트 포인터 weak_ptr에서 더 자세히 다룹니다.
왜 필요한가?:
- 순환 참조 방지:
shared_ptr간 순환 참조로 인한 메모리 누수 방지 - 캐시: 객체가 사용 중일 때만 캐시 유지
- 관찰자 패턴: 관찰자가 소멸되어도 주체에 영향 없음
- 역참조: 부모-자식 관계에서 자식이 부모를 참조
// ❌ shared_ptr 순환 참조: 메모리 누수
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // 순환 참조
};
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->prev = n1; // 순환 참조 → 메모리 누수
// ✅ weak_ptr: 순환 방지
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 순환 방지
};
스마트 포인터 비교
| 포인터 타입 | 소유권 | 참조 카운트 | 사용 시나리오 |
|---|---|---|---|
| unique_ptr | 독점 | 없음 | 단일 소유자 |
| shared_ptr | 공유 | 증가 | 여러 소유자 |
| weak_ptr | 없음 | 증가 안함 | 관찰, 순환 참조 방지 |
auto shared = std::make_shared<int>(10);
std::weak_ptr<int> weak = shared;
// 참조 카운트 증가 안함
std::cout << shared.use_count() << std::endl; // 1
weak_ptr 동작 원리
graph TD
A[shared_ptr 1] -->|소유| B[객체]
C[shared_ptr 2] -->|소유| B
D[weak_ptr 1] -.->|관찰| B
E[weak_ptr 2] -.->|관찰| B
B -->|참조 카운트| F[2]
style A fill:#90EE90
style C fill:#90EE90
style D fill:#FFB6C1
style E fill:#FFB6C1
사용 방법
weak_ptr 생명주기
sequenceDiagram
participant Code
participant Shared as shared_ptr
participant Weak as weak_ptr
participant Obj as Object
Code->>Shared: make_shared
Shared->>Obj: create (ref=1)
Code->>Weak: from shared
Weak->>Obj: observe (ref=1)
Code->>Weak: lock()
Weak->>Shared: temp shared_ptr
Note over Shared: ref=2
Code->>Code: use
Note over Shared: ref=1
Code->>Shared: destroy shared
Shared->>Obj: destroy (ref=0)
Code->>Weak: lock()
Weak->>Code: nullptr
std::weak_ptr<int> weak;
{
auto shared = std::make_shared<int>(10);
weak = shared;
// lock으로 shared_ptr 얻기
if (auto ptr = weak.lock()) {
std::cout << *ptr << std::endl; // 10
}
}
// shared 소멸 후
if (auto ptr = weak.lock()) {
// 실행 안됨
} else {
std::cout << "만료됨" << std::endl;
}
실전 예시
예시 1: 순환 참조 방지
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 순환 방지
int data;
Node(int d) : data(d) {}
~Node() {
std::cout << "Node " << data << " 소멸" << std::endl;
}
};
int main() {
auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
node1->next = node2;
node2->prev = node1; // weak_ptr
// 자동 소멸
}
예시 2: 캐시
class Cache {
std::map<int, std::weak_ptr<Resource>> cache;
public:
std::shared_ptr<Resource> get(int id) {
auto it = cache.find(id);
if (it != cache.end()) {
if (auto ptr = it->second.lock()) {
return ptr; // 캐시 히트
}
}
// 캐시 미스: 새로 생성
auto ptr = std::make_shared<Resource>(id);
cache[id] = ptr;
return ptr;
}
};
예시 3: 관찰자 패턴
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify() {
// 만료된 관찰자 제거
observers.erase(
std::remove_if(observers.begin(), observers.end(),
{ return weak.expired(); }),
observers.end()
);
// 알림
for (auto& weak : observers) {
if (auto obs = weak.lock()) {
obs->update();
}
}
}
};
예시 4: 부모-자식 관계
class Child;
class Parent {
public:
std::vector<std::shared_ptr<Child>> children;
~Parent() {
std::cout << "Parent 소멸" << std::endl;
}
};
class Child {
public:
std::weak_ptr<Parent> parent; // 순환 방지
~Child() {
std::cout << "Child 소멸" << std::endl;
}
};
멤버 함수
std::weak_ptr<int> weak;
// expired: 만료 확인
if (weak.expired()) {
std::cout << "만료됨" << std::endl;
}
// lock: shared_ptr 얻기
if (auto ptr = weak.lock()) {
std::cout << *ptr << std::endl;
}
// use_count: 참조 카운트
std::cout << weak.use_count() << std::endl;
// reset: 초기화
weak.reset();
자주 발생하는 문제
문제 1: lock 없이 사용
std::weak_ptr<int> weak;
// ❌ 직접 접근 불가
// *weak; // 에러
// weak->func(); // 에러
// ✅ lock 사용
if (auto ptr = weak.lock()) {
*ptr = 20;
}
문제 2: 만료 확인
// ❌ expired 후 lock
if (!weak.expired()) {
auto ptr = weak.lock(); // 사이에 만료될 수 있음
}
// ✅ lock만 사용
if (auto ptr = weak.lock()) {
// 안전
}
문제 3: 순환 참조
// ❌ shared_ptr 순환
class Node {
std::shared_ptr<Node> parent; // 순환
std::shared_ptr<Node> child;
};
// ✅ weak_ptr 사용
class Node {
std::weak_ptr<Node> parent; // 순환 방지
std::shared_ptr<Node> child;
};
문제 4: 캐시 정리
class Cache {
std::map<int, std::weak_ptr<Data>> cache;
public:
void cleanup() {
for (auto it = cache.begin(); it != cache.end();) {
if (it->second.expired()) {
it = cache.erase(it);
} else {
++it;
}
}
}
};
사용 패턴
// 1. 순환 참조 방지
std::weak_ptr<Parent> parent;
// 2. 캐시
std::map<Key, std::weak_ptr<Value>> cache;
// 3. 관찰자
std::vector<std::weak_ptr<Observer>> observers;
// 4. 역참조
std::weak_ptr<Node> backref;
실무 패턴
패턴 1: 타이머 시스템
class Timer {
std::weak_ptr<Task> task_;
public:
Timer(std::shared_ptr<Task> task) : task_(task) {}
void tick() {
if (auto task = task_.lock()) {
task->execute();
} else {
std::cout << "Task 만료됨\n";
}
}
};
// 사용
auto task = std::make_shared<Task>();
Timer timer(task);
timer.tick(); // 실행
task.reset(); // Task 소멸
timer.tick(); // "Task 만료됨"
패턴 2: 이벤트 리스너
class EventManager {
std::vector<std::weak_ptr<Listener>> listeners_;
public:
void addListener(std::shared_ptr<Listener> listener) {
listeners_.push_back(listener);
}
void notify(const Event& event) {
// 만료된 리스너 제거
listeners_.erase(
std::remove_if(listeners_.begin(), listeners_.end(),
{ return weak.expired(); }),
listeners_.end()
);
// 알림
for (auto& weak : listeners_) {
if (auto listener = weak.lock()) {
listener->onEvent(event);
}
}
}
};
// 사용
EventManager manager;
{
auto listener = std::make_shared<MyListener>();
manager.addListener(listener);
manager.notify(event); // 알림 받음
}
// listener 소멸
manager.notify(event); // 자동으로 제거됨
패턴 3: 공유 리소스 풀
class ResourcePool {
std::vector<std::weak_ptr<Resource>> pool_;
public:
std::shared_ptr<Resource> acquire() {
// 재사용 가능한 리소스 찾기
for (auto& weak : pool_) {
if (auto resource = weak.lock()) {
if (resource.use_count() == 1) {
return resource; // 재사용
}
}
}
// 새로 생성
auto resource = std::make_shared<Resource>();
pool_.push_back(resource);
return resource;
}
void cleanup() {
pool_.erase(
std::remove_if(pool_.begin(), pool_.end(),
{ return weak.expired(); }),
pool_.end()
);
}
};
FAQ
Q1: weak_ptr은 언제 사용하나요?
A:
- 순환 참조 방지: 부모-자식, 양방향 링크
- 캐시: 객체가 사용 중일 때만 유지
- 관찰자 패턴: 관찰자 소멸 시 자동 제거
- 역참조: 소유하지 않고 참조만
// 순환 참조 방지
class Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak_ptr
};
Q2: weak_ptr은 참조 카운트를 증가시키나요?
A: 아니요. 관찰만 하고 참조 카운트를 증가시키지 않습니다.
auto shared = std::make_shared<int>(10);
std::cout << shared.use_count() << '\n'; // 1
std::weak_ptr<int> weak = shared;
std::cout << shared.use_count() << '\n'; // 1 (증가 안함)
Q3: weak_ptr은 어떻게 사용하나요?
A: lock() 으로 shared_ptr을 얻어 사용합니다.
std::weak_ptr<int> weak;
{
auto shared = std::make_shared<int>(10);
weak = shared;
if (auto ptr = weak.lock()) {
std::cout << *ptr << '\n'; // 10
}
}
// shared 소멸 후
if (auto ptr = weak.lock()) {
// 실행 안됨
} else {
std::cout << "만료됨\n";
}
Q4: 만료 확인은 어떻게 하나요?
A: expired() 또는 lock() 을 사용합니다. lock()이 더 안전합니다.
// ❌ expired 후 lock: 경쟁 조건
if (!weak.expired()) {
auto ptr = weak.lock(); // 사이에 만료될 수 있음
}
// ✅ lock만 사용: 안전
if (auto ptr = weak.lock()) {
// ptr이 유효함을 보장
}
Q5: weak_ptr의 성능은?
A: shared_ptr과 동일한 제어 블록을 사용합니다. lock() 호출 시 원자적 연산이 필요합니다.
// weak_ptr 생성: O(1)
std::weak_ptr<int> weak = shared;
// lock(): 원자적 연산
auto ptr = weak.lock(); // ~10ns
Q6: weak_ptr은 nullptr을 가질 수 있나요?
A: 가능합니다. 기본 생성 시 비어있습니다.
std::weak_ptr<int> weak; // 비어있음
if (weak.expired()) {
std::cout << "비어있음\n";
}
Q7: weak_ptr의 크기는?
A: shared_ptr과 동일합니다. 제어 블록 포인터를 저장합니다.
std::cout << sizeof(std::weak_ptr<int>) << '\n'; // 16 (64비트)
std::cout << sizeof(std::shared_ptr<int>) << '\n'; // 16 (64비트)
Q8: weak_ptr 학습 리소스는?
A:
- “Effective Modern C++” by Scott Meyers (Item 20)
- “C++ Concurrency in Action” by Anthony Williams
- cppreference.com - weak_ptr
관련 글: 스마트 포인터, weak_ptr 상세, 순환 참조, 메모리 누수.
한 줄 요약: weak_ptr은 shared_ptr을 관찰하되 참조 카운트를 증가시키지 않는 C++11 스마트 포인터입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 스마트 포인터 | unique_ptr/shared_ptr “메모리 안전” 가이드
- C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]
- C++ shared_ptr 순환 참조 완전 정복 | 부모-자식·옵저버·그래프·캐시 패턴 [#33-4]
- C++ Memory Leak | “메모리 누수” 가이드
관련 글
- C++ async & launch |
- C++ Atomic Operations |
- C++ Attributes |
- C++ auto 키워드 |
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기