본문 바로가기
코드

std::tuple<Ts...> ➔ std::tuple<U<Ts>...>

by ehei 2021. 3. 25.

최근의 즐거움은 템플릿 프로그래밍이다. 템플릿 프로그래밍은 지적 호기심을 자극시키는 것이 있다. 어떤 문제에 대한 결정적이고 순수한 해결 방법을 찾아야한다. 변수를 조작하거나 동적 분기를 허락하지 않는다. 오로지 컴파일 타임에 해결해야한다. 어떤 로직을 템플릿 프로그래밍으로 해결했을 때의 즐거움은 이루 말할 수 없다. C++을 그저 객체지향언어로 사용한다면 C#이 더 좋은 선택이라고 생각한다. C++은 템플릿 프로그래밍을 통해 무한의 개념에 접근한다. 당신은 로직을 추상화시켜야 한다. 도구가 그것을 강제한다. 컴파일 시간이 길어지고 쓸데없이 템플릿 처리가 많아져 메모리를 많이 차지하는 부작용도 있다. 허나 동적 분기의 제거로 인한 최적화 가능성 무엇보다 지적 즐거움 때문에 포기할 수 없다.

 

최근에 맞닥뜨린 문제는 튜플에 담긴 형식을 다른 형식으로 감싸서 다시 튜플로 만드는 것이었다. 요점은 튜플로 튜플을 생성시키는 일이었다. 이를 위해 튜플의 원소들을 클래스 상속으로 만들었다. 튜플의 구현과도 동일하다고 하겠다. 한편 이전 타입을 일일이 적는 건 매우 번거로운 일이다. decltype을 사용하여 그것을 간단히 하였다. 이 문제에 대해 매우 머리가 아팠지만 해결하고 나니 너무나 신나서 행복했다.

 

이것은 별칭 템플릿을 이용한다.

/*
아래 템플릿 인자를 전달받아 VALUE_TYPE이 튜플의 각 인자로 인스턴싱 가능하도록 하는 별칭 템플릿
- OLD_TUPLE: 변환할 튜플
- VALUE_TYPE: 템플릿 인자를 하나 받는 형식
- SIZE: 튜플의 길이
*/
template<typename OLD_TUPLE, template<typename ELEM> typename VALUE_TYPE, size_t SIZE = std::tuple_size_v<OLD_TUPLE>>
using wrapping = typename _wrapping<OLD_TUPLE, std::tuple<>, VALUE_TYPE, SIZE, SIZE>::type;

 

아래는 구체적인 구현이다.

/*
튜플의 각 원소를 순회한다

INDEX가 0보다 클 때까지 NEW_TUPLE에 차례대로 VALUE_TYPLE으로 감싼 튜플의 원소의 형식이 추가된다. 따라서 _wrapping은 튜플의 원소 개수만큼 상속을 거듭하게 된다. 예를 들어 tuple<int, float, std::string>이 있다면 아래와 같은 계층도가 생긴다. NEW_TUPLE이 차츰 완성된다

_wrapping<tuple<int, float, std::string>, std::tuple<>, VALUE_TYPE, 3, 3> :
	_wrapping<tuple<int, float, std::string>, std::tuple<VALUE_TYPE<int>>, VALUE_TYPE, 3, 2> :
		_wrapping<tuple<int, float, std::string>, std::tuple<VALUE_TYPE<int>, VALUE_TYPE<float>>, VALUE_TYPE, 3, 1> :
			_wrapping<tuple<int, float, std::string>, std::tuple<VALUE_TYPE<int>, VALUE_TYPE<float>, VALUE_TYPE<std::string>>, VALUE_TYPE, 3, 0> :
*/
template<typename OLD_TUPLE, typename NEW_TUPLE, template<typename ELEM> typename VALUE_TYPE, size_t SIZE, size_t INDEX>
struct _wrapping : _wrapping <
	OLD_TUPLE,
	decltype(std::make_tuple( VALUE_TYPE<std::tuple_element_t<SIZE - INDEX, OLD_TUPLE>>{} )),
	VALUE_TYPE,
	SIZE,
	INDEX - 1 >
{
	/*
	부모 클래스를 type 선언에서 나열하면 번거롭기 때문에 base라는 별칭을 쓴다
	*/
	using base = _wrapping <
		OLD_TUPLE,
		decltype(std::make_tuple( VALUE_TYPE<std::tuple_element_t<SIZE - INDEX, OLD_TUPLE>>{} )),
		VALUE_TYPE,
		SIZE,
		INDEX - 1 > ;
	/*
	튜플을 결합한다. NEW_TUPLE뿐 아니라 base::type도 튜플이다. 이것의 형식을 알아내기 위해 tuple_cat을 사용하여 실체화시킨 후 decltype을 사용한다. base::type을 얻는 과정은 재귀적이다. 거듭 부모를 거슬러 올라가며 결합시킨다
	*/
	using type = decltype(std::tuple_cat( NEW_TUPLE{}, base::type{} ));
};

/*
튜플의 말단에 도달하면 NEW_TYPE을 type의 형식으로 결정한다
*/
template<typename OLD_TUPLE, typename NEW_TUPLE, template<typename ELEM> typename VALUE_TYPE, size_t SIZE>
struct _wrapping<OLD_TUPLE, NEW_TUPLE, VALUE_TYPE, SIZE, 0>
{
	using type = NEW_TUPLE;
};

 

wrapping 별칭 템플릿을 사용한 예제이다. 

template<typename T>
struct Value
{
	Value() = default;
	Value( T&& v ) : value{ std::move( v ) }
	{}

	T value;
}

// 상이한 형식이 들어있는 튜플
using types = std::tuple<int, float, size_t, std::string>;

// types의 형식을 Value의 템플릿 인자로 넘기고, 이걸로 Value<T>의 튜플을 선언
using wrapped_types = wrapping<types, Value>;

// Value<T>의 튜플
wrapped_types wrapped_empty;

// 값을 담고 있는 Value<T>의 튜플
wrapped_types wrapped_value{ 1, 1.f, 0, "test" };

 

이걸 작성해본 이유는 어떤 구조체의 형식을 구체적으로 나열하는 방식을 피하는데 썼다. 전통적으로 어떤 데이터 형식을 정의하기 위해 구조체에 형식과 이름을 적어야 한다. 

// 전통적인 방법
struct part_concrete
{
	int v0;
	float v1;
}

 

그러나 나는 형식에 무관한 로직 클래스를 두고 이것을 조립하는 방법을 택했다. 이것은 데이터를 문자 그대로 조립할 수 있게 해주었다. 위의 경우는 각 형식마다 로직을 별도로 작성해야 하지만, 아래의 경우에는 형식마다 이미 존재한 클래스를 결합하는데 그친다.

// 형식이 담긴 튜플을 선언한다
struct config {
	using types = std::tuple<int, float>
};

/*
part_impl은 config::types를 순회해서 형식에 따라 지정된 형식으로 감싼다
*/
template<typename CONFIG>
using part_concrete = part_impl<CONFIG>;

'코드' 카테고리의 다른 글

GPUView를 위한 log.cmd가 실행되지 않을 때  (0) 2021.05.13
Game Server Performance on Tom Clancy's The Division 2  (0) 2021.04.20
위치 지정 new  (0) 2021.02.17
메모리  (0) 2021.02.15
alignas  (0) 2021.02.03