[Advanced C++] 61. std::move, SmartPointer(std::unique_ptr, std::shared_ptr, std::weak_ptr)

dev.kelvin·2025년 6월 10일
1

Advanced C++

목록 보기
61/74
post-thumbnail

1. std::move

std::move

    template <typename T>
    void mySwapCopy(T& a, T& b)
    {
        T tmp { a }; // 복사 생성자 호출 (tmp는 a의 복사본으로 생성)
        a = b;       // 복사 대입 연산자 호출 (a에 b의 값을 복사)
        b = tmp;     // 복사 대입 연산자 호출 (b에 tmp의 값을 복사)
    }
    
    std::string a{ "Kelvin" };
    std::string b{ "Park" };
    
    mySwapCopy(a, b); //a,b가 서로 교환됨

위와 같이 이동을 사용하지 않는 Swap은 복사가 많이 발생하기 때문에 성능상 좋지 않다

따라서 이러한 swap을 복사가 아닌 이동을 통해 구현해야 하는데 매개변수 a,b가 r-value 참조가 아닌 l-value 참조이기 때문에 이동 생성자/이동 대입 연산자가 호출되지 않는다 (컴파일러는 lvalue는 추후에 다시 사용될 가능성이 있다고 판단해서 복사를 하기 때문)

std::move는 인자를 rvalue로 casting해서 (static_cast) move semantic을 호출시키는 STL 함수이다
(< utility > 헤더에 정의)

따라서 std::move를 사용하여 lvalue를 복사보다 이동을 시킬 수 있게 되는것이다

위 코드를 std::move()를 사용하여 move semantic 호출을 시킨 코드는 다음과 같다

	template <typename T>
    void mySwapMove(T& a, T& b)
    {
        T tmp { std::move(a) }; // 이동 생성자 호출 (a의 내용이 tmp로 이동)
        a = b;       // 이동 대입 연산자 호출 (a에 b의 값을 이동)
        b = tmp;     // 이동 대입 연산자 호출 (b에 tmp의 값을 이동)
    }
    
    std::string x{ "Kelvin" };
    std::string y{ "Park" };
    
    mySwapMove(x, y); //x,y가 서로 교환됨

결과는 위의 복사를 이용한 swap함수와 같지만 훨씬 더 효율적으로 동작한다

tmp를 초기화 하기 위해서 a의 복사가 발생하지 않고 그대로 이동하게 되며 a, b에도 각각 b, tmp값이 복사되지 않고 이동하게 된다

std::move는 이름때문에 무언가 이동시키는 함수로 인식될 수 있지만 실제로는 lvalue를 rvalue참조로 캐스팅 하는것이다 (그렇기 때문에 move semantic 호출이 가능해짐)

또한 std::vector와 같은 컨테이너의 element를 lvalue로 채울때 std::move를 사용하여 복사 대신 move semantic을 사용하여 채울 수 있다

	std::vector<std::string> v1{};

	std::string str{ "Kelvin" };
    
	v1.push_back(str); //str을 복사하여 vector에 push_back
	std::cout << "str: " << str << '\n'; //Kelvin
	std::cout << "vector: " << v1[0] << '\n'; //Kelvin

	v1.push_back(std::move(str)); //str을 vector에 이동시켜 push_back
	std::cout << "str: " << str << '\n'; //없음
	std::cout << "vector: " << v1[1] << '\n'; //Kelvin

첫번째 복사를 이용하여 vector element를 push_back했을때는 복사기 때문에 기존 str이 남아있었지만 두번째는 std::move()를 이용하여 rvalue 참조 캐스팅이 되어 move semantic이 호출되었기 때문에 기존 str이 비워지게 된다

여기서 중요한 점은 이동된 객체는 valid하지만 미지정 상태가 된다는 점이다

이동된 객체가 임시객체(rvalue)라면 해당 객체가 이동 후에 어떻게 되든지 전혀 상관이 없다, 어차피 임시객체라 바로 소멸되기 때문이다, 하지만 std::move()에는 lvalue가 들어갈 수 있으며 이는 rvalue 참조로 캐스팅되어 move semantic이 호출될 수 있다

이때 이동된 lvalue 객체에는 계속해서 접근이 가능하다, STL에서는 이동된 객체는 valid하지만 미지정 상태가 된다라고 정의한다

물론 이동된 lvalue 객체에 접근하여 해당 객체의 값을 사용하는건 피하는게 좋다, 이동된 객체의 현재 값에 의존하지 않는 함수를 호출하는건 안전하다
ex) operator=, claer(), reset()

하지만 이동된 lvalue객체의 현재 값에 의존하는 함수인 operator[], front()와 같은 함수는 피하는게 좋다

std::move는 어디에 많이 사용할까?

보통 정렬에 많이 사용이 된다, 선택 정렬이나 버블 정렬과 같은 많은 정렬 알고리즘은 swap하며 작동하게 되는데 이때 복사대신 move semantic 호출로 성능을 더 올릴 수 있다

또한 스마트 포인터가 관리하는 내용을 다른 스마트 포인터로 옮기고 싶을때 사용한다
(std::unique_ptr은 복사가 불가능하고 이동만 가능함)


2. std::unique_ptr

std::unique_ptr

일반적인 동적할당을 받은 포인터는 제대로 delete 되지 않으면 메모리 누수와 버그가 발생하기 쉽다

	int main()
    {
        auto* fooPtr{ new Foo };

        int a{ 0 };
        if (a == 0)
        {
            throw 0;
        }

        delete fooPtr; //여기까지 안가기 때문에 포인터 해제가 안됨

        return 0;
    }

스마트 포인터의 가장 큰 역할을 프로그래머가 제공한 동적할당된 리소스를 관리하고 적절한 시점에 알아서 해제하는걸 보장하는것이다 (스마트 포인터 소멸 시점)

이 뜻은 곧 스마트 포인터는 동적으로 할당 해서는 안된다는 의미가 된다, 스마트 포인터를 동적할당 할 경우 해당 스마트 포인터가 delete되지 않으면 그 스마트 포인터가 관리하는 리소스도 delete되지 않아 큰 메모리 누수가 발생할 가능성이 있다

스마트 포인터는 항상 stack에 할당 (지역변수, 멤버 변수)해서 해당 스마트 포인터가 포함된 함수가 종료되거나 스마트포인터가 포함된 객체가 소멸될 때 스마트 포인터가 관리하는 리소스가 정상적으로 해제되어야 한다

C++11 STL에는 총 4가지 스마트 포인터 클래스를 제공한다, 여기서 std::auto_ptr은 C++17에서 제거되었다

std::unique_ptr은 여러 객체에 의해 공유되지 않는 동적 할당 객체를 관리하는데 사용되어야 한다, 정리하면 std::unique_ptr은 관리하는 리소스는 다른 클래스와 소유권을 공유하지 않고 단독으로 소유해야 한다는 것이다 (그래서 복사가 안됨)

    Resource* res{ new Resource() };
    std::unique_ptr<Resource> res1{ res };
    std::unique_ptr<Resource> res2{ res };
    
    //X
	#include <memory>
    
	int main()
    {
        std::unique_ptr<Foo> f{ new Foo };

        return 0;
    }

Foo클래스 타입 객체를 동적할당하고 이를 std::unique_ptr이 소유하게 한다

main()에 선언되었기 때문에 unique_ptr은 해당 함수가 종료될 때 자동으로 소멸되며 관리하는 리소스를 delete한다

std::unique_ptr은 복사가 아닌 move semantic을 고려하여 설계되었다, 따라서 복사 생성자, 복사 대입 연산자 사용이 불가능하다 (반드시 이동시켜야 함)

	std::unique_ptr<Foo> f1{ new Foo };
	std::unique_ptr<Foo> f2{ nullptr };

	f2 = f1; //error 복사 금지
	f2 = std::move(f1); //ok

그렇다면 std::unique_ptr이 관리하는 리소스에 어떻게 접근할까?

std::unique_ptr은 관리중인 resource를 return하는 operator*, operator->가 오버로딩 되어 있다 (일반 포인터와 같은 문법)

operator*는 참조를 반환하고 operator->는 포인터를 return한다

std::unique_ptr은 항상 리소스를 관리하고 있지 않을 수 있다 (일반 포인터도 nullptr일 수 있는것처럼)

따라서 관리중인 리소스에 접근하기 전에 체크를 해주는게 좋다, 관리하고 있는 리소스가 있다면 true, 없다면 false로 캐스팅이 된다

	std::unique_ptr<Foo> f1{ new Foo };
	std::unique_ptr<Foo> f2{ nullptr };

	f2 = std::move(f1);

	if (f1)
	{
		std::cout << "f1 manage resource" << '\n';
	}
	else
	{
		std::cout << "f1 not manage resource" << '\n'; //move되었기 때문에 여기가 출력됨
	}

std::unique_ptr은 스마트 포인터 답게 내부에서 delete를 사용해야 할 지 delete[]를 사용해야 할 지 알 수 있다, 따라서 단일 객체 뿐 아니라 배열에도 사용해도 괜찮다

하지만 고정크기 배열, 동적 배열, C-style문자열에 std::unique_ptr보다 std::array, std::vector, std::string을 사용하는게 훨씬 권장된다

std::make_unique

C++14부터 std::make_unique()라는 함수가 제공되었다, 이는 템플릿 함수로 해당 템플릿 타입의 객체를 동적할당하여 std::unique_ptr이 관리할 수 있도록 한다

    class Foo
    {
    public:
        Foo(int inValue1, int inValue2) : value1{ inValue1 }, value2{ inValue2 }
        {

        }

    private:
        int value1{};
        int value2{};
    };

    int main()
    {
        std::unique_ptr<Foo> f1{ new Foo(1, 2) }; //이런 방식보다 아래 방식을 권장한다

        auto f2{ std::make_unique<Foo>(1, 2) }; //auto는 std::unique_ptr로 추론됨
        auto f3{ std::make_unique<Foo[]>(5) };

        return 0;
    }

std::unique_ptr 타입의 변수를 선언하고 new로 직접 동적할당하는 방식보다 std::make_unique<>()를 통해 더 쉽게 사용이 가능하다

또한 C++17 이전에는 다음과 같은 예외 안전성 문제가 발생할 수 있기 때문에 std::make_unique<>()를 더 권장한다

	function(std::unique_ptr<T>(new T), 예외가 발생할 수 있는 함수());
    
    /*new T가 되고 예외가 발생할 수 있는 함수가 호출되어 예외가 발생 시 그대로 delete되지 않을 수 있기 때문에 메모리 누수 가능성이 높아진다, 이때 std::make_unique<>()를 사용하면 해당 함수 내부에서 동적할당을 처리하기 때문에 이러한 문제가 없어진다*/

이 문제는 C++17에서 함수 인자 평가 순서 규칙이 변경되며 해결됨

std::unique_ptr 반환, 전달

std::unique_ptr은 함수에서 값 타입으로 안전하게 반환이 가능하다

	std::unique_ptr<Foo> createFoo()
    {
    	return std::make_unique<Foo>();
    }
    
    int main()
    {
    	std::unique_ptr<Foo> f1{ createFoo() }; //이동 되거나 NRVO로 인해 이동도 생략될 수 있다
    }

std::unique_ptr은 함수의 인자로 전달도 가능하다, std::unique_ptr이 관리하는 리소스의 소유권을 이전하고 싶다면 해당 std::unique_ptr을 move semantic을 호출하여 전달하면 된다

    class Foo
    {
    public:
        Foo()
        {
            std::cout << "Foo construct!" << '\n';
        }

        ~Foo()
        {
            std::cout << "Foo destruct!" << '\n';
        }

    private:
        int value1{};
        int value2{};
    };

    void FooFunc(std::unique_ptr<Foo> InFoo)
    {

    }

    int main()
    {
        std::unique_ptr<Foo> f1{ new Foo() };

        FooFunc(std::move(f1));

        return 0;
    }

Foo클래스 타입 객체가 동적할당 되고 이를 std::unique_ptr< Foo > f1에서 관리를 했지만 FooFunc()의 인자로 move되었기 때문에 결국 FooFunc()이 호출 종료되면서 InFoo가 소멸되어 main()이 return되기 전에 Foo는 destruct가 된다

함수가 std::unique_ptr이 관리하는 리소스를 사용만 하고 소유권 이전을 원하지 않는다면 std::unique_ptr 자체 전달은 피해야 한다, 이때 std::unique_ptr의 get()을 사용하여 관리하는 리소스의 raw 포인터를 얻어 넘길 수 있다 혹은 참조를 이용하는 방법도 있다

    void FooFunc(const Foo* InFoo)
    {

    }

    void FooFunc(const Foo& InFoo)
    {

    }

    int main()
    {
        std::unique_ptr<Foo> f1{ new Foo() };

        FooFunc(f1.get());
        FooFunc(*f1);

        return 0;
    }

이렇게 하면 std::unique_ptr< Foo > f1이 관리하는 리소스 소유권이 함수로 넘어가지 않기 때문에 main()이 종료될 때 Foo의 소멸자가 호출된다

std::unique_ptr 사용 시 주의할 점

여러 std::unique_ptr객체가 동일한 리소스를 관리해서는 안된다, (일부 std::unique_ptr 객체가 관리하는 리소스를 delete 시 나머지도 delete되기 때문에 의도치 않은 동작이 발생한다)

    Resource* res{ new Resource() };
    std::unique_ptr<Resource> res1{ res };
    std::unique_ptr<Resource> res2{ res };
    
    //X

또한 std::unique_ptr이 관리하는 리소스를 명시적으로 delete해서는 안된다 (프로그래머가 명시적으로 delete하고 std::unique_ptr이 또 delete해서 double free가 발생할 수 있다)

    Resource* res{ new Resource() };
    std::unique_ptr<Resource> res1{ res };
    delete res; //X

3. std::shared_ptr

std::shared_ptr

std::unique_ptr은 리소스를 단독으로 소유하고 관리하는 스마트 포인터 클래스이지만 std::shared_ptr은 하나의 리소스를 공동으로 소유하고 관리하는 스마트 포인터 클래스이다

곧 여러개의 std::shared_ptr이 동일한 리소스를 가리킬 수 있다는 의미이다, std::shared_ptr은 내부적으로 해당 리소스를 소유하고 관리하는 std::shared_ptr이 몇개인지 추적하며 하나 이상의 std::shared_ptr이 리소스를 소유하고 관리하고 있다면 다른 std::shared_ptr이 소멸되더라도 리소스는 delete 되지 않는다 (참조 카운트를 체크), 해당 리소스를 소유하고 관리하는 마지막 std::shared_ptr이 소멸되면 해당 리소스는 그때 delete된다

std::shared_ptr도 std::unique_ptr과 마찬가지로 < memory >에 정의되어 있다

	#include <memory>

    int main()
    {
        Foo* f{ new Foo() };

        std::shared_ptr<Foo> ptrFoo1{ f }; //참조 카운트 1
        {
            std::shared_ptr<Foo> ptrFoo2{ ptrFoo1 }; //ptrFoo1을 복사, 참조 카운트 2
        }
        //참조 카운트 1

        return 0; //참조 카운트 0 -> f소멸
    }

ptrFoo2는 { }를 벗어나게 되면 소멸되지만 실제 소유하고 관리하는 리소스인 f는 소멸되지 않는다, 이는 ptrFoo1이 참조를 하고 있기 때문이다

하지만 이때 중요한 점은 기존의 std::shared_ptr을 복사해서 넣지 않고 리소스로 초기화하면 double free문제가 발생한다

	std::shared_ptr<Foo> ptrFoo1{ f };
	{
		std::shared_ptr<Foo> ptrFoo2{ f };
	}
    
    //double free!

이는 독립적인 std::shared_ptr이기 때문에 참조 카운팅에 영향을 주지 않는다, 따라서 각각 참조 카운트가 1인 상태이고 { }를 벗어나면서 ptrFoo2가 소멸될 때 참조 카운트가 0이기 때문에 f가 소멸되고 return 0에서 ptrFoo1이 소멸되어 참조 카운트가 0이기 때문에 f가 또 소멸되어 double free가 발생하여 크래시가 난다

물론 같은 리소스를 가리키고 있지만 각각의 std::shared_ptr은 독립적이기 때문에 서로를 알지 못하여 참조 카운팅에 영향을 주지 못한다

따라서 std::shared_ptr을 사용할때는 기존의 shared_ptr을 복사해서 사용해야 한다, 또한 std::unique_ptr 과 마찬가지로 nullptr 체크를 하고 사용하는게 좋다

std::make_shared

C++14에서 std::make_unique()로 std::unique_ptr을 만드는 것 처럼 std::make_shared()로 std::shared_ptr을 만들 수 있다 (권장)

	auto ptrFoo1{ std::make_shared<Foo>() };
	{
		auto ptrFoo2{ ptrFoo1 };
	}

new 키워드를 사용하지 않아 코드가 간결하며 안전하고 효율적이다

std::unique_ptr은 내부적으로 관리하는 리소스를 가리키는 포인터 하나만 사용하지만 std::shared_ptr은 관리하는 리소스를 가리키는 포인터, 제어 블록(control block)을 가리키는 포인터 총 2개를 사용한다

여기서 제어 블록이란 참조 카운트와 같은 여러 정보를 담고있는 동적 할당된 객체를 의미한다

new를 사용하여 std::shared_ptr의 생성자를 호출하게 되면 관리하는 리소스와 제어 블록을 위한 메모리가 각각 별도로 할당된다, 하지만 make_shared()를 사용하면 이 두개의 할당을 한번의 메모리 할당으로 최적화 시키기 때문에 성능상 유리하다

이러한 동작이 결국 위에서 각각의 std::shared_ptr이 각각 같은 리소스로 초기화 되었을 때 double free를 발생시키는 원인이 된다 (관리하는 리소스를 가리키는 포인터는 같지만 제어 블록이 독립적이기 때문에 참조 카운팅에 영향을 못 줌)

기존의 shared_ptr을 복사하게 되면 이러한 제어 블록의 정보가 공유되기 때문에 double free가 발생하지 않는다

std::unique_ptr과 std::shared_ptr의 관계

결론적으로 말하면 std::unique_ptr은 std::shared_ptr로 변환될 수 있지만 그 역은 성립하지 않는다

std::unique_ptr의 리소스 소유권이 std::shared_ptr로 move된다, 하지만 그 역은 std::shared_ptr은 하나의 리소스를 여러 std::shared_ptr이 소유권을 갖고 관리하기 때문에 불가능하다

std::shared_ptr도 std::unique_ptr과 마찬가지로 std::shared_ptr 자체가 소멸되지 않는다면 관리하는 리소스가 소멸되지 않아 메모리 누수가 발생하게 된다, unique_ptr은 단독으로 리소스를 관리하지만 shared_ptr은 한 리소스를 여러개가 관리하기 때문에 소멸에 더 신경을 써줘야 한다

std::shared_ptr은 C++20부터 C-style 배열을 지원한다 (그 전에는 사용할 수 없다)

	std::shared_ptr<타입[]>

2. 순환 참조 문제, std::weak_ptr

순환 참조 문제

std::shared_ptr은 한 리소스를 여러개의 std::shared_ptr이 공동으로 소유하고 관리할 수 있는데 이로인해 순환 참조 문제가 발생할 수 있다

    class Bar
    {
    public:
        Bar()
        {
            std::cout << "Bar()" << '\n';
        }

        ~Bar()
        {
            std::cout << "~Bar()" << '\n';
        }

        friend bool TestFunc(std::shared_ptr<Bar>& Inb1, std::shared_ptr<Bar>& Inb2)
        {
            if (!Inb1 || !Inb2)
            {
                return false;
            }

            Inb1->ptrBar = Inb2;
            Inb2->ptrBar = Inb1;
        }

        std::shared_ptr<Bar> ptrBar{};
    };

    int main()
    {
        auto b1{ std::make_shared<Bar>() };
        auto b2{ std::make_shared<Bar>() };

        TestFunc(b1, b2);

        return 0;
    }

위 코드는 main()이 종료되어 b1, b2가 소멸되어 Bar클래스의 소멸자가 호출될 것 같지만 그렇지 않다

왜냐하면 TestFunc()에 의해 b1이 b2를 소유하고 관리하게 되고 b2는 b1을 소유하고 관리하기 때문이다

main()이 종료될 시기에 b1 참조 카운트는 2에서 1로 감소하고 b2도 마찬가지로 2에서 1로 감소한다, 하지만 참조 카운트가 0이 아니기 때문에 소멸되지 않는것이다

쉽게 이야기하면 A가 B를 참조하고 B가 A를 참조하는 상황이 있다면 서로가 서로를 참조하여 어떠한 객체도 delete될 수 없기 때문에 메모리 누수가 발생하게 되는데 이것이 바로 순환 참조 문제이다

이러한 현상은 자기자신을 참조할때도 문제가 발생한다

	auto b1{ std::make_shared<Bar>() };
	b1->ptrBar = b1;

마찬가지로 소멸자가 호출되지 않는다

std::weak_ptr

std::weak_ptr은 이러한 순환 참조 문제를 해결하기 위해 설계된 스마트 포인터 클래스이다

std::weak_ptr은 shared_ptr처럼 객체에 접근할 수 있지만 소유자로 간주되지 않는다 (관찰자)

따라서 std::weak_ptr은 std::shared_ptr 참조 카운팅에 영향을 주지 않는다

    class Bar
    {
    public:
        Bar()
        {
            std::cout << "Bar()" << '\n';
        }

        ~Bar()
        {
            std::cout << "~Bar()" << '\n';
        }

        friend bool TestFunc(std::shared_ptr<Bar>& Inb1, std::shared_ptr<Bar>& Inb2)
        {
            if (!Inb1 || !Inb2)
            {
                return false;
            }

			//참조 카운트 증가하지 않음
            Inb1->weakptrBar = Inb2;
            Inb2->weakptrBar = Inb1;
        }

        std::weak_ptr<Bar> weakptrBar{}; //weakptr로 변경
    };

    int main()
    {
        auto b1{ std::make_shared<Bar>() };
        auto b2{ std::make_shared<Bar>() };

        TestFunc(b1, b2);

        return 0;
    }

std::weak_ptr로 변경을 하면 참조 카운팅에 영향을 주지 않아 순환 참조가 발생하지 않고 소멸자가 정상적으로 호출된다

b1의 weakptrBar는 b2를 관찰만 하고 b2의 weakptrBar는 b1을 관찰만 하기 때문이다

std::weak_ptr 사용법

std::weak_ptr은 직접 사용이 불가능하다 (operator->가 오버로딩 되어있지 않다), 따라서 std::shared_ptr로 캐스팅 후 사용해야 한다

이때 lock() 멤버 함수로 std::weak_ptr을 std::shared_ptr로 변환할 수 있다 (std::weak_ptr이 관찰하는 리소스를 std::shared_ptr로 가져오는것임)

	std::shared_ptr<int> sp1{ std::make_shared<int>(10) };
	std::weak_ptr<int> wp1{ sp1 };

	std::cout << *(wp1.lock()); //10

일반 raw pointer를 사용할 때 굉장히 중요한 점은 해당 raw pointer가 어떤 객체의 주소를 가지고 있다가 그 객체가 파괴될 때 dangling pointer가 되고 이러한 포인터를 역참조하게 되면 정의되지 않은 동작이 발생한다는 점이다

일반 raw pointer는 nullptr이 아닌 주소를 가진 포인터가 dangling 상태인지 아닌지를 알 수 있는 방법이 없다

std::weak_ptr은 관찰하고 있는 리소스의 참조 카운트에 접근이 가능하여 weak_ptr이 관찰하는 리소스가 dangling인지 아닌지를 판별할 수 있다
(참조 카운트가 0이면 리소스가 유효하지 않고 0이 아니면 유효하다)

weak_ptr이 관찰하고 있는 리소스의 dangling 판단은 expired() 멤버 함수를 사용하여 확인이 가능하다

    std::weak_ptr<Bar> GetWeakPtr()
    {
        auto temp{ std::make_shared<Bar>() };
        return std::weak_ptr<Bar>{temp};
    }

    Bar* GetRawPtr()
    {
        auto temp{ std::make_unique<Bar>() };
        return temp.get();
    }

    int main()
    {
        auto rawptr{ GetRawPtr() };
        std::cout << "Our dumb ptr is: " << ((rawptr == nullptr) ? "nullptr\n" : "non-null\n");

        auto weakptr{ GetWeakPtr() };
        std::cout << "Our weak ptr is: " << ((weakptr.expired()) ? "expired\n" : "valid\n");

        return 0;
    }

non-null이 나오고 expired가 나오게 된다

GetRawPtr()은 함수가 종료되고 temp가 소멸되어 관리하던 리소스가 해제되고 dangling pointer를 return하게 되어 nullptr은 아니지만 유효하지 않은 주소를 담고 있게 된다

하지만 GetWeakPtr()은 함수가 종료되고 temp가 소멸되어 관리하던 리소스가 해제되고 이를 weak_ptr에 넣어서 return했기 때문에 return된 std::weak_ptr은 관찰하던 temp가 소멸된 걸 알고 std::shared_ptr인 temp가 관리하는 리소스가 해제되었기 때문에 expired()로 참조 카운트를 체크하여 유효하지 않다는걸 알 수 있다

따라서 expired()가 true인 weak_ptr에서 lock()을 호출하면 안된다 (nullptr을 가리키는 shared_ptr이 return된다)

reset()

std::shared_ptr, std::unique_ptr, std::weak_ptr과 같은 스마트 포인터는 reset() 멤버함수가 존재한다

shared_ptr에서 reset()은 해당 shared_ptr이 관리하던 리소스이 소유권을 포기하고 참조 카운트를 1 감소시킨다, 이때 shared_ptr은 nullptr 상태가 된다

이때 reset(new_ptr);을 하게 되면 기존에 관리하던 리소스 소유권은 포기하고 인자로 들어온 리소스의 소유권을 가진다

unique_ptr에서 reset()은 관리하던 리소스를 메모리에서 즉시 해제한다 (단독 소유권이기 때문에 바로 메모리에서 해제해버림), 마찬가지로 nullptr상태가 되고 reset(new_ptr);은 기존에 관리하던 리소스 소유권은 포기하고 인자로 들어온 리소스의 소유권을 가진다

weak_ptr에서 reset()은 관찰하던 리소스나 shared_ptr의 참조 카운트에 영향을 주지 않고 단순히 더 이상 관찰하지 않겠다는 상태가 된다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글