아헿헿헿 2022. 7. 7. 06:55

템플릿의 강력함을 보이기 위해서는 동종 컨테이너 뿐 아니라 배열과 유사한 형식들을 많이 사용하였는데, C++에서 동종이 아닌 저장 공간으로 클래스과 구조체가 존재합니다. 이와 유사하게 데이터를 모으는 방식으로 튜플이 존재하는데, 이는 클래스와 유사하지만 튜플은 요소를 위치로 참조하는데 반해 구조체는 이름으로 참조합니다. 위치를 이용한 인터페이스가 형식 목록에서 튜플을 보다 쉽게 만들기에 템플릿 메타 프로그래밍 기법에서 구조체 보다 튜플이 적합합니다. 이번 장에서는 std::tuple을 간략화한 Tuple 클래스를 만들어 이를 알아볼 예정입니다.

25.1 기본 튜플 설계

튜플은 함수 템플릿 get을 통해 접근하여 참조자를 반환 받습니다.

template<typename... Types>
class Tuple;

// recursive case:
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
{
 private:
  Head head;
  Tuple<Tail...> tail;
 public:
  // constructors:
  Tuple() {
  }
  Tuple(Head const& head, Tuple<Tail...> const& tail)
    : head(head), tail(tail) {
  }
  //...

  Head& getHead() { return head; }
  Head const& getHead() const { return head; }
  Tuple<Tail...>& getTail() { return tail; }
  Tuple<Tail...> const& getTail() const { return tail; }
};

// basis case:
template<>
class Tuple<> {
  // no storage required
};

재귀 상황에서 각 Tuple 인스턴스에는 목록 내의 첫 번재 요소를 저장하는 데이터 멤버인 head와 목록 내의 나머지 요소를 저장하는 데이터 멤버인 tail이 존재 합니다. 기본 상황은 비어있는 튜플이므로 따로 저장소가 존재하지 않습니다. get 함수는 호출을 위해 재귀적으로 수행됩니다.

// recursive case:
template<unsigned N>
struct TupleGet {
  template<typename Head, typename... Tail>
  static auto apply(Tuple<Head, Tail...> const& t) {
    return TupleGet<N-1>::apply(t.getTail());
  }
};

// basis case:
template<>
struct TupleGet<0> {
  template<typename Head, typename... Tail>
  static Head const& apply(Tuple<Head, Tail...> const& t) {
    return t.getHead();
  }
};

template<unsigned N, typename... Types>
auto get(Tuple<Types...> const& t) {
  return TupleGet<N>::apply(t);
}

튜플의 생성자는 유용하지만, 독립적인 값 집합과 다른 튜플에서 튜플을 생성하기를 원합니다. 특히, 사용자는 요소의 일부 값 초기화할 때 이동 생성하기를 원할 수도 있으며, 다양한 형식의 값에서 생성된 요소를 원할 수도 있습니다. 따라서 완벽한 전달을 사용할 필요가 있습니다. 또한 튜플을 통해 다른 튜플을 생성하기를 원하지만 이때 템플릿을 enable_if로 적절하게 제어할 필요가 있습니다.

25.2 기본 튜플 연산

튜플의 비교는 다음과 같이 진행될 수 있습니다.

// basis case:
bool operator==(Tuple<> const&, Tuple<> const&)
{
  // empty tuples are always equivalent
  return true;
}

// recursive case:
template<typename Head1, typename... Tail1,
         typename Head2, typename... Tail2,
         typename = std::enable_if_t<sizeof...(Tail1)==sizeof...(Tail2)>>
bool operator==(Tuple<Head1, Tail1...> const& lhs,
                Tuple<Head2, Tail2...> const& rhs)
{
  return lhs.getHead() == rhs.getHead() &&
         lhs.getTail() == rhs.getTail();
}

이도 재귀적으로 이루어지며, 출력도 재귀적으로 수행할 수 있습니다.

25.3 튜플 알고리즘

튜플은 각 요소에 접근하고 수정할 수 있는 컨테이너일 뿐만 아니라, 새로운 튜플을 생성하고 분리할 수도 있습니다. 따라서 이를 통해 추가,제거, 재배열 및 하위 집합 선택 등의 여러 알고리즘을 만들 수 있습니다. 이는 앞서 TypeList에서 한 것과 동일하게 수행될 수 있습니다.

// basis case
template<typename V>
Tuple<V> pushBack(Tuple<> const&, V const& value)
{
  return Tuple<V>(value);
}

// recursive case
template<typename Head, typename... Tail, typename V>
Tuple<Head, Tail..., V>
pushBack(Tuple<Head, Tail...> const& tuple, V const& value) 
{
  return Tuple<Head, Tail..., V>(tuple.getHead(),
                                 pushBack(tuple.getTail(), value));
}

또한 역전 알고리즘도 이와 같이 재귀를 통해 구성할 수 있습니다. 하지만 이와 같이 비슷하게 역전 알고리즘을 짠다면, 자가 복제가 상당히 많이 일어나기 때문에, 비효율 적입니다. 따라서 한 튜플에 대해서만 역전 연산을 구현하면 효율적으로 구현이 가능한데, 이는 인덱스 목록을 표현하는 std::integer_sequence를 통해 수행 가능합니다.

 먼저 다음과 같이 역순의 인덱스 목록을 얻는 템플릿을 만들고 이를 통해, 역으로 하나씩 뒤에서부터 앞으로 구축해나가는 식으로 해결해나갈 수 있습니다.

// recursive case
template<unsigned N, typename Result = Valuelist<unsigned>>
struct MakeIndexListT
 : MakeIndexListT<N-1, PushFront<Result, CTValue<unsigned, N-1>>>
{
};

// basis case
template<typename Result>
struct MakeIndexListT<0, Result>
{
  using Type = Result;
};

template<unsigned N>
using MakeIndexList = typename MakeIndexListT<N>::Type;

template<typename... Elements, unsigned... Indices>
auto reverseImpl(Tuple<Elements...> const& t, 
                 Valuelist<unsigned, Indices...>)
{
  return makeTuple(get<Indices>(t)...);
}

template<typename... Elements>
auto reverse(Tuple<Elements...> const& t) 
{
  return reverseImpl(t, 
                     Reverse<MakeIndexList<sizeof...(Elements)>>());
}

이와 비슷하게 특정 위치의 튜플도 가져올 수 있습니다. 이를 확장하여 splat이라는 함수는 가져온 형식을 정해진 수 만큼 복사하는데 이는 다음과 같이 구현 가능합니다.

template<unsigned I, unsigned N, typename IndexList = Valuelist<unsigned>>
class ReplicatedIndexListT;

template<unsigned I, unsigned N, unsigned... Indices>
class ReplicatedIndexListT<I, N, Valuelist<unsigned, Indices...>>
 : public ReplicatedIndexListT<I, N-1,
                               Valuelist<unsigned, Indices..., I>> {
};

template<unsigned I, unsigned... Indices>
class ReplicatedIndexListT<I, 0, Valuelist<unsigned, Indices...>> {
 public:
  using Type = Valuelist<unsigned, Indices...>;
};

template<unsigned I, unsigned N>
using ReplicatedIndexList = typename ReplicatedIndexListT<I, N>::Type;

template<unsigned I, unsigned N, typename... Elements>
auto splat(Tuple<Elements...> const& t) 
{
  return select(t, ReplicatedIndexList<I, N>());
}

이와 같이 수행하면 정렬도 가능합니다.

// metafunction wrapper that compares the elements in a tuple:
template<typename List, template<typename T, typename U> class F>
class MetafunOfNthElementT {
 public:
  template<typename T, typename U> class Apply;

  template<unsigned N, unsigned M> 
  class Apply<CTValue<unsigned, M>, CTValue<unsigned, N>>
    : public F<NthElement<List, M>, NthElement<List, N>> { };
};

// sort a tuple based on comparing the element types:
template<template<typename T, typename U> class Compare,
         typename... Elements>
auto sort(Tuple<Elements...> const& t)
{
  return select(t, 
                InsertionSort<MakeIndexList<sizeof...(Elements)>,
                              MetafunOfNthElementT<
                                         Tuple<Elements...>,
                                         Compare>::template Apply>());
}

25.4 튜플 확장

튜플은 서로 관련 있는 값들을 한 값으로 묶을 때 유용한데, 어느 순간에 이르면 이를 풀어야 합니다. 튜플을 풀어 낼려면 인덱스 목록을 사용하여 이를 재귀적으로 풀어 나가야만 합니다.

template<typename F, typename... Elements, unsigned... Indices>
auto applyImpl(F f, Tuple<Elements...> const& t,
                    Valuelist<unsigned, Indices...>)
  ->decltype(f(get<Indices>(t)...))
{
  return f(get<Indices>(t)...);
}

template<typename F, typename... Elements, 
         unsigned N = sizeof...(Elements)>
auto apply(F f, Tuple<Elements...> const& t) 
  ->decltype(applyImpl(f, t, MakeIndexList<N>()))
{
  return applyImpl(f, t, MakeIndexList<N>());
}

25.5 튜플 최적화

튜플은 많은 공간을 차지하는데 그 중에서도 Tail이 빈 튜플로 끝나 공간을 차지합니다. 이를 해결하려면 꼬리를 멤버가 아니라 상속을 받게할 수 있지만, 이는 튜플 요소가 생성자에서 초기화될 때 순서가 바뀌게 되어 문제가 발생할 수 있습니다. 이 문제는 기본 클래스 목록 내에 Tail에 앞서는 기본 클래스를 만들어 그 클래스 안에 head 멤버를 둔다면 해결할 수 있습니다. 하지만 이 방법도 동일한 형식이 두개면 요소를 추출하기 어렵기에 튜플의 높이를 미리 새겨 놓는 것도 방법이 됩니다.

template<typename... Types>
class Tuple;

template<unsigned Height, typename T>
class TupleElt {
  T value;
 public:
  TupleElt() = default;

  template<typename U>
  TupleElt(U&& other) : value(std::forward<U>(other)) { }

  T&       get()       { return value; }
  T const& get() const { return value; }
};

// recursive case:
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> 
 : private TupleElt<sizeof...(Tail), Head>, private Tuple<Tail...>
{
  using HeadElt = TupleElt<sizeof...(Tail), Head>;
 public:
  Head& getHead() { 
    return static_cast<HeadElt *>(this)->get();
  }
  Head const& getHead() const { 
    return static_cast<HeadElt const*>(this)->get();
  }
  Tuple<Tail...>& getTail() { return *this; }
  Tuple<Tail...> const& getTail() const { return *this; }
};

// basis case:
template<>
class Tuple<> {
  // no storage required
};

25.6 튜플 첨자

튜플의 요소에 접근하는 operator[]를 정의할 수 있지만, 튜플의 요소는 형식이 서로 다를 수 있기에 요소의 인덱스에 따라 결과형이 달라져야 합니다. 이는 CTValue를 통해 구현 가능합니다.

template<typename T, T Value>
struct CTValue {
  static constexpr T value = Value;
};


template<typename T, T Index>
auto& operator[](CTValue<T, Index>) {
  return get<Index>(*this);
};