smart_ptr : weak_ptr
weak_ptr
앞서 스마트 포인터 중 unique_ptr, shared_ptr에 대해서 알아보았다. 이제 마지막으로 살펴볼 weak_ptr이 있다. weak_ptr은 어디에 쓰일까?
스마터 포인터에서 shared_ptr은 공유하는 자원을 관리하는 포인터였다. 그런데 스마트 포인터의 장점은 가지고있고, 객체에 대한 참조 카운트에는 영향을 주지않는 경우가 필요하다. 그런 경우에 weak_ptr를 사용한다. 좀 더 자세히 알아보도록 하자.
자신이 가리키는 대상이 이미 파괴되었을 수도 있다는 문제를 극복할 수 있어야 한다. 스마트 포인터는 가리키는 객체에 대하여 해당 객체가 유효한지 그렇지 않은지에 대하여 알 수 있어야한다. weak_ptr은 그러한 기능을 가지고 있는 스마트 포인터이다. weak_ptr은 객체의 수명을 shared_ptr로 관리하는 경우에 자신이 대상을 잃었음(만료되었음)을 감지할 수 있다. 하지만 weak_ptr은 그 자체로는 역참조도 할 수 없으며, 널인지 판정할 수도 없다. weak_ptr의 목적은 shared_ptr을 보강하는 것이다.
참조 카운트에 영향을 주지 않는다
weak_ptr과 shared_ptr의 관계에 대해서 살펴보자. weak_ptr은 shared_ptr을 통해서 생성한다. weak_ptr은 자신을 생성하는데 쓰인 shared_ptr이 가리키는 것과 동일한 객체를 가리킨다. 허나 그 객체의 소멸에 관여하는 참조 횟수에 영향을 주지 않는다. 이전 스터디 자료에서 보았듯이 shared_ptr은 control block이라는 자료구조를 가지고 있다. 이 자료구조에는 커스텀 할당자, 커스텀 삭제자와 더불어 참조 카운트에 대한 정보도 들어간다. 이때 우리가 흔히 알고있는 참조 카운트인 strong ref와 weak_ptr로 해당 객체를 가리켰을때 참조 카운팅을 하는 weak_ref가 있다. shared_ptr이 가리키는 갯수의 참조카운트는 strong ref, weak_ptr로 가리키는 객체의 참조 카운트는 weak_ref인 것이다. 이때 객체의 소멸에 기준이 되는 참조 카운팅은 strong ref이다. 따라서 weak_ptr은 shared_ptr의 소멸에는 영향을 주지 않는다.
위 처럼 가리키던 객체가 더이상 없는 weak_ptr를 가리켜 만료되었다(expired)라고 말한다. 만료가 된 것을 확인하는 방법은 weak_ptr의 expired 메서드를 사용하여 확인가능하다.
만료상태를 확인하고 만료되지 않았다면 해당 객체를 참조하려는 상황이 있을 것이다. 하지만 위에서 언급했듯이 weak_ptr로는 객체에 접근할 수 없다. 만약 위와같은 코드를 weak_ptr를 통해서 작성한다고 했을때는 멀티스레드 상황에서 문제가 될 수 있다.
- A 스레드에서 expired를 체크했더니 객체가 아직 유효하다고 판정
- A 스레드에서 해당 객체에 대한 참조를 하려고 함(아직 참조를 하진 않았음)
- B 스레드에서 해당 객체를 가리키는 shared_ptr의 재배정 혹은 파괴함(쉽게말해 shared_ptr이 가리키던 객체의 참조카운트를 0으로 만듦, expired상태가 됨)
- A스레드에서 해당 객체를 참조하려고 했더니 이미 만료된 객체였음.
충분히 위와같은 문제상황이 발생할 수 있다. 따라서 이러한 문제점을 해결하기 위해서는 “만료검사 + 객체 참조” 에 대한 연산을 하나의 원자적 연산으로 처리해야한다. 이를 처리하는 방법은 weak_ptr로부터 shared_ptr을 생성하면된다.
weak_ptr::lock을 사용하는 방식
이 멤버 함수는 shared_ptr객체를 돌려주는데 만일 weak_ptr이 만료되어 있었다면 이 메서드가 돌려주는 shared_ptr은 nullptr이다.
weak_ptr을 인수로 사용한 shared_ptr 생성
팩토리 함수에서의 유용함
어떤 팩토리 함수가 주어진 고유 ID에 해당하는 읽기 전용 객체를 가리키는 스마트 포인터를 리턴한다고 하자. 만약 이 메서드의 호출 및 실행 비용이 크다면 호출 결과에 대해서 캐쉬를 하는 방법으로 최적화를 할 수 있다. 또한 더이상 쓰이지 않는 Widget은 캐시에서 삭제함으로써 자연스럽게 최적화를 진행할 수 있다. 팩토리 함수를 통해서 반환된 객체에 대한 수명은 호출자에서 전적으로 관리를 한다. 그러나 캐시에 있는 데이터도 해당 객체에 대한 접근을 위해 포인터 정보가 있어야 한다. 그리고 동시에 캐시하고 있는 객체가 유효한지 아닌지에 대해 검출할 수 있어야 한다. 팩토리 함수가 돌려준 객체를 클라이언트가 다 사용하고 나면 그 객체는 파괴되며, 그러면 해당 캐시 항목은 대상을 잃게 될 것이기 때문이다. 따라서 캐시에 저장할 포인터는 자신이 대상을 잃었음을 감지할 수 있는 weak_ptr이 되어야 한다. 또한 weak_ptr을 사용하려면 팩토리 함수의 반환형형식도 shared_ptr이 되어야한다.
옵저버 패턴에서의 유용함
옵저버 패턴에서 관찰 대상(subject : 상태가 변할 수 있는 객체)와 관찰자(observer : 상태 변화를 통지받는 객체)가 있다. 대부분의 옵저버 패턴에서 각 관찰대상들에게는 자신들의 변화를 통지해줄 관찰자들을 포인터로서 들고 있다.
- 관찰 대상자에게 변화가 생겼다.
- 관찰 대상자는 관찰자 들에 대한 정보(포인터로)를 들고있다.
- 관찰 대상자는 각 관찰자 들에게 자신의 변화를 알려준다.
이런 과정을 통해서 옵저버 패턴은 설계가 된다. 이때 관찰 대상은 관찰자들의 파괴 시점에 대해서는 크게 중요하게 생각하지 않지만, 이미 파괴된 관찰자들에게 자신의 변화를 알려주려고 하는 일은 하지않도록 심혈을 기울여야한다. 이러한 경우 weak_ptr들의 컨테이너로써 관찰자들에 대한 정보를 담아두면, 관찰자가 유효한지 아닌지에 대해 expired 만료체크를 사용하여 확인가능하다.
순환 참조의 문제를 해결
객체 A,B,C로 이루어진 자료구조를 생각해보자. A와 C가 B를 가리키는 shared_ptr을 가지고 있다고 해보자. 이때 B에서 다시 A를 가리키는 포인터가 필요하게 되었다고 가정해보자. 그 포인터는 어떤 종류의 포인터 이어야 할까? 그림으로 나타내면 다음과 같다.
- raw_pointer : 이 접근 방식에서의 문제점은 A가 파괴되었을때 B는 A가 파괴되었는지에 대한 유효성 검사를 할 수 없다. 따라서 B가 A에 대한 역참조를 통해 미정의 행동이 발생할 수 있다..
- shared_ptr : shared_ptr을 사용하게 되면 A는 B를 B는 A를 shared_ptr로 가리키고 있다. 그렇다면 둘 중 어느 누가 하나 사라져야 하는데, 마치 데드락 처럼 순환 참조가 발생하여 A, B 둘 다 해제되지 않는다. 따라서 이는 사실상 메모리 릭과 같다.
- weak_ptr : 이 경우는 앞의 raw_pointer와 shared_ptr로 작업을 했을때의 문제점을 모두 해결가능하다. A가 파괴되면 B의 포인터가 가리키는 대상은 사라지지만 B는 그 사실을 weak_ptr의 expired메서드를 통해서 확인가능하다. 따라서 미정의 행동을 방지할 수 있따. 또한 weak_ptr은 strong ref에 영향을 주지 않으므로 순환 참조 문제도 해결가능하다. 즉 shared_ptr들이 더 이상 A를 가리키지 않게 되면 A가 정상적으로 파괴된다.
코드로 살펴보자. 다음은 순환참조의 예시 코드이다.
다음은 순환 참조의 문제를 해결하는데 weak_ptr을 사용한 코드이다.
하지만 항상 shared_ptr과 weak_ptr로 설계를 해야하는 것은 아니다. 트리 구조를 살펴보자. 트리구조는 일반적으로 부모가 짤려나가면 자식들(sub tree)도 모두 잘려나간다. 즉 lifetime을 봤을때 자식이 부모보다 더 오래 살아있을 수 없는 것이다. 이렇게 lifetime이 확실히 보장되고, 계층적인 자료구조에서는 일반적으로 unique_ptr을 사용하는것이 최선이다. 또한 자식에서 부모로의 역링크를 가진다고할때 이 역링크는 반드시 스마트포인터를 쓰진 않아도 된다. 위에 언급했듯이 lifetime이 확실히 보장되기때문에(부모가 자식보다 항상 그 이상을 산다) 미정의 행동을 유발할 가능성이 없기 때문이다.
weak_ptr 정리
- weak_ptr은 shared_ptr와 본질적으로 동일하다. weak_ptr 객체는 그 크기가 shared_ptr 객체와 같으면 shared_ptr이 사용하는 것과 같은 제어블록을 사용한다. 또한 소유권 공유에 영향을 주지 않는 weak_ref를 가지고 있다.
- shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 weak_ptr을 사용할것
- weak_ptr의 잠재적인 용도로는 캐싱, 옵저버 패턴, shared_ptr 순환 참조 방지등이 있다
참고자료: effective c++, https://msdn.microsoft.com/ko-kr/library/hh279672.aspx