본문 바로가기

진리는어디에

템플릿 특화를 이용한 튜플 클래스

'Modern C++ Design'에서 소개되고 있는 튜플(Tuple)이라는 개념은 일종의 레코드로써 우리가 일반적으로 생각하는 구조체라고 생각하면 된다. 단 구조체는 만들 때 마다 멤버 변수의 이름이나 구조체의 이름을 지정해 줘야 하지만 '그것것에 대해 신경 쓸 필요가 없는.. 다시 말하면 '이름없는 구조체'를 튜플이라고 하고 있다.

어디에 쓰는지야 각자 프로그램을 만들면서 살다 보면 스스로 알게 될 것이고, 이번 포스트에서는 템플릿 특화(Template specialize) 를 이용한 튜플 클래스 구현에 대해서 알아 보겠다.


Tuple 클래스의 기본적인 개념은 '상속'이다.

클래스 Derived가 클래스 Base를 public으로 상속 한다고 생각해 보자. 이런 경우 Derived는 Base의 모든 멤버 변수를 상속 받을 수 있고 또한 접근 할 수 있다. 단, 하위 클래스에서 상위 클래스와 동일한 멤버 변수 혹은 함수를 사용한다면 하위 클래스에 의해 가리워지기 때문에 명시적으로 스코프를 지정하여 특정 멤버 변수에 접근해야 한다는 것이 유일하게 신경 써줘야 할 점이다.

Tuple 클래스는 위에서 설명한 상속의 개념과 멤버 변수 접근 방법을 기반으로 만들어 졌다. 템플릿 인자로 넘어오는 타입 리스트를 이용하여, 각 자료형을 담고 있는 클래스 코드를 생성하고 자료형의 나열 순서에 따라 상속 관계를 자동으로 지정해 줌으로써 최종적으로 모든 멤버 변수를 다 담고 있는 클래스 코드를 생성 해 낸다.

예를 들어 int, std::string, double 형의 타입 리스트가 주어진다고 하면 튜플 클래스는 :

class A {
    double m_value;
};

class B : public A {
    std::string m_value;
};

class C : pubilc B {
    int m_value;
};

와 같은 코드를 자동으로 생성해 준다.

이제 기본 개념은 익혔으니 코드를 살펴 보면서 Tuple 클래스가 어떻게 동작하는지 좀 더 자세히 살펴 보기로 하자.코드에서 사용되고 있는 NullType, Typelist 같은 부분은 'Modern C++ Design'에서 소개 되고 있는 내용을 그대로 가져 왔다(..라고 하지만 지금 쓰고 있는 내용 전부가 그 책에서 다루고 있는 것임..).

template <class TList>
struct Tuple;

template <>
struct Tuple <NullType> {
    Tuple() {};
    Tuple(NullType) {};
};

template <class T, class U>
struct Tuple <Typelist<T, U> > : public Tuple <U>
{
     // ... 다른 코드들
     T m_value;
};

위의 코드는 템플릿 특화를 이용하여 앞에서 설명한 클래스 사속의 자동화를 만들고 있다.

Tuple 클래스를 살펴 보면 기본형 템플릿 클래스를 선언하고, Typelist에 대한 처리와 NullType에 대한 처리를 하는  두개의 특화된 템플릿 클래스를 추가 했음을 알 수 있다. Typelist와 NullType에 관련된 사항은 Modern C++ Design 혹은 포스트 가장 밑에 Reference 항목 혹은 예제로 올려진 코드를 통해 확인하도록 하자.

우리가 TYPELIST_n 매크로를 이용하여 Tuple 클래스의 템플릿을 선언하게 되면, Typelist의 첫번째 인자로 자신의 로컬 영역에 변수를 선언하고, 나머지 인자를 이용해 새로운 부모 Tuple클래스를 만든다. 이런 일들은 컴파일 타임에 재귀적으로 일어나게 되며 Typelist NullType인자만 남아 또다른 특화된 클래스를 만나기 전까지 계속 된다.

위의 과정이 끝나면(NullType의 처리가 끝나면) 필요로 하는 모든 변수를 담고 있는 최종적인 클래스가 생성되고, 그 클래스 상속 트리의 깊이는 Typelist의 길이와 동일하다.

필요한 타입을 모두 담고 있는 클래스를 생성하는데는 성공 하였지만 이제 특정 변수에 접근하는 것이 문제다. 우리가 생성한 Tuple<XXX> 클래스들은 재귀적으로 생성되어 동일한 변수 이름을 사용한다. 이렇게 되면 우리가 사용하게 되는 최종적인 클래스에서는 단 하나의 변수 밖에 볼 수가 없게 된다.

이러한 동일한 이름을 가진 변수들에 접근하기 위해서는 변수 이름 앞에 스코프를 명시적으로 선언하여야만 해당 변수에 접근이 가능해 진다. 예를 들어 위의 class A, B, C 예로 다시 돌아 가서 A에서 선언한 m_value에 접근 하기 위해서는 :
class C : public B {...};
C obj;

obj.C::B::A::m_value;
obj.C::B::m_value;
와 같은 방법으로 변수에 접근해야 한다.

위와 같은 접근을 가능하게 해 주는 것이 Tuple 클래스 내의 Value() 멤버 함수다. 최하위 Tuple클래스는 컴파일 타임에 호출되는 Value() 함수와 템플릿 인자로 넘어오는 상수를 이용하여, 각 상수마다 다른 스코프의 변수에 접근하는 Value() 함수를 만들어 준다.
template <class T, class U>
struct Tuple <Typelist<T, U> > : public Tuple <U>
     // ...다른 코드들
     typedef Typelist<T, U> TYPELIST; // 그냥 편하게 써보려고..-_-;;

     template <unsigned int i>
     typename At<TYPELIST, i>::Result& // 리턴 값. Typelist의 i번째 타입 반환(?)
     Value()
     {
          typedef typename AtScope<TYPELIST, i>::Rsult Result; // 스코프 지정. i번째 부터의 타입 반환
          return Tuple<Result>::m_value;
     }
     // ...다른 코드들...
};

Value가 리턴하는 값을 알아 내기 위해 At 템플릿 클래스와 명시해야 할 스코프를 알아내기 위해 AtScope 템플릿 클래스가 사용되어 진다. 둘 다 재귀적으로 Typelist를 탐색하면서 인자로 주어진 상수가 0이되는 데이터 타입과 스코프를 리턴한다. 코드는 아래와 같다 :

template <class TList, unsigned int i> struct AtScope; // 클래스 원형

template <class T, class U>
struct AtScope<Typelist<T, U>, 0> // 특화된 구현 0 이면 더 이상의 재귀 호출을 하지 않는다
{
    typedef Typelist<T, U> Result;
};

template <class T, class U, unsigned int i>
struct AtScope<Typelist<T, U>, i> // 0이 아닌 상수일 경우 -1하여 계속 재귀적으로 코드 생성
{
    typedef typename AtScope<U, i-1>::Result Result;
};

template <class TList, unsigned int> struct At;

template <class T, class U>
struct At<Typelist<T, U>, 0>
{
    typedef T Result;
};

template <class T, class U, unsigned int i>
struct At<Typelist<T, U>, i>
{
    typedef typename At<U, i-1>::Result Result;
};

여기까지 하면 기본적인 Tuple의 기능은 완료된다. 하지만 매번 사용할 때 마다 각 멤버 변수에 접근하여 값을 일일이 집어 넣어주는 것은 여간 귀찮은 일이 아니다. 그래서 이번에는 생성자를 추가 해 보도록 하자.

생성자는 PARAMLIST_n의 인자를 받아 들인다. 기본적인 개념은 Typelist와 비슷하나 이번에는 데이터 타입이 아닌 변수의 값을 저장한다. 예를 들면 :
#define PARAMLIST_1(P1) std::make_pair(P1, NullType())
#define PARAMLIST_2(P1, P2) std::make_pair(P1, PARAMLIST_1(P2))
#define PARAMLIST_3(P1, P2, P3) std::make_pair(P1, PARAMLIST_2(P2, P3))
... 계속 필요한 만큼 만들것...
와 같이 작성 될 수 있다.

Tuple 클래스의 생성자는 PARAMLIST를 인자로 받아 가장 첫번째 인자는 자신의 로컬 변수를 초기화 하는데 사용하고, 나머지 인자를 상위 클래스의 초기화로 넘긴다.
template <class T, class U>
struct Tuple <Typelist<T, U> > : public Tuple <U> {
     // ...다른 코드들...

     Tuple() {};  // 기본 생성자
     template <class PList> Tuple(PList plist); // 파라메터 리스트를 인자로 받는 생성자

     // ...다른 코드들...
};

template <class T, class U> // 클래스의 템플릿 인자
template <class PList> // 멤버 함수의 템플릿 인자
Tuple<Typelist<T, U> >::Tuple(PList plist) : m_value(plist.first), Tuple<U>(plist.second) {}

이렇게 PARAMLIST의 가장 앞에 있는 변수를 이용해 초기화하는 코드는 NullType 변수를 만날때 까지 계속되며, NullType를 만나게 되면 아무런 초기화 작업도 하지 않고 재귀적 초기화 과정은 종료 종료된다.

지금 까지 작성 해온 Tuple 클래스의 사용 예제 코드를 보면 아래와 같다(전체 코드를 보고 싶다면 첨부 파일을 받아 보도록 하자). :

int main()
{
     typedef Tuple<TYPELIST_3(int, std::string, double)> THREE;
     THREE three(PARAMLIST_3(1, "2", 3.0f));
     std::cout << three.Value<0>() << std::endl;
     std::cout << three.Value<1>() << std::endl;
     std::cout << three.Value<2>() << std::endl;

     typedef Tuple<TYPELIST_1(std::string)> VarToken;
     VarToken varToken(PARAMLIST_1("int"));
     std::cout << varToken.Value<0>() << std::endl;
     return 0;
}

위와 같이 새로운 구조체 생성의 필요 없이 간단한 typedef 문하나만으로 다양한 타입을 가지는 Tuple클래스를 생성할 수 있다. 단점이라면 클래스의 각 변수를 이름이 아닌 숫자로 접근해야 한다는 단점이 있어, 멤버 변수가 많다거나 방대한 양의 코드때문에 순서를 제대로 기억하지 못할 때는 정상적으로 사용하기가 까다롭다.

개인적으로 Tuple 클래스가 가장 적합하게 사용 될 때는 딱히 구조체를 만들기는 뭐하고, 데이터는 넘겨야 하겠고, 별다른 부담 없는 자료형이 없을까 할 때 사용 한다거나, union으로 만들기도 그렇고, void* 로 만들기 뭐할 때 사용 하면 부담 없이 쓰고 버릴 수 있는 자료형으로 사용하면 좋을 듯하다.

이상 포스트를 마친다..(간만에 쓰니..힘들다...헥헥..)

Reference :
 * NullType에 대한 설명 : http://ikpil.tistory.com/1035
 * Typelist에 대한 설명 : http://ikpil.tistory.com/search/Typelist
 * [진리는어디에] - 템플릿 특화(Template specialize)
 * boost Tuple Library(와방 좋음) : http://www.boost.org/doc/libs/1_42_0/libs/tuple/doc/tuple_users_guide.html
유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!