값도 받을 수 있는 템플릿

template <typename T, unsigned int N>
class Array {
  T data[N];
public:
  Array(T (&arr)[N]) {
    for (int i = 0; i < N; i++) {
      data[i] = arr[i];
    }
  }

  T* get_array() { return data; }

  unsigned int size() { return N; }

  void print_all() {
    for (int i = 0; i < N; i++) {
      cout << data[i] << ", ";
    }
    cout << endl;
  }
};

int main() {
  int arr[3] = {1, 2, 3};

  Array<int, 3> arr_w(arr);
  arr_w.print_all();
}

template <typename T, unsigned int N> 같은 식으로 값도 받을 수 있다. 하지만 객체는 받을 수 없다 (컴파일 타임엔 알 수 없으므로) 그래서 constant, pointer, reference 만 받을 수 있다.

두 개는 같은 클래스?

같은 객체 (인스턴스) 말고, 같은 클래스로 구체화되었는지 알아보자.

Array<int, 5> Array<int, 3>

왜냐하면 N 자리가 다르기 때문에 구체화가 2벌 되었을 것으로 예상할 수도 있다. 정말인지 확인해보자.

cout << (typeid(Array<int, 3>) == typeid(Array<int, 5>)) << endl;

결과는 ‘0’ 이 나온다. 즉, 다르다는 것이다. 예상대로 N 값이 달라 서로 다르게 풀린 것이다.

놓치면 안 되는 것 : typedef 가 값을 나타낸다고?

template <int N>
struct Int {
  static const int num = N;
};

이런 구조체가 있다고 하자. 그러면 이렇게 타입 재정의를 할 수 있겠다.

typedef struct Int<1> one;
cout << one::num << endl; // 1

구조체에 static const 가 있는데, 이건 그냥 타입이다. one::num 은 코드 영역에 있다. 메모리에 없다. (물론 읽으면 메모리에 있겠지만 그 개념이 아님)

그럼 이런 ‘값을 나타내는 구조체’ 를 사칙 연산한 새로운 구조체를 만들 수 있지 않을까?

template <typename T, typename U>
struct add {
  typedef Int<T::num + U::num> result;
};
typedef add<one, one>::result two; // 새로운 타입 add<one, one>
cout << two::num << endl; // 2 (1+1)

자, 핵심은 ‘값을 나타내는 (템플릿) 타입들을 가지고 계산을 미리 하자’ 는 것이다.

컴파일 타임 확정 = TMP!

템플릿 메타 프로그래밍 (TMP) 은 컴파일 타임에 확정된 템플릿 코드를 그대로 결과로 반환하는 것으로 해석하면 된다. 즉, 매번 스택과 힙을 소비하는게 아니라 이미 코드로 미리 계산한다는 것이다.

TMP 예제 (1) Factorial

/* 컴파일 타임 팩토리얼 계산 */
#include <iostream>
using namespace std;

template <int N>
struct Factorial {
  static const int result = N * Factorial<N - 1>::result;
};

template <>
struct Factorial<1> {
  static const int result = 1;
};

int main() { cout << "6! = 1*2*3*4*5*6 = " << Factorial<6>::result << endl; }

이렇게 적으면, Factorial<1> ~ <6> 까지 모두 구체화될 것이다.

TMP 로 할 수 있는 것들을 정리해 보면,

TMP 왜 써요?

TMP 를 쓱 보면, 몇 가지 단점이 보인다.

하지만, 무엇보다도 빠르다. Boost 라이브러리를 비롯한 다양한 라이브러리들이 TMP 를 이용해서 구현되어 있다.

TMP 예제 (2) GCD

#include <iostream>
using namespace std;

template <int X, int Y>
struct GCD {
  static const int value = GCD<Y, X % Y>::value;
};

template <int X>
struct GCD<X, 0> {
  static const int value = X;
};

int main() { cout << "gcd (36, 24) :: " << GCD<36, 24>::value << endl; }

TMP 예제 (3)

Ratio 구조체를 만들 수 있다. 사칙 연산을 위의 add<> 처럼 계산할 수 있다. 만약 멤버 변수까지 쓰기 싫으면 한번 더 typedef 로 wrapping 하면 된다.

template <class R1, class R2>
struct _Ratio_add {
  typedef Ratio<R1::num * R2::den + R2::num * R1::den, R1::den * R2::den> type;
};
template <class R1, class R2>
struct Ratio_add : _Ratio_add<R1, R2>::type {};
// 이제부터 Ratio_add<Ratio1, Ratio2> 타입은, 컴파일러가 각 구조체의 값을 통해 더한 값을 계산해 둔 타입이 됩니다!

Using than typedef

언젠가 정리했지만, 다시 정리한다.

typedef Ratio_add<rat, rat2> rat3;
using rat3 = Ratio_add<rat, rat2>;

두 구문은 동일하지만, using 이 더 직관적이다. (rat3 은 변수가 아니라, 여전히 타입이다.)