* 정의

 

std::shared_ptr은 공유 포인터를 의미하며 이를 통해 접근되는 객체의 수명은 그 공유 포인터가 공유된 소유권(shared ownership)을 통해 관리된다. 특정한 하나의 std::shared_ptr가 객체를 소유하는 않으며 모든 std::shared_ptr는 객체가 필요하지 않게 된 시점에 객체가 파괴되도록 서로 협동한다.

 

 

* 동작 방식

 

- 특징

 

기본적인 특징은 다음과 같이 정리할 수 있다.

  • 객체를 가리키던 마지막 std::shared_ptr가 객체를 더 이상 가리키지 않게 되면(다른 객체를 가리키게 되었거나 파괴) 해당 std::shared_ptr이 객체를 파괴한다.
  • 본인이 최후의 공유 포인터임을 알기 위해 참조 횟수(reference count)를 활용한다. 참조 횟수는 그 자원을 가리키는 공유 포인터의 개수를 의미한다.
  • 가비지 컬렉션과 같이 공유 포인터가 가리키는 객체의 파괴에 대한 신경을 쓰지 않아도 된다.
  • 공유 포인터의 생성자에서 참조 횟수를 증가시키고(이동 생성의 경우 제외) 소멸자에서 감소시킨다.
  • 자원의 참조 횟수의 생 포인터도 저장해야하기 때문에 일반적으로 std::shared_ptr의 크기는 생 포인터의 두 배이다. 
  • 참조 횟수를 담을 메모리는 반드시 동적으로 할당한다. 참조 횟수는 공유 포인터의 객체와 연관이 있지만 객체는 참조 횟수를 알지 못하며 참조 횟수를 담을 장소를 따로 마련하지 않는다.
  • 참조 횟수의 증가와 감소는 반드시 원자적 연산이다.
  • std::shared_ptr와 이들이 참조하는 한 객체를 관리하기 위한 추가 메모리인 컨트롤 블록( control block)이 존재하며 이는 힙에 할당된다.
  • 컨트롤 블록(control block)에 참조 횟수, 커스텀 삭제자 등이 담기기 때문에 std::shared_ptr에 추가 메모리가 들지 않는다.

 

 

- 컨트롤 블록의 구조

 

컨트롤 블록의 구조는 다음과 같이 되어있다.

 

 

컨트롤 블록의 생성 규칙은 다음과 같다.

  • std::make_shared는 항상 컨트롤 블록을 생성한다. 이 함수는 객체를 새로 생성하기 때문에 생성되는 공유 포인터는 해당 객체에 대한 최초의 공유 포인터이다.
  • 고유 소유권 포인터(std::unique_ptr or std::auto_ptr)로부터 std::shared_ptr 객체를 생성하면 컨트롤 블록이 생성된다.
  • 생 포인터로 std::shared_ptr 생성자를 호출하면 제어 블록이 생성된다.

 

관리할 객체의 생 포인터로 공유 포인터를 만드는 것은 한번만 해야한다. 그렇지 않으면 생 포인터를 사용해 공유 포인터를 만들 때마다 컨트롤 블록이 생성된다. 이는 객체가 컨트롤 블록을 모르기 때문에 당연한 결과이다.

// 테스트 출력을 확인하기 위해 삭제 되지 않는 커스텀 삭제자를 사용했다.
auto NotDeleted = [](Object* obj) { std::cout << "Delete : "; obj->printData(); };

void SharedPtr_ControlBlock(void)
{
	// 가능하면 생 포인터 자체를 사용하지 않아야 한다.
	// make_shared를 사용하고, 커스텀 삭제자를 쓰는 경우에는 생성과 동시에 넘겨주는 방식을 사용한다.
	Object* obj1 = new Object(1, "obj1");

	std::cout << "<raw pointer ver>" << '\n';
	{
		std::shared_ptr<Object> sharedObj1_1(obj1, NotDeleted);
		std::shared_ptr<Object> sharedObj1_2(obj1, NotDeleted);
		std::cout << "ref count : " << sharedObj1_1.use_count() << '\n';
		std::cout << "ref count : " << sharedObj1_2.use_count() << '\n';
	}

	std::cout << "\n<shared pointer ver>" << '\n';
	{
		std::shared_ptr<Object> sharedObj1_1(obj1, NotDeleted);
		std::shared_ptr<Object> sharedObj1_2(sharedObj1_1);
		std::shared_ptr<Object> sharedObj1_3 = sharedObj1_1;
		std::cout << "ref count : " << sharedObj1_1.use_count() << '\n';
		std::cout << "ref count : " << sharedObj1_2.use_count() << '\n';
	}
}
<raw pointer ver>
ref count : 1
ref count : 1
Delete : id : 1 , name : obj1
Delete : id : 1 , name : obj1

<shared pointer ver>
ref count : 3
ref count : 3
Delete : id : 1 , name : obj1

 

 

- 클래스에서 this의 공유 포인터를 얻기

 

클래스에서 공유 포인터를 반환하기 위해서는 본인을 std::shared_ptr로 감싸서 반환하면 안된다. 이렇게 되면 컨트롤 블록이 여러 개 만들어지기 때문이다.

 

이런 기능을 제공하기위해 std::enable_shared_from_this라는 템플릿을 상속받아 사용할 수 있도록 지원하고 있다. 이 템플릿 클래스를 상속 받으면 std::shared_from_this라는 함수를 사용할 수 있게 되며 이 함수는 현재 객체에 대한 컨트롤 블록을 조회하고 그 컨트롤 블록과 연관된 std::shared_ptr을 생성한다.

 

여기서 주의할 점은 반드시 해당 객체에 대한 컨트롤 블록이 존재해야한다는 점이다. 컨트롤 블록이 없을 때 이 함수를 사용하는 것을 방지하기 위해 객체 생성을 무조건 std::shared_ptr을 통해서만 가능하도록 보장하면 된다. 이는 팩토리 함수와 생성자를 private으로 설정함으로써 실현할 수 있다.

// shared_from_this
class DerivedObject : public std::enable_shared_from_this<DerivedObject>
{
public:
	template <typename... Ts>
	static std::shared_ptr<DerivedObject> create(Ts&&... params)
	{
		return std::shared_ptr<DerivedObject>(new DerivedObject(params...));
	}

	void printData(void)
	{
		std::cout << "id : " << _id << " , name : " << _name << '\n';
	}

	void process(void)
	{
		// ...
		// ... shared_from_this();
		// ...
	}

private:
	DerivedObject(int id, const std::string& name)
		: _id(id), _name(name) {}

public:
	int _id;
	std::string _name;
	// ...
};


void SharedPtr_SharedFromThis(void)
{
	std::shared_ptr<DerivedObject> sharedObj1_1 = DerivedObject::create(0, "obj1");
	sharedObj1_1->process();
}

 

 

- 커스텀 삭제자 예시

 

커스텀 삭제자의 경우 std::shared_ptr의 일부가 아니기 때문에 커스텀 삭제자가 std::unique_ptr의 일부인 경우와 비교했을 때 더 유연하게 사용할 수 있다. 

// 커스텀 삭제자1
auto DeleterObject1 = [](Object* obj)
{
	obj->printData();
	delete obj;
};

// 커스텀 삭제자2
auto DeleterObject2 = [](Object* obj)
{
	obj->printData();
	delete obj;
};

void SharedPtr_UniquePtr_CustomDeleter(void)
{
	// std::make_xxx 함수는 커스텀 삭제자를 사용할 수 없다.
	// 불가능 : std::shared_ptr<Object> sharedPtr1 = std::make_shared<Object>(new Object(1, "obj1"), DeleterObject1);

	std::unique_ptr<Object, decltype(DeleterObject1)> uniquePtr1(new Object(1, "obj1"), DeleterObject1);
	std::unique_ptr<Object, decltype(DeleterObject2)> uniquePtr2(new Object(2, "obj2"), DeleterObject2);

	std::vector<std::unique_ptr<Object, decltype(DeleterObject1)>> uniqueList;
	uniqueList.push_back(std::move(uniquePtr1));
	//uniqueList.push_back(std::move(uniquePtr2)); // 형식이 달라 불가능하다.


	std::shared_ptr<Object> sharedPtr1(new Object(1, "obj1"), DeleterObject1);
	std::shared_ptr<Object> sharedPtr2(new Object(2, "obj2"), DeleterObject2);

	std::vector<std::shared_ptr<Object>> sharedList;
	sharedList.push_back(std::move(sharedPtr1));
	sharedList.push_back(std::move(sharedPtr2));
}

 

 

- std::shared_ptr의 비용

 

  • 컨트롤 블록의 구현은 통상적으로 상속을 활용하며 피지칭 객체가 제대로 파괴되게 만들기 위해 가상 함수도 존재하는데, 이에 비용이 들어간다.
  • 참조 횟수를 원자적으로 조작하기 위한 비용이 든다.
  • 커스텀 삭제자나 할당자로 인해 컨트롤 블록이 커질 수 있다.

 

위와 같은 비용이 존재하긴 하지만 하나씩 따져보면 그리 큰 비용은 아닐 수 있다. 일반적으로 기본 삭제자와 할당자가 쓰이고, 원자적 수행은 하나의 명령으로 되어있으며 std::shared_ptr의 역참조 비용은 생 포인터의 역참조 비용보다 크지 않다.

 

 

- std::shared_ptr가 배열을 지원하지 않는 이유

 

근본적으로 std::shared_ptr은 단일 객체를 가리키는 포인터를 위해 설계되었다. 그래서 std::shared_ptr<T[]>가 금지된다.

 

억지로 T에 배열을 넣을 수는 있지만 std::shared_ptr은 operator[]를 지원하지 않기 때문에 어색한 표현식을 사용해야하고 std::shared_ptr이 지원하는 파생 클래스 포인터에서 기반 클래스 포인터의 변환은 단일 객체에서만 합당하도록 되어있다.

'C++' 카테고리의 다른 글

std::move  (0) 2022.03.26
스마트 포인터 - std::weak_ptr  (0) 2022.03.23
스마트 포인터 - std::unique_ptr  (0) 2022.03.22
스마트 포인터 개요  (0) 2022.03.21
decltype  (0) 2022.03.17

+ Recent posts