C++/C++

C++ - weak_ptr 을 안전하게 사용하는 방법

오의현 2024. 11. 15. 15:43

C++ 에서는 shared_ptr 이라는 스마트 포인터를 제공해준다.

shared_ptr을 활용하면 동적 메모리의 해제를 직접 신경쓸 필요가 없어져 편리하고 안전하게 메모리를 관리하는 것이 가능해진다. 하지만, shared_ptr에는 치명적인 단점이 하나 있다. 바로 순환참조 문제이다.

 

두 객체를 shared_ptr로 만들고, 각 객체의 멤버변수에 상대 객체를 가리키는 상황이 발생한다면? 두 shared_ptr은 상대 객체가 소멸하기만을 기다리다가 둘 다 소멸하지 못하는 상황이 발생하고 만다.이를 해결하기 위해 탄생한 것이 weak_ptr이다.

 

std::make_shared 함수를 통해 동적 메모리를 할당하고 그 메모리를 관리하는 스마트 포인터 객체를 만들었다면, 동적 메모리와 함께 참조 테이블이 생성된다.

 

참조 테이블엔 강한 참조 카운트와 약한 참조 카운트가 존재하며, 메모리 영역을 가리키는 shared_ptr 객체의 수에 따라 강한 참조 카운트가 증가하거나 감소하게 된다. 그리고 강한 참조 카운트가 0이 되면 메모리 영역을 해제하게 된다.

 

weak_ptr은 강한 참조 카운트에는 영향을 주지 않고 약한 참조 카운트에만 영향을 준다. 즉, 메모리 영역을 가리키고 있는 shared_ptr이 없다면 해당 메모리 영역을 관리하는 weak_ptr이 있더라도 메모리 영역은 해제된다는 것이다.

 

이를 통해 멤버함수에서 shared_ptr이 아닌 weak_ptr로 상대 객체를 가리키게 되면 순환참조에서 벗어날 수 있게 되는 것이다. 하지만, 이 상황엔 아무런 문제가 발생하지 않을까?

 

아래의 그림을 보자.

 

이렇게 3개의 shared_ptr과 1개의 weak_ptr이 메모리 영역을 가리키고 있다고 해보자.

현재 강한 참조 카운트는 3이고, 약한 참조 카운트는 1이다.

이 때, 아래의 그림과 같이 참조 상태가 변경된다면?

 

3개의 shared_ptr 객체가 nullptr을 가리키고 있거나 객체가 소멸한 상황이라면 이처럼 동적 메모리를 가리키는 것은 weak_ptr 뿐이고 이 때, 강한 참조 카운트는 0이되어 메모리 영역을 해제하게 된다.

 

그런데, 잘 생각해보자. 위 그림을 보면 무언가 떠올라야 한다. 바로 댕글링 포인터이다. 메모리 영역은 해제되었지만 weak_ptr은 여전히 메모리 영역을 가리키고 있고, 객체가 소멸되지 않은 상황이다. 이 때, weak_ptr을 통해 메모리 영역에 접근한다면 어떻게 될까? 당연히 유효하지 않은 메모리 영역에 접근하게 되어 오류가 발생할 것이다.

 

이를 해결하기 위해 우리는 weak_ptr의 멤버함수를 통해 메모리 영역의 유효성을 판단해야 한다. weak_ptr의 멤버함수에는 expired라는 함수가 있다. 이 함수가 true를 반환하면 메모리 영역은 유효하지 않다는 의미이며, false를 반환하면 메모리 영역이 유효하다는 의미이다.

if(WeakPtr.expired() == false)
{
    //메모리 영역에 접근하는 코드
}

즉 위 코드와 같이 로직을 작성할 수 있다. 하지만, 여기서도 문제가 발생할 여지는 있다. 바로 멀티 스레드 환경에서 발생할 수 있는 문제이다.

 

WeakPtr의 유효성을 검사한 시기엔 expired가 false였지만, 메모리에 접근하기 직전 다른 스레드에서 shared_ptr객체의 참조를 끊어 해당 메모리 영역을 해제하였다면? 위의 코드에서 메모리 영역에 실제 접근하는 순간엔 오류가 발생하게 될 것이다.

 

이를 위해, 우리는 lock()을 사용해 일시적으로 강한 참조 카운트를 증가시키는 것이 좋다. weak_ptr을 사용하면 직접적으로 메모리 영역에 접근할 수 없고, lock()함수를 사용해야만 접근할 수 있다.이 lock()이라는 함수는 weak_ptr의 형태로 메모리에 접근하게 해주는 것이 아니라 shared_ptr 객체를 하나 만들어주는 역할을 한다. 

 

즉, weak_ptr의 lock을 호출하면 강한 참조 카운트가 1이 증가하게 되므로 다른 스레드에 의해 해당 메모리 영역이 해제되는 것을 방지할 수 있게 된다. (최소한의 1은 현재 스레드에서 보장하고 있으므로)

 

weak_ptr의 lock()함수는 참조할 메모리 영역이 없다면 nullptr을 반환하기 때문에, weak_ptr 의 lock()가 반환하는 값이 nullptr인지를 판단하여 유효성을 검사하는 것이 expired보다 더 안전할 수 있다.

std::shared_ptr<int> LockPtr = WeakPtr.lock();

if(LockPtr != nullptr)
{
    //메모리 접근
}

 

weak_ptr은 순환참조를 방지하는 역할을 해주지만, 잘못 사용하면 안정성을 크게 해칠 수 있으므로 반드시 위와 같은 안전검사를 해주도록 하자!