본문으로 바로가기

STL) 나만의 Allocator( 할당자 ) 만들기 - 2

category C++/STL 2019. 4. 17. 01:17

저번 글에도 작성했듯이, allocator는 다음과 같은 형식을 정의해야 한다.

 

	using value_type = T;
	using pointer = T * ;
	using const_pointer = const T*;
	using void_pointer = void*;
	using const_void_pointer = const void*;
	using size_type = size_t;
	using difference_type = std::ptrdiff_t; 

std::ptrdiff_t는 포인터 간의 차이를 담고있는 정수 유형이다 ( 쉽게 말해 그냥 typedef int ptrdiff_t 다. )

 

그리고 Allocator에서 추가로 구현해야하는 인터페이스가 있는데, 하나 하나 작성하며 설명해놓겠다.

 

 Alloc() = default;
~cAlloc() = default;

allocator도 결국엔 template class 이기 때문에, 생성자와 소멸자를 작성해야하는데, allocator 가 하는 일은 T 타입의 포인터를 할당 해제 해주는 Factory 라고 생각하면 쉽다. 

생성자 내에선 해줄 일이 없고, 다른 함수에서 작성한다.

 

  struct rebind {
	using other = cAlloc<U>;
};

 

rebind 는 어떤 종류의 객체를 얻기 위해 사용된다.

std::list와 A라는 타입을 예로들면, std::list<T,A> 할당자는 T를 할당하기 위한 A이지만, 

실제로 내부에서 list는 노드 기반으로 가지고 있어야 한다.

이렇게 T타입이 아닌, 다른 타입으로도의 할당( node )이 필요해지게 되는데 

이와 같은 요구사항을 충족하기 위해 rebind 를 가져야 할 것을 권고 하고 있다.

( 이는 C++17에서 사용중지 권고가 내려졌고, C++20에서 삭제 예정이다. )

 

template<class _Value_type,
	class _Voidptr>
	struct _List_node
		{	// list node
		using _Nodeptr = _Rebind_pointer_t<_Voidptr, _List_node>;
		_Nodeptr _Next;	// successor node, or first element if head
		_Nodeptr _Prev;	// predecessor node, or last element if head
		_Value_type _Myval;	// the stored value, unused if head
 //중략

 

결과적으로 list<T>는 rebind<_List_node>::other를 참조함으로써, T 객체의 할당자를 통해 위의 List_node를 찾게 된다.

( 자세한 설명은 https://rookiecj.tistory.com/118 이곳을 보자 )

( 사실 C++17 부턴, rebind가 사용중지 권고가 내려져 정의하지않아도 컴파일은 잘 된다. )

 

template <typename U>
cAlloc(const cAlloc<U>& other) { }

다른 타입 (U) 에 대한 복사 생성자를 정의해준다. 이는 위에서 설명했다.

 

pointer allocate(size_type ObjectNum, const_void_pointer hint) {
		allocate(ObjectNum);
}
pointer allocate(size_type ObjectNum) {
	return static_cast<pointer>(operator new(sizeof(T) * ObjectNum));
}

 

초기화 되지 않은 곳의 n * sizeof(T) 바이트 만큼을 할당합니다. 

지역참조를 위해 hint 라는 포인터를 사용할 수 있습니다. ( 구현자가 지원하는 경우 가깝에 만드려고 시도할 수 있음 )

( const_void_pointer 를 받는 allocate는 C++17부터 사용중지 권고가 내려졌다. )

 

void deallocate(pointer p, size_type ObjectNum) {
	operator delete(p);
}

 

pointer에 의해 참조되는 곳을 할당 해제한다. deallocate가 삭제하는 곳은 꼭 allocate가 할당한 저장소여야 한다.

 

size_type max_size() const { return std::numeric_limits<size_type>::max() / sizeof(value_type);

이 container가 사용할 수 있는 최대 용량

 

template<typename U, typename... Args>
void construct(U *p, Args&& ...args) {
	new(p) U(std::forward<Args>(args)...);
}

template <typename U>
void destroy(U *p) {
	p->~U();
}

 

실제로 allocate 된 곳에 새로운 형태의 오브젝트를 생성하는 construct와 destroy 함수

 

construct 와 allocate는 container 의 reserve와 resize 를 생각하면 이해하기 쉬움

reserve : 생성자를 호출하지 않고 사이즈 만큼 공간을 예약 ( allocate )만 해둠

resize : 사이즈만큼 공간을 할당 ( allocate ) 하고, 사이즈만큼 생성자 ( construct ) 를 호출함

 

#pragma once
#include <limits>
template <typename T>
class cAlloc {
public:
	using value_type = T;

	using pointer = T * ;
	using const_pointer = const T*;

	using void_pointer = void*;
	using const_void_pointer = const void*;

	using size_type = size_t;

	using difference_type = std::ptrdiff_t;

	cAlloc() = default;
	~cAlloc() = default;
	template <typename U>
	struct rebind {
		using other = cAlloc<U>;
	};
	template <typename U>
	cAlloc(const cAlloc<U>& other) { }
	pointer allocate(size_type ObjectNum, const_void_pointer hint) {
		allocate(ObjectNum);
	}
	pointer allocate(size_type ObjectNum) {
		return static_cast<pointer>(operator new(sizeof(T) * ObjectNum));
	}
	void deallocate(pointer p, size_type ObjectNum) {
		operator delete(p);
	}
	size_type max_size() const { return std::numeric_limits<size_type>::max() / sizeof(value_type); }

	template<typename U, typename... Args>
	void construct(U *p, Args&& ...args) {
		new(p) U(std::forward<Args>(args)...);
	}

	template <typename U>
	void destroy(U *p) {
		p->~U();
	}
};

 

 

이렇게 아무것도 추가되지 않은 첫 custom allocator가 만들어졌다.

#include <iostream>
#include <vector>
#include "cAlloc.h"
int main() {
	std::vector<int, cAlloc<int>> v;
	v.push_back(5);
	std::cout << v.max_size();
}