프로그래밍/C/C++

[C++] 스마트포인터 shared_ptr - 정보공유의 장

nanze 2022. 1. 15. 18:50
반응형

이번 포스팅은 스마트 포인터 중 shared_ptr 에 대해서 정리해보겠다. 

shared_ptr in C++ 11

shared_ptr 은 c++ 11 이후 제공되는 스마트 포인터 중 하나로 포인터를 더 이상 사용하지 않을 경우 메모리를 자동으로 해제해준다. 보통 unmaged 코드에서는 메모리를 개발자가 직접적으로 관리하는 경우가 많은데 이럴 경우 할당된 메모리를 해제하지 않는 실수를 많이 범할 수 있다. 이런 경우를 미연에 조금 더 방지할 수 있게 해준다. 

 

다음 코드는 shared_ptr 을 사용하는 예제를 보여주고 있다. 

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<int> x(new int(1));
    shared_ptr<int> y = make_shared<int>(1);
    
    wcout << *x << endl;
    wcout << *y << endl;
    
    return 0;
}

위 코드는 두가지 방식을 보여주고 있다. shared_ptr 의 생성자를 이용하는 방식과 make_shared 를 이용하는 방식이다. 

유의할 점은 shared_ptr 에 직접적인 힙메모리를 가리키게 해서는 안된다. 

 

shared_ptr 이 가리키고 있는 메모리는 내부적으로 관리되는 참조 카운트에 의해서 해제되는데 해당 참조 카운터가 0이 되면 가리키는 메모리는 해제되게 되어있다. 다음 코드를 보자.

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr <int> x;
    shared_ptr <int> y;
    wcout << "x ref count" << x.use_count() << endl;
    
    x = make_shared<int>(0);
    wcout << "x ref count" << x.use_count() << endl;
    
    y = x;
    wcout << "x ref count" << x.use_count() << endl;
    wcout << "y ref count" << y.use_count() << endl;
    
    x = nullptr;
    wcout << "x ref count" << x.use_count() << endl;
    wcout << "y ref count" << y.use_count() << endl;
    
    y.reset();
    wcout << "y ref count" << y.use_count() << endl;
    
    return 0;
}

우선 shared_ptr 변수 자체는 참조 카운터가 0이다. 그리고 다른 shared_ptr 이 대입되거나 객체를 생성하면 해당 참조 카운터는 1이 증가된다. 그리고 shared_ptr 에 nullptr 을 대입하게 해당 변수의 참조 포인터는 0이 되며 해당 변수와 다른 shared_ptr 이 연결되어 있다면 해당 shared_ptr 의 참조 카운터는 1 감소한다. reset() 함수 또한 해당 변수의 참조 카운터를 0으로 만든다. 

 

shared_ptr 객체는 변수가 선언된 영역을 벗어나면 소멸자는 참조 카운터를 1 감소시킨다. 아래 코드로 해당 내용을 확인할 수 있다. 

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<int> x = make_shared<int>(0);
    
    if(true)
    {
        shared_ptr<int> y = x;
        wcout << "x ref count : "<< x.use_count() << endl;
        wcout << "y ref count : "<< y.use_count() << endl;
    }
    
    wcout << "x ref count : " << x.use_count() << endl;
    
    return 0;
}

위 코드에서 if 안의 shared_ptr y 변수는 영역을 벗어나면 소멸자에서 참조카운터를 1 감소시킨다. 

 

함수 인자로 shared_ptr 를 사용할 경우 참조를 사용할 때는 해당 shared_ptr 의 참조 카운트가 증가하지 않는다. 다음 코드를 보면 그 예를 확인할 수 있다. 

#include <iostream>
#include <memory>

using namespace std;

void func(shared_ptr<int>& p)
{
    wcout << "p ref count : " << p.use_count() << endl;
}

int main()
{
    shared_ptr <int> p = make_shared<int>(0);
    wcout << "p ref count : " << p.use_count() << endl;
    
    func(p);
    
    wcout << "p ref count : " << p.use_count() << endl;

    return 0;
}

다음은 std::move 함수를 통한 shared_ptr 이동에 대해서 알아보겠다. move 함수를 사용하면 shared_ptr 변수가 가리키는 포인터를 다른 shared_ptr 로 이동시킬 수 있다. 그리고 기존 shared_ptr 참조 카운터는 0이 된다. 다음 코드를 보자.

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<int> p1 = make_shared<int>(0);
    shared_ptr<int> p2;
    
    wcout << "p1 ref count : " << p1.use_count() << endl;
    
    p2 = move(p1);
    
    wcout << "p1 ref count : " << p1.use_count() << endl;
    wcout << "p2 ref count : " << p2.use_count() << endl;
    
    return 0;
}

위 코드를 실행하게 되면 p1의 포인터가 p2 로 이동되고 p1 의 참조 카운터는 0이 되게 된다. 

 

이번에는 shared_ptr 의 reset() 쓰임새에 대해 알아보자. reset() 함수는 인자가 없이 사용될 경우에는 참조 카운터를 1 감소시킨다. 아래 코드 확인으로 그 쓰임을 확인할 수 있다. 

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<int> p1 = make_shared<int>(0);
    shared_ptr<int> p2;
    
    p2 = p1;
    wcout << "p1 ref count : "+ p1.use_count() + " p2 ref count : " + p2.use_count() << endl;
    p1.reset();
    wcout << "p1 ref count : "+ p1.use_count() + " p2 ref count : " + p2.use_count() << endl;
    
    return 0;
}

위 코드를 실행해보면 reset() 함수 호출전에는 참조 카운터가 p1 :2, p2:2 이었다가 reset() 호출 후 0, 1 로 변경되는 것을 확인할 수 있다. 

 

 

 

 

 

reset() 함수를 인자와 함께 사용하면 기존 가리키는 포인터 대신 인자로 들어온 새로운 포인터를 가리키게 되며 참조 카운터는 1이 된다. 아래 코드를 확인하자.

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<int> p1 = make_shared<int>(0);
    
    p1.reset(new int(1));
    
    wcout << "p1 ref count : " << p1.use_count() << "value : " *p1 << endl;
    
    return 0;
}

 

위에서 설명했듯이 참조 카운터가 0이되면 shared_ptr 객체가 가리키는 메모리가 해제된다. shared_ptr 클래스에서는 메모리 해제를 위해 기본적으로 delete 를 호출하지만 reset 함수를 통하여 사용자 정의 메모리 해제 함수를 호출하게 할 수도 있다. 아래 코드를 보자.

#include <iostream>
#include <memory>

using namespace std;

void dectl(int* p)
{
    wcout << "custom func called." << endl;
    delete [] p;
}

int main()
{
    shared_ptr<int> sp;
    shared_ptr<int> sp1;
    int *p = new int[10];
    int *p1 = new int[10];
    sp.reset(p, [](int* u){
            delete [] u;
        }
    );
    
    sp1.reset(p1, dectl);
    
    return 0;
}

위 코드를 보면 사용자 정의 메모리 해제 함수는 2가지 방식으로 가능하다. 함수 포인터를 넘겨주거나 람다 표현식으로 사용도 가능하다. 

 

다음은 shared_ptr 의 null 비교법을 알아보자. 단순하다. 그저 null 값과 비교만 하면 된다. 

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<int> ptr = make_shared<int>(10);
    
    ptr = nullptr;
    
    if(!ptr){  
    // ==  if(ptr == nullptr)
        wcout << "shared_ptr is null. " << endl;
    }
    
    return 0;
}

 

 

 

 

 

shared_ptr 사용시 유의할 점이 한가지 더있다. shared_ptr 은 thread safe 하지 않다는 것이다. 그렇기 스레드 사용할 때는 추가적인 동기화 객체를 사용하거나 atomic_ 관련 함수를 사용해야 한다. 하지만 사용시 성능 상의 문제도 잘  생각해서 사용해야 한다. 멀티 스레드에서 사용시 아래 코드와 같이 추가적인 동기화 메카니즘이 필요하다. 

#include <atomic>
#include <memory>

using namespace std;

shared_ptr<int> g_ptr;

DWORD WINAPI threadfunc(void* param)
{
    while(true){
        shared_ptr<int> p = atomic_load(&g_ptr);
        ...
    }
    return 0;
}

DWORD WINAPI threadfunc2(void* param)
{
    while(true){
        shared_ptr<int> p = make_shared<int>(0);
        atomic_store(&g_ptr, p);
        ...
    }    
    return 0;
}

int main()
{
    HANDLE hThread1 = CreateThread(NULL, 0, threadfunc, NULL, 0, NULL);
    HANDLE hThread2 = CreateThread(NULL, 0, threadfunc2, NULL, 0, NULL);
    
    WaitforSingleObject(hThread1, INFINITE);
    WaitforSingleObject(hThread2, INFINITE);
    return 0; 
}

 

마지막으로 shared_ptr 와 유사하게 동작하도록 하는 클래스를 작성해보고 테스트해보자.  reset() 는 구현하지 않았지만 기존 shared_ptr 과 비슷하게 동작한다. 

test.h

#pragma once


template <typename T>
class nzshared_ptr
{
public:
	nzshared_ptr() {
		mRefCount = nullptr;
		mPtr = nullptr;
	}

	explicit  nzshared_ptr(T* ptr) {
		mRefCount = nullptr;
		mPtr = ptr;
		addRef();
	}

	nzshared_ptr(const nzshared_ptr<T>& rhs)
	{
		mRefCount = rhs.mRefCount;
		mPtr = rhs.ptr;
		addRef();
	}

	~nzshared_ptr() {
		release();
	}

	T* get() const noexcept { 
		return mPtr; 
	}

	int use_count() const noexcept {
		if (mRefCount == nullptr)  return 0;
		return (*mRefCount);
	}

	bool unique() const noexcept
	{
		if (mRefCount == nullptr) {
			return false;
		}
		else {
			if ((*mRefCount) == 0) {
				return true;
			}
			else {
				return false;
			}
		}
	}

	T* operator->() { return mPtr; }
	T& operator* () { return *mPtr; }
	operator bool() const {
		return (mRefCount != nullptr);
	}

	nzshared_ptr<T>& operator=(const nzshared_ptr<T>& rhs)
	{
		mRefCount = rhs.mRefCount;
		mPtr = rhs.mPtr;
		addRef();
		return *this;
	};
	template <typename U>
	nzshared_ptr<T>& operator=(const U& rhs)
	{
		if (rhs == nullptr) {
			(*mRefCount)--;

			mPtr = nullptr;
			mRefCount = nullptr;
		}
		else
			throw exception("cant direct addr.");

		return *this;
	};


private:
	void addRef() noexcept
	{
		if (mRefCount == nullptr) {
			mRefCount = new int(0);
		}

		(*mRefCount)++;
	}

	void release() noexcept
	{
		(*mRefCount)--;
		if (*mRefCount == 0)
		{
			delete mRefCount;
			delete mPtr;
		}
	}

	int* mRefCount;
	T* mPtr;
};

main.cpp

#include <iostream>
#include <memory>
#include "test.h"

using namespace std;

int main()
{
	nzshared_ptr<int> p1(new int(0));
	nzshared_ptr<int> p2;

	wcout << "p1 ref count : " << p1.use_count() << endl;
	wcout << "p2 ref count : " << p2.use_count() << endl;

	p2 = p1;
	wcout << "p1 ref count : " << p1.use_count() << endl;
	wcout << "p2 ref count : " << p2.use_count() << endl;

	{
		nzshared_ptr<int> p3;
		wcout << "p3 ref count : " << p3.use_count() << endl;
		p3 = p2;
		wcout << "p1 ref count : " << p1.use_count() << endl;
		wcout << "p2 ref count : " << p2.use_count() << endl;
		wcout << "p3 ref count : " << p3.use_count() << endl;
	}

	wcout << "p1 ref count : " << p1.use_count() << endl;
	wcout << "p2 ref count : " << p2.use_count() << endl;

	p2 = nullptr;

	wcout << "p1 ref count : " << p1.use_count() << endl;
	wcout << "p2 ref count : " << p2.use_count() << endl;

	return 0;
}

실제 실행해보면 참조 카운트가 올바르게 설정되는 것을 알 수 있다. 

 

관련 다음 포스팅

2022.01.16 - [프로그래밍/C/C++] - [C++] 스마트포인터 unique_ptr [정보공유의 장]

 

[C++] 스마트포인터 unique_ptr [정보공유의 장]

C++ 스마트 포인터(Smart pointer) unique_ptr 이번 포스팅 정리는 스마트 포인터 중 unique_ptr 에 대해서 정리해 보자. unique_ptr 은 소유하는 포인터에 대해 다음과 같은 규칙을 갖는다. 1. 소유 포인터는 한

nanze.tistory.com

 

반응형