std::weak_ptr은 std::shared_ptr에서의 문제점인 자신이 가리키는 대상이 이미 파괴되었을 수 있다는 문제를 극복 할 수 있다. std::weak_ptr는 역참조 할 수 없으며, 널인지 판정할 수도 없다. std::weak_ptr는 그 자체로 smart pointer가 아닌 std::shared_ptr를 보강하는 위치에 있기 때문이다. 대체로 std::weak_ptr는 std::shared_ptr를 이용해 생성한다. std::weak_ptr는 자신을 생성하는데 쓰인 std::shared_ptr가 가리키는 것과 동일한 객체를 가리키지만 그 객체의 참조 횟수에는 영향을 주지 않는다. 대상을 잃은 std::weak_ptr를 만료 되었다고 하며, expire를 호출함으로써 알 수 있게 된다.
어차피 std::weak_ptr는 역참조가 불가능하므로 expire를 통해 만료 여부를 점검하고 아직 만료되지 않았다면 피지칭 객체에 대한 접근을 돌려주는 연산을 하나의 원자적 연산으로 수행한다. std::weak_ptr로부터 std:share_ptr를 생성하면 되는데 이미 만료된 std::weak_ptr로 std::shared_ptr를 생성했을 때이 행동 방식에 따라 두가지로 나뉜다.
// 첫번째, std::weak_ptr::lock 사용, 만료되면 null, 아니면 std::shared_ptr 객체 반환
std::shared_ptr<Widget> spw1 = wpw.lock(); // wpw가 만료이면 spw1은 null
// 두번째, std::shared_ptr 생성자를 사용, 만료시 std::bad_weak_ptr 예외 발생
std::shared_ptr<Widget> spw2(wpw); // wpw가 만료이면 std::bad_weak_ptr 발생
유용하게 쓰이는 첫번째 상황
되풀이해서 쓰는 경우가 많은 고비용 함수를 최적화 하기 위해 캐싱하는 함수들을 사용한다. 만약 모두 캐시에 담아 둔다면 그 자체로 성능 문제가 발생할 것이므로, 더이상 쓰지 않는 객체들은 캐시에서 삭제하는 것이 자연스러운 최적화 방법이다. 이러한 캐시 적용 팩터리 함수의 반환 형식은 smart pointer여야 하며, 자신이 대상을 잃었는지 검출할 수 있어야 하기 때문에 std::weak_ptr이어야 한다. 이는 팩터리 함수의 반환 형식이 반드시 std::shared_ptr이어야 한다는것을 뜻한다.
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // 캐시에 있는 객체를 가리키는 std::shared_ptr 혹은 null
if (!objPtr) { // 캐시에 없으면 적재하고 캐시에 저장
objPtr = loadWidget(id);
cache[id] = objPtr;
}
return objPtr;
}
두번째 상황
대부분 Observer 설계 패턴 구현에서 각 관찰 대상 객체에는 자신의 관찰자들을 가리키는 포인터들을 담은 자료 멤버가 있다. 관찰 대상은 관찰자들의 수명 제어는 관심이 없지만 자신이 파괴된 관찰자에 접근하는 일이 없어야 한다. 그렇기 때문에 관찰자들을 가리키는 std::weak_ptr들의 컨테이너를 자료 멤버로 두는 것으로 해결할 수 있다. 그래서 만료 여부를 확인한 후 관찰자에 접근할 수 있다.
세번째 상황으로는
세 객체가 하나의 객체를 가리키는 std::shared_ptr를 가지고 있다고 할 때, 그 하나의 객체가 역참조 하는 포인터가 필요하다고 할 때, std::weak_ptr을 사용하면 된다. 하지만 std::shared_ptr를 사용했을 경우에는 서로가 서로를 가리키는 순환 고리가 생겨 A,B 둘다 파괴되지 못하므로 메모리 누수가 생긴다. raw pointer를 사용했을 경우 대상을 잃은 포인터를 역참조 하는 경우가 생길 수 있다.
효율성 면에서는 std::weak_ptr과 std::shared_ptr는 본질적으로 동일하다. 크기가 같고, 같은 제어블록을 사용하며, 생성이나 파괴, 배정 같은 연산에 원자적 참조 횟수 조작이 관여한다.(제어 블록의 약한 참조 부분)
Conclusion
- std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
- std::weak_ptr의 잠재적인 용도로는 캐싱, 관찰자 목록, 그리고 std::shared_ptr 순환 고리 방지가 있다.
Reference
Effective Modern C++ 항목 20: std::shared_ptr처럼 작도하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
'C++ > Effective Modern C++' 카테고리의 다른 글
Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정리하라 (0) | 2020.07.16 |
---|---|
new 보다 std::make_shared와 make_unique를 선호하라 (0) | 2020.07.15 |
소유권 공유 자원의 관리에는 std::shared_ptr를 사용하라 (0) | 2020.07.13 |
std::unique_ptr (0) | 2020.07.12 |
특수 멤버 함수들의 자동 작성 조건을 숙지하라 (0) | 2020.07.10 |
댓글