본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : std::weak_ptr (11)

by snowoods 2025. 10. 15.

Modern C++

 

std::weak_ptr

 

개요

std::weak_ptrstd::shared_ptr가 관리하는 객체에 대한 비소유(non-owning) 참조를 제공하는 스마트 포인터입니다. std::shared_ptr와 달리 std::weak_ptr는 참조 카운트를 증가시키지 않으므로 순환 참조(circular reference) 문제를 해결하는 데 사용됩니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++11: std::weak_ptr가 도입되었습니다.

 

내용 설명

std::weak_ptrstd::shared_ptr 인스턴스를 가리키지만, 해당 객체의 생명 주기에 영향을 주지 않습니다. 즉, std::weak_ptr가 가리키는 객체는 마지막 std::shared_ptr가 소멸될 때 함께 소멸되며, std::weak_ptr는 더 이상 유효하지 않은 객체를 가리키게 됩니다.

주요 멤버 함수는 다음과 같습니다.

  • expired(): 가리키는 객체가 이미 소멸되었는지 확인합니다.
  • lock(): std::weak_ptr가 가리키는 객체를 참조하는 새로운 std::shared_ptr를 생성합니다. 만약 객체가 이미 소멸되었다면, 비어있는 std::shared_ptr를 반환합니다.
  • use_count(): 객체를 참조하는 std::shared_ptr의 개수를 반환합니다. (디버깅 용도로 주로 사용)

std::weak_ptrstd::shared_ptr만으로 해결할 수 없는 순환 참조 문제를 방지하기 위해 필수적입니다. 두 객체가 서로를 std::shared_ptr로 가리키면 참조 카운트가 0이 되지 않아 메모리 누수가 발생하지만, 한쪽 또는 양쪽을 std::weak_ptr로 바꾸면 이 문제를 해결할 수 있습니다.

 

예제 코드

#include <iostream>
#include <memory>
#include <string>

class ScopeTest
{
public:
    ScopeTest(int val) : m_val(val)
    {
        std::cout << "Constructor: " << m_val << '\n';
    }

    ~ScopeTest()
    {
        std::cout << "Destructor:" << m_val << '\n';
    }

    void test()
    {
        std::cout << "Val: " << m_val << '\n';
    }

    std::weak_ptr<ScopeTest> m_partner;
    int m_val;
};

void f1()
{
    auto t = std::make_shared<ScopeTest>(1);
    t->test();

    std::cout << "Count: " << t.use_count() << '\n';

    {
        auto t2 = t;
        t2->test();

        std::cout << "Count: " << t.use_count() << '\n';
    }

    std::cout << "Count: " << t.use_count() << '\n';
}

void f2()
{
    auto t4 = std::make_shared<ScopeTest>(11);
    std::cout << "Count t4: " << t4.use_count() << '\n';
    auto t5 = std::make_shared<ScopeTest>(12);
    std::cout << "Count t5: " << t5.use_count() << '\n';

    t4->m_partner = t5;
    std::cout << "Count t5: " << t5.use_count() << '\n';
    t5->m_partner = t4;
    std::cout << "Count t4: " << t4.use_count() << '\n';

    if (!t4->m_partner.expired())
    {
        auto t4_partner_shard = t4->m_partner.lock();
        std::cout << t4_partner_shard->m_val << std::endl;
        std::cout << "Count t5: " << t5.use_count() << '\n';
    }
}

int main()
{
    f1();
    std::cout << '\n';
    f2();

    return 0;
}

 

실행 결과

Constructor: 1
Val: 1
Count: 1
Val: 1
Count: 2
Count: 1
Destructor:1

Constructor: 11
Count t4: 1
Constructor: 12
Count t5: 1
Count t5: 1
Count t4: 1
12
Count t5: 2
Destructor:12
Destructor:11

 

활용팁

  • 순환 참조 방지: std::weak_ptr의 가장 중요한 용도입니다. 클래스들이 서로를 std::shared_ptr로 참조해야 할 때, 한쪽을 std::weak_ptr로 만들어 순환 참조를 끊고 메모리 누수를 방지할 수 있습니다.
  • 캐시(Cache) 구현: 객체에 대한 참조가 필요하지만, 해당 객체의 수명을 연장하고 싶지 않을 때 유용합니다. 캐시된 객체가 다른 곳에서 더 이상 사용되지 않아 소멸되면, 캐시는 expired()를 통해 이를 감지하고 무효한 참조를 제거할 수 있습니다.
  • 안전한 접근: lock() 함수를 통해 std::shared_ptr를 얻은 후 객체에 접근하면, 다중 스레드 환경에서도 객체가 접근 도중에 소멸되는 것을 방지하여 안전하게 사용할 수 있습니다.