모두의 코드
씹어먹는 C++ - <9 - 4. 템플릿 메타 프로그래밍 2>

작성일 : 2017-07-02 이 글은 32869 번 읽혔습니다.

이번 강좌에서는

  • 의존 타입 (dependent type)

  • Unit 라이브러리 만들기

  • auto 키워드

에 대해서 배웁니다.

안녕하세요 여러분! 지난 강좌에서 왜 TMP 를 활용하여 힘들게 힘들게 Ratio 클래스를 만들었는데, 아직 아마 이걸 왜 굳이 TMP 로 만들었는지는 설명하지 않았었습니다.

그에 앞서, 이번 강좌에서 왜 Ratio 클래스를 만들었는지 설명하기 전에 지난 강좌의 생각 해보기에 대해서 짚고 넘어가보자 합니다.

지난 강의 생각해보기 문제

지난번 생각해보기 문제는 아래와 같습니다.

TMP 를 사용해서 어떤 수가 소수인지 아닌지를 판별하는 프로그램을 만들어보세요. (난이도 : 상)

int main()
{
  std::cout << std::boolalpha;
  std::cout << "Is prime ? :: " << is_prime<2>::result << std::endl; // true
  std::cout << "Is prime ? :: " << is_prime<10>::result << std::endl; // false
  std::cout << "Is prime ? :: " << is_prime<11>::result << std::endl; // true
  std::cout << "Is prime ? :: " << is_prime<61>::result << std::endl; // true
}

사실 처음에 딱 보았을 때 도대체 어떻게 TMP 로 구현할 것인지 감이 안잡혔을 것입니다. 하지만 만약에 소수 인지 아닌지 판별하라는 '함수' 를 작성하게 하였다면 잘 작성하였겠지요. 아마 여러분은 아래와 같은 코드를 쓰셨을 것입니다.

bool is_prime(int N) {
  if (N == 2) return true;
  if (N == 3) return true;

  for (int i = 2; i <= N / 2; i++) {
    if (N % i == 0) return false;
  }

  return true;
}

왜 2 와 3 일 때 따로 처리하냐면 N / 2 까지 나누는 걸로 비교할 때 2, 3 일 경우 제대로 처리가 안되기 때문입니다. 이제 여러분이 해야할 일은 간단히 저 코드를 TMP 형식으로 옮기는 것입니다.

template <>
struct is_prime<2> {
  static const bool result = true;
};

template <>
struct is_prime<3> {
  static const bool result = true;
};

template <int N>
struct is_prime {
  static const bool result = !check_div<N, 2>::result;
};

template <int N, int d>
struct check_div {
  static const bool result = (N % d == 0) || check_div<N, d + 1>::result;
};

template <int N>
struct check_div<N, N / 2> {
  static const bool result = (N % (N / 2) == 0);
};

무언가 잘 짜여진 코드 같습니다. 하지만 실제로 컴파일 해보면 다음과 같은 오류가 발생합니다.

컴파일 오류

check_div<N,N/>: non-type parameter of a partial specialization must be a simple identifier

바로

template <int N>
struct check_div<N, N / 2> {
  static const bool result = (N % (N / 2) == 0);
};

이 부분에서 발생하는 문제 이지요. 위 오류가 발생한 문제는 템플릿 부분 특수화 시에 반드시 다른 연산자가 붙지 않고 단순한 식별자만 입력해주어야만 합니다. 따라서 C++ 컴파일러에 한계 상

컴파일 오류

struct check_div<N, N / 2>

와 같은 문법은 불가능 합니다. 그렇다면 이를 어떻게 해결할 수 있을까요? 생각을 잘 해보면, Nint 인자로 나타내는 대신에, 아예 N 을 나타내는 '타입' 으로 구현하면 어떨까요? 그렇다면 N / 2 역시, 직접 계산하는것이 아니라 N / 2 를 나타내는 타입으로 대체할 수 있고 따라서 템플릿 부분 특수화 문제를 해결할 수 있습니다.

따라서 아래와 같이 int 값을 표현하는 타입을 만들 수 있습니다.

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

template <typename a, typename b>
struct add {
  typedef INT<a::num + b::num> result;
};

template <typename a, typename b>
struct divide {
  typedef INT<a::num / b::num> result;
};

using one = INT<1>;
using two = INT<2>;
using three = INT<3>;

예를 들어 one 타입은 1을, two 타입은 2 를 나타내게 됩니다. 그렇다면 이를 바탕으로 TMP 코드를 수정해보도록 하겠습니다.

using one = INT<1>;
using two = INT<2>;
using three = INT<3>;

template <typename N, typename d>
struct check_div {
  // result 중에서 한 개라도 true 면 전체가 true
  static const bool result =
    (N::num % d::num == 0) || check_div<N, add<d, one>::result>::result;
};

template <typename N>
struct is_prime {
  static const bool result = !check_div<N, two>::result;
};

template <>
struct is_prime<two> {
  static const bool result = true;
};

template <>
struct is_prime<three> {
  static const bool result = true;
};

template <typename N>
struct check_div<N, divide<N, two>::result> {
  static const bool result = (N::num % (N::num / 2) == 0);
};

그런데 컴파일 한다면 다음과 같은 오류를 보게 됩니다.

컴파일 오류

'check_div': 'divide<N,two>::result' is not a valid template type argument for parameter 'd'

왜 저런 오류가 발생하였을까요? 일단 오류가 발생하는 다음 두 부분의 코드를 살펴보겠습니다.

(N::num % d::num == 0) || check_div<N, add<d, one>::result>::result;

struct check_div<N, divide<N, two>::result> {

입니다. 먼저 컴파일러 입장에서 저 ::result 를 어떻게 해석할지에 대해 생각해봅시다. 물론 우리는 add<d, one>::result 가 언제나 INT<> 타입 이라는 사실을 알고 있습니다. 왜냐하면 typename 인자로 들어오는 Nd 가 항상 INT 타입이기 때문에 저 result 를 항상 '타입'이네 라고 생각할 것입니다.

그런데, 컴파일러에 구조상 어떠한 식별자(변수 이름이든 함수 이름이든 코드 상의 이름들 - 위 코드의 경우 add, check_div,, result, one 등등 ...) 를 보았을 때 이 식별자가 '값' 인지 '타입' 인지 결정을 해야 합니다. 왜냐하면 예를들어서

template <typename T>
int func() {
  T::t* p;
}

class A {
  const static int t;
};

class B {
  using t = int;
};

위와 같은 템플릿 함수에서 저 문장을 해석할 때 만약에 클래스 A 에 대해서, func 함수를 특수화 한다면, t 가 어떠한 int 값이 되어서

T::t* p;

위 문장은 단순히 클래스 Atp 를 곱하는 식으로 해석이 됩니다.

반면에 func 함수가 클래스 B 에 대해서 특수화 된다면,

T::t* p;

이 문장은 int 형 포인터 p 를 선언하는 꼴이 되겠지요. 따라서 컴파일러가 이 두 상황을 명확히 구분하기 위해 저 T::t 가 타입인지 아니면 값인지 명확하게 알려줘야만 합니다.

우리가 쓴 코드도 마찬가지로 컴파일러가 result 가 항상 '타입' 인지 아니면 '값' 인지 알 수 없습니다. 예컨대 만약에

template <>
struct divide<int a, int b> {
  const static int result = a + b;
};

이런 템플릿이 정의가 되어있다면, 만약에 Ntwo 가 그냥 int 값이였다면 저 resultstatic const int 타입의 '값' 이 됩니다.이렇게 템플릿 인자에 따라서 어떠한 타입이 달라질 수 있는 것을 의존 타입(dependent type) 이라고 부릅니다. 위 경우 저 resultN 에 의존하기 때문에 의존 타입이 되겠지요.

따라서 컴파일러가 저 문장을 성공적으로 해석하기 위해서는 우리가 반드시 "야 저 result 는 무조건 타입이야" 라고 알려주어야만 합니다. 이를 위해서는 간단히 아래 코드 처럼

struct check_div<N, typename divide<N, two>::result> {

typename 키워드를 붙여주면 됩니다.마찬가지로

(N::num % d::num == 0) || check_div<N, add<d, one>::result>::result;

에서 typename 키워드를 붙인다면

(N::num % d::num == 0) || check_div<N, typename add<d, one>::result>::result;

이 되겠지요. 참고로 의존 '값' 의 경우 typename 을 안 붙여줘도 됩니다. 컴파일러는 어떤 식별자를 보았을 때 기본으로 '값' 이라고 생각합니다. 따라서 check_div 앞에 아무것도 안올 수 있는 것입니다 (check_divresultstatic const bool 이기 때문에!)

따라서 이를 고치면 다음과 같습니다.

template <typename N, typename d>
struct check_div {
  // result 중에서 한 개라도 true 면 전체가 true
  static const bool result = (N::num % d::num == 0) ||
                             check_div<N, typename add<d, one>::result>::result;
};

// 생략

template <typename N>
struct check_div<N, typename divide<N, two>::result> {
  static const bool result = (N::num % (N::num / 2) == 0);
};

마지막으로, 위 is_prime 을 사용하기 위해서는

is_prime<INT<11>>::result

이런 식으로 사용해야 합니다. 하지만 생각해보기에서 요구한 것은 is_prime<11>::result 로 사용하는 것이기 때문에 이를 위해서 is_prime 을 다음과 같이 정의하고, 기존의 is_prime_is_prime 으로 바꾸도록 하겠습니다.

template <int N>
struct is_prime {
  static const bool result = _is_prime<INT<N>>::result;
};

그렇다면 전체 코드를 살펴보겠습니다.

#include <iostream>

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

template <typename a, typename b>
struct add {
  typedef INT<a::num + b::num> result;
};

template <typename a, typename b>
struct divide {
  typedef INT<a::num / b::num> result;
};

using one = INT<1>;
using two = INT<2>;
using three = INT<3>;

template <typename N, typename d>
struct check_div {
  // result 중에서 한 개라도 true 면 전체가 true
  static const bool result = (N::num % d::num == 0) ||
                             check_div<N, typename add<d, one>::result>::result;
};

template <typename N>
struct _is_prime {
  static const bool result = !check_div<N, two>::result;
};

template <>
struct _is_prime<two> {
  static const bool result = true;
};

template <>
struct _is_prime<three> {
  static const bool result = true;
};

template <typename N>
struct check_div<N, typename divide<N, two>::result> {
  static const bool result = (N::num % (N::num / 2) == 0);
};

template <int N>
struct is_prime {
  static const bool result = _is_prime<INT<N>>::result;
};

int main() {
  std::cout << std::boolalpha;
  std::cout << "Is 2 prime ? :: " << is_prime<2>::result << std::endl;
  std::cout << "Is 10 prime ? :: " << is_prime<10>::result << std::endl;
  std::cout << "Is 11 prime ? :: " << is_prime<11>::result << std::endl;
  std::cout << "Is 61 prime ? :: " << is_prime<61>::result << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

Is 2 prime ? :: true
Is 10 prime ? :: false
Is 11 prime ? :: true
Is 61 prime ? :: true

와 같이 제대로 판별함을 알 수 있습니다.

참고로 컴파일러에 따라서 재귀적으로 몇 번까지 사용 가능한지 깊이가 정해져있기 때문에, 꽤 큰 수를 넣는다면 컴파일 오류가 발생할 수 도 있습니다.

자, 그렇다면 본격적으로 이번 강의를 시작해보도록 하겠습니다. 지난 강좌에서 Ratio 클래스를 만들면서 왜 굳이 이것을 TMP 로 만들었을까, 그냥 일반적인 클래스로 만들었다면 훨씬 직관적이고 편하지 않나라고 많이 생각하셨을 것입니다. 바로 지금부터 TMP 의 진짜 파워에 대해서 알아보도록 하겠습니다.

단위(Unit) 라이브러리

C++ 코드를 작성하는 이유는 여러가지가 있겠지만, 그 중 하나로 바로 여러 수치 계산을 사용하는데에도 많이 사용합니다. 예를 들어서 인공위성의 궤도를 계산한다던지, 입자의 운동을 계산한다던지 말이지요. 이러한 물리적 수치 계산 시에 꼭 필요한 것이 바로 '단위' 입니다.

단위라 하면 쉽게 말해 킬로그램 (kg), 미터 (m), 초 (s) 등을 생각하시면 됩니다. 이러한 것들을 계산하는 프로그램들의 경우, double 이나 float 변수에 들어가는 값에는 '단위' 가 붙어서 들어가겠지요.

예를 들어서 핸드폰의 가속도 센서에서 부터 데이터를 받는 프로그램은 아마도 m/s^2 단위로 데이터를 받겠지요. 혹은 시계로 부터 데이터를 받는 프로그램은 s 단위로 데이터를 받을 것 입니다.

아무튼 이렇게 단위가 붙은 데이터를 처리할 때 중요한 점은 바로 데이터를 연산할 때 항상 단위를 확인해야 된다는 점입니다. 예를 들어서, 다음과 같은 코드가 있다고 생각해봅시다.

float v1, v2;  // v1, v2 는 속도
std::cout << v1 + v2;

당연히 v1v2 는 속도 값을 나타내므로 같은 단위이기 때문에 더할 수 있습니다. (여기서 더할 수 있다는 말은 물리적으로 더한 값이 말이 된다는 의미 입니다). 반면에;

float v;        // 속도; m/s
float a;        // 가속도; m/s^2
std::cout << v + a;  // ???

만약에 v 가 속도를 나타내는 값이고, a 가 가속도를 나타내는 값이라면, v + a 는 불가능한 연산입니다. 만약에 프로그래머가 저러한 코드를 썻다면 분명히 실수일 것입니다. 물론 C++ 컴파일러 입장에서는 그냥 두 개의 float 변수를 더한 것이기 때문에 문제 없이 컴파일 됩니다. 하지만 프로그램을 돌리게 된다면 골치아픈 문제가 발생하겠지요.

실제로, NASA 의 경우 단위를 잘못 처리해서 1조원 짜리 화성 탐사선을 날려먹은 경우가 있습니다. 이 경우 1조원 자리 버그 이겠네요.

여러분 이라면 이러한 실수를 어떻게 막을 것인가요? 일단 가장 먼저 드는 생각으로 단위 데이터를 일반적인 변수에 보관하지 말고 클래스를 만들어서 클래스 객체에서 보관하는 것입니다. 그리고 operator+ 등으로 연산자들을 오버로딩 한 뒤에, 연산 시에 객체 끼리 단위를 체크해서 단위가 맞지 않으면 적절히 처리하면 됩니다.

물론 이 방법은 꽤나 괜찮아 보이지만 한 가지 문제가 있습니다. 만일 틀린 단위를 연산하는 코드가 매우 드물게 일어난다면 어떨까요? 즉 런타임에서 그 문제를 발견하지 못한 채 넘어갈 수 있다는 점입니다.

가장 이상적인 상황은 단위가 맞지 않는 연산을 수행하는 코드가 있다면 아예 컴파일 시에 오류를 발생시켜버리는 것입니다. 그렇다면 적어도 틀린 단위를 연산하는 일은 막을 수 있게 되고, 프로그램을 실행 시키면서 기다리는 수고를 줄일 수 있게 되지요.

이를 위해서 다음과 같은 클래스를 생각해봅시다.

template <typename U, typename V, typename W>
struct Dim {
  using M = U;  // kg
  using L = V;  // m
  using T = W;  // s

  using type = Dim<M, L, T>;
};

Dim 이라는 클래스는 어떠한 데이터의 단위를 나타내기 위해서 사용됩니다. 어떠한 물리량의 단위를 나타내기 위해서는 무게(kg), 길이(m), 시간(s) 이 3 개로 나타낼 수 있습니다. (실제로는 8개가 필요하지만 단순화를 위해 3개만 사용하도록 하겠습니다).

예를 들어서 속도의 경우 m/s 이므로, 저 Dim 클래스로 표현하자면 Dim<0, 1, -1> 로 나타낼 수 있습니다. 왜냐하면 m/s = kg^0 m^1 s^-1 이기 때문이지요.

마찬가지로 힘의 경우 단위가 kg m /s^2 이므로 Dim 클래스로 표현하자면 Dim<1, 1, -2> 가 됩니다.

물론 저 Dim 의 경우 템플릿 인자로 타입을 받기 때문에 단순히 Dim<0, 1, -1> 이렇게 사용할 수 있는 것이 아닙니다. 대신에 앞서 만들었던Ratio 클래스를 이용해서 저 숫자들을 '타입' 으로 표현해주어야 합니다. 따라서, 실제로는

Dim<1, 1, -2>

가 아니라

Dim<Ratio<1, 1>, Ratio<1, 1>, Ratio<-2, 1>>

이런 식으로 정의를 해야겠지요. 그렇다면 Dim 끼리 더하고 빼는 템플릿 클래스도 아래와 같이 만들 수 있게 됩니다.

template <typename U, typename V>
struct add_dim_ {
  typedef Dim<typename Ratio_add<typename U::M, typename V::M>::type,
              typename Ratio_add<typename U::L, typename V::L>::type,
              typename Ratio_add<typename U::T, typename V::T>::type>
    type;
};

template <typename U, typename V>
struct subtract_dim_ {
  typedef Dim<typename Ratio_subtract<typename U::M, typename V::M>::type,
              typename Ratio_subtract<typename U::L, typename V::L>::type,
              typename Ratio_subtract<typename U::T, typename V::T>::type>
    type;
};

typename 이 저렇게도 많이 붙어있는지는 아마 잘 이해하실 거라 생각합니다. 왜냐하면 예를 들어 M 의 경우 U 에 의존한 타입이고, type 의 경우도 마찬가지로 UV 에 의존하는 타입이기 때문이지요.

자 이제, 실제 데이터를 담는 클래스를 만들어보도록 하겠습니다.

template <typename T, typename D>
struct quantity {
  T q;
  using dim_type = D;
};

일단 위 처럼 q 라는 멤버 변수에 데이터를 담고, (데이터의 타입은 T 가 되겠지요), dim_type 에 차원 정보를 담게 됩니다. 차원 정보는 데이터와는 다르게 'Dim 타입' 그 자체로 표현됩니다.

자 이제, 실제로 quantity 객체를 가지고 연산을 수행하기 위해서는 우리가 연산자들을 오버로드 해줘야만 합니다. 일단 간단히 +- 연산자를 어떻게 오버로드 할 지 생각해봅시다. 앞서 말했듯이, 두 개의 데이터를 더하거나 빼기 위해서는 반드시 단위가 일치해야 합니다. 이 말은, dim_type 이 같은 타입이어야만 하다는 것이지요.

따라서 operator+operator- 는 다음과 같이 간단히 정의할 수 있습니다.

quantity operator+(quantity<T, D> quant) { return quantity<T, D>(q + quant.q); }
quantity operator-(quantity<T, D> quant) { return quantity<T, D>(q - quant.q); }

operator+ 는 인자로 받는 quantity 의 데이터 타입과 Dim 타입이 일치해야지만 인스턴스화 됩니다. 만약에, 데이터 타입이나 Dim 타입이 일치하지 않았더라면 저 operator+ 는 인스턴스화 될 수 없고 따라서 컴파일러는 저 operator+ 를 찾을 수 없다는 오류를 발생시키게 됩니다!

그렇다면 실제로 테스트를 해볼까요.

#include <iostream>

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;
};

template <int N, int D = 1>
struct Ratio {
 private:
  const static int _gcd = GCD<N, D>::value;

 public:
  typedef Ratio<N / _gcd, D / _gcd> type;
  static const int num = N / _gcd;
  static const int den = D / _gcd;
};
template <class R1, class R2>
struct _Ratio_add {
  using type = Ratio<R1::num * R2::den + R2::num * R1::den, R1::den * R2::den>;
};

template <class R1, class R2>
struct Ratio_add : _Ratio_add<R1, R2>::type {};

template <class R1, class R2>
struct _Ratio_subtract {
  using type = Ratio<R1::num * R2::den - R2::num * R1::den, R1::den * R2::den>;
};

template <class R1, class R2>
struct Ratio_subtract : _Ratio_subtract<R1, R2>::type {};

template <class R1, class R2>
struct _Ratio_multiply {
  using type = Ratio<R1::num * R2::num, R1::den * R2::den>;
};

template <class R1, class R2>
struct Ratio_multiply : _Ratio_multiply<R1, R2>::type {};

template <class R1, class R2>
struct _Ratio_divide {
  using type = Ratio<R1::num * R2::den, R1::den * R2::num>;
};

template <class R1, class R2>
struct Ratio_divide : _Ratio_divide<R1, R2>::type {};

template <typename U, typename V, typename W>
struct Dim {
  using M = U;
  using L = V;
  using T = W;

  using type = Dim<M, L, T>;
};

template <typename U, typename V>
struct add_dim_ {
  typedef Dim<typename Ratio_add<typename U::M, typename V::M>::type,
              typename Ratio_add<typename U::L, typename V::L>::type,
              typename Ratio_add<typename U::T, typename V::T>::type>
      type;
};

template <typename U, typename V>
struct subtract_dim_ {
  typedef Dim<typename Ratio_subtract<typename U::M, typename V::M>::type,
              typename Ratio_subtract<typename U::L, typename V::L>::type,
              typename Ratio_subtract<typename U::T, typename V::T>::type>
      type;
};

template <typename T, typename D>
struct quantity {
  T q;
  using dim_type = D;

  quantity operator+(quantity<T, D> quant) {
    return quantity<T, D>(q + quant.q);
  }

  quantity operator-(quantity<T, D> quant) {
    return quantity<T, D>(q - quant.q);
  }

  quantity(T q) : q(q) {}
};
int main() {
  using one = Ratio<1, 1>;
  using zero = Ratio<0, 1>;

  quantity<double, Dim<one, zero, zero>> kg(1);
  quantity<double, Dim<zero, one, zero>> meter(1);
  quantity<double, Dim<zero, zero, one>> second(1);

  // Good
  kg + kg;

  // Bad
  kg + meter;
}

컴파일 하였다면 다음과 같은 오류가 납니다.

컴파일 오류

no operator "+" matches these operands
binary '+': no operator found which takes a right-hand operand of type 'quantity<double,Dim<zero,one,zero>>' (or there is no acceptable conversion)

즉 위 + 에 해당하는 연산자 함수를 찾을 수 없다는 것이지요. 예상했던 대로,

위 부분에서 오류가 발생하는데, kgmeter 의 단위가 다르기 때문에 발생하게 됩니다. 반면에

// Good
kg + kg;

는 잘 컴파일되지요.

그렇다면 이제 */ 연산자만 만들어주면 되겠습니다. 하지만 */ 의 경우 +- 보다 좀 더 까다롭습니다. 왜냐하면 */ 의 경우 굳이 Dim 이 일치하지 않아도 되거든요! 다만 이 연산을 수행하였을 때 새로운 차원의 데이터가 나올 뿐입니다.

예를 들어서 가속도를 나타내기 위해서는

meter / (second * second)

이렇게 해주면 됩니다. 다만 새로운 차원의 데이터 (Dim<zero, one, minus_two>) 가 탄생할 뿐이지요. 따라서, operator*operator/ 의 경우 두 개의 다른 차원의 값을 받아도 처리할 수 있어야 합니다. 따라서 opreator*/ 를 정의해보자면 아래와 같습니다.

template <typename D2>
quantity<T, typename add_dim_<D, D2>::type> operator*(quantity<T, D2> quant) {
  return quantity<T, typename add_dim_<D, D2>::type>(q * quant.q);
}

template <typename D2>
quantity<T, typename subtract_dim_<D, D2>::type> operator/(
  quantity<T, D2> quant) {
  return quantity<T, typename subtract_dim_<D, D2>::type>(q / quant.q);
}

새로 만들어지는 타입의 차원은 당연히도 add_dim_<D, D2>::type 이 되겠고 (opreator* 의 경우), 그 값은 그냥 실제 값을 곱해주면 됩니다. 이와 더불어서

3 * kg

과 같은 곱도 처리해야 하기 때문에, 아래와 같은 함수들도 정의해줘야 합니다.

quantity<T, D> operator*(T scalar) { return quantity<T, D>(q * scalar); }
quantity<T, D> operator/(T scalar) { return quantity<T, D>(q / scalar); }

이는 위 처럼 일반적인 차원이 없는 값 과의 곱도 지원해줍니다. 그렇다면 예를 들어서 아래와 같이 정의된 F 의 타입은 어떻게 될까요?

// F 의 타입은?
F = kg * meter / (second * second);

일단 F 의 차원은 계산해보면 (1, 1, -2) 이렇게 나올 것 입니다. 따라서, Fdim 타입은 <Ratio<1, 1>, Ratio<1, 1>, Ratio<-2, 1>> 가 되겠지요. 다시 말해, F 를 다음과 같이 나타낼 수 있습니다.

quantity<double, Dim<one, one, Ratio<-2, 1>>> F =
    kg * meter / (second * second);

그런데, 매번 변수를 정의할 때 마다 저렇게 길고 긴 타입을 써주는 것은 매우 귀찮은 일입니다. 저 kg * meter / (second * second) 를 계산해서 나오는 객체의 타입이 저렇게 된다는 사실은 저도 알고 컴파일러도 알고 있습니다. 컴파일러가 쉽게 알아낼 수 있는 타입을 굳이 우리가 써주어야 할까요? 똑똑한 컴파일러가 타입을 알아서 생각하도록 하면 안될까요?

물론 가능합니다.

타입을 알아서 추측해라! `- auto` 키워드

C++ 코드를 많이 짜면서 느꼈겠지만, 객체를 생성할 때, 많은 경우 굳이 타입을 쓰지 않아도 알아서 추측할 수 있는 경우들이 많이 있습니다.

예를 들어서,

(??) a = 3;

와 같이 썼다면 저 (??) 는 아마 int 를 의도한 것이겠지요. 아니면

some_class a;
(??) b = a;

의 경우 저 (??) 에는 아마 some_class 가 들어가겠지요? 즉 객체가 복사 생성 될 때, 그 복사 생성하는 대상의 타입을 확실히 알 수 있다면 굳이 그 객체의 타입을 명시하지 않아도 컴파일러가 알아낼 수 있습니다.

물론 때때로 컴파일러가 타입을 제대로 유추할 수 없는 경우도 있습니다. 예를 들어서, 우리의 위 예제 코드에서

quantity<double, Dim<one, zero, zero>> kg(1);

의 경우 만약에 저 타입 부분을 가리고

(??) kg(1);

와 같이 살펴본다면 어떨까요? 컴파일러에 입장에서는 단순히 생각해봤을 때 그냥 1 로 초기화 하는 변수 이므로 (??) 에는 int 가 들어가겠지요. 따라서 이 경우에는 우리가 원하는 타입으로 생성할 수 없습니다. 반면에,

(??) F = kg * meter / (second * second);

F 의 경우 우리가 굳이 타입을 적지 않아도 컴파일러가 오른쪽의 연산을 통해서 F 의 타입을 정확하게 알아낼 수 있습니다.

이와 같이 컴파일러가 타입을 정확히 알아낼 수 있는 경우 굳이 그 길고 긴 타입을 적지 않고 간단히 auto 로 표현할 수 있습니다. 그리고 그 auto 에 해당하는 타입은 컴파일 시에 컴파일러에 의해 추론됩니다. 아래 간단한 예제를 살펴볼까요.

#include <iostream>
#include <typeinfo>

int sum(int a, int b) { return a + b; }

class SomeClass {
  int data;

 public:
  SomeClass(int d) : data(d) {}
  SomeClass(const SomeClass& s) : data(s.data) {}
};

int main() {
  auto c = sum(1, 2);  // 함수 리턴 타입으로 부터 int 라고 추측 가능
  auto num = 1.0 + 2.0;  // double 로 추측 가능!

  SomeClass some(10);
  auto some2 = some;

  auto some3(10);  // SomeClass 객체를 만들까요?

  std::cout << "c 의 타입은? :: " << typeid(c).name() << std::endl;
  std::cout << "num 의 타입은? :: " << typeid(num).name() << std::endl;
  std::cout << "some2 의 타입은? :: " << typeid(some2).name() << std::endl;
  std::cout << "some3 의 타입은? :: " << typeid(some3).name() << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

c 의 타입은? :: i
num 의 타입은? :: d
some2 의 타입은? :: 9SomeClass
some3 의 타입은? :: i

와 같이 나옵니다.

std::cout << "c 의 타입은? :: " << typeid(c).name() << std::endl;
std::cout << "num 의 타입은? :: " << typeid(num).name() << std::endl;
std::cout << "some2 의 타입은? :: " << typeid(some2).name() << std::endl;

일단 위 3줄은 우리의 예상대로 auto 키워드가 잘 타입을 추론해줍니다. c 의 경우 함수의 리턴 타입으로 부터 int 타입이라는 것을 알 수 있고, num 의 경우 1.0 + 2.0 의 결과가 double 이므로 num 역시 double 타입 변수로 초기화 됩니다. 마지막으로 some2 의 경우 SomeClass 타입인 some 으로 부터 복사 생성 되므로 SomeClass 타입이 되지요.

마지막으로 some3 를 살펴봅시다.

auto some3(10);  // SomeClass 객체를 만들까요?

이전에 some 을 만들 때 SomeClass some(10) 으로 만들었기 때문에 저 some3 도 혹시 SomeClass 타입으로 추론하지 않을까 생각할 수 있습니다. 하지만 컴파일러는 최대한 단순하게 가능한 방법으로 추론하기 때문에 (실제로 auto 타입을 추론하는 방법은 템플릿에 들어갈 타입을 추론하는 것과 동일합니다), 그냥 int 변수로 만들어 버립니다.

하지만 아래의 F 의 경우 정확히 타입을 추론할 수 있기 때문에 그냥

// F 의 타입은 굳이 알필요 없다!
auto F = kg * meter / (second * second);

위와 같이 auto 키워드를 이용하면 됩니다.

참고로 편의를 위해 quantityostream 으로 출력해주는 함수인

template <typename T, typename D>
std::ostream& operator<<(std::ostream& out, const quantity<T, D>& q) {
  out << q.q << "kg^" << D::M::num / D::M::den << "m^" << D::L::num / D::L::den
      << "s^" << D::T::num / D::T::den;

  return out;
}

를 제작하였습니다. 따라서 전체 코드를 살펴보면 다음과 같습니다.

#include <iostream>
#include <typeinfo>

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;
};

template <int N, int D = 1>
struct Ratio {
 private:
  const static int _gcd = GCD<N, D>::value;

 public:
  typedef Ratio<N / _gcd, D / _gcd> type;
  static const int num = N / _gcd;
  static const int den = D / _gcd;
};
template <class R1, class R2>
struct _Ratio_add {
  using type = Ratio<R1::num * R2::den + R2::num * R1::den, R1::den * R2::den>;
};

template <class R1, class R2>
struct Ratio_add : _Ratio_add<R1, R2>::type {};

template <class R1, class R2>
struct _Ratio_subtract {
  using type = Ratio<R1::num * R2::den - R2::num * R1::den, R1::den * R2::den>;
};

template <class R1, class R2>
struct Ratio_subtract : _Ratio_subtract<R1, R2>::type {};

template <class R1, class R2>
struct _Ratio_multiply {
  using type = Ratio<R1::num * R2::num, R1::den * R2::den>;
};

template <class R1, class R2>
struct Ratio_multiply : _Ratio_multiply<R1, R2>::type {};

template <class R1, class R2>
struct _Ratio_divide {
  using type = Ratio<R1::num * R2::den, R1::den * R2::num>;
};

template <class R1, class R2>
struct Ratio_divide : _Ratio_divide<R1, R2>::type {};

template <typename U, typename V, typename W>
struct Dim {
  using M = U;
  using L = V;
  using T = W;

  using type = Dim<M, L, T>;
};

template <typename U, typename V>
struct add_dim_ {
  typedef Dim<typename Ratio_add<typename U::M, typename V::M>::type,
              typename Ratio_add<typename U::L, typename V::L>::type,
              typename Ratio_add<typename U::T, typename V::T>::type>
      type;
};

template <typename U, typename V>
struct subtract_dim_ {
  typedef Dim<typename Ratio_subtract<typename U::M, typename V::M>::type,
              typename Ratio_subtract<typename U::L, typename V::L>::type,
              typename Ratio_subtract<typename U::T, typename V::T>::type>
      type;
};

template <typename T, typename D>
struct quantity {
  T q;
  using dim_type = D;

  quantity operator+(quantity<T, D> quant) {
    return quantity<T, D>(q + quant.q);
  }

  quantity operator-(quantity<T, D> quant) {
    return quantity<T, D>(q - quant.q);
  }

  template <typename D2>
  quantity<T, typename add_dim_<D, D2>::type> operator*(quantity<T, D2> quant) {
    return quantity<T, typename add_dim_<D, D2>::type>(q * quant.q);
  }

  template <typename D2>
  quantity<T, typename subtract_dim_<D, D2>::type> operator/(
      quantity<T, D2> quant) {
    return quantity<T, typename subtract_dim_<D, D2>::type>(q / quant.q);
  }

  // Scalar multiplication and division
  quantity<T, D> operator*(T scalar) { return quantity<T, D>(q * scalar); }

  quantity<T, D> operator/(T scalar) { return quantity<T, D>(q / scalar); }

  quantity(T q) : q(q) {}
};

template <typename T, typename D>
std::ostream& operator<<(std::ostream& out, const quantity<T, D>& q) {
  out << q.q << "kg^" << D::M::num / D::M::den << "m^" << D::L::num / D::L::den
      << "s^" << D::T::num / D::T::den;

  return out;
}

int main() {
  using one = Ratio<1, 1>;
  using zero = Ratio<0, 1>;

  quantity<double, Dim<one, zero, zero>> kg(2);
  quantity<double, Dim<zero, one, zero>> meter(3);
  quantity<double, Dim<zero, zero, one>> second(1);

  // F 의 타입은 굳이 알필요 없다!
  auto F = kg * meter / (second * second);
  std::cout << "2 kg 물체를 3m/s^2 의 가속도로 밀기 위한 힘의 크기는? " << F
            << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

2 kg 물체를 3m/s^2 의 가속도로 밀기 위한 힘의 크기는? 6kg^1m^1s^-2

와 같이 잘 나옵니다.

auto 키워드는 템플릿의 사용으로 복잡해진 타입 이름들을 간단하게 나타낼 수 있는 획기적인 방법입니다. 물론 짧은 이름의 타입일 경우 그냥 써주는 것이 좋지만 (왜냐면 그 코드를 읽는 사람에 입장에서 한눈에 타입을 알 수 있으면 좋기 때문에), 위 경우 처럼 복잡한 타입 이름의 경우, 그 타입을 쉽게 추측할 수 있다면 auto 키워드를 활용하는 것도 좋습니다.

이것으로 템플릿 메타프로그래밍에 대한 강좌를 마치도록 하겠습니다. 사실 실제 현업에서 템플릿 메타 프로그래밍을 활용하는 경우는 그다지 많지 않습니다. 왜냐하면 일단 TMP 의 특성상복잡하고, 머리를 매우 많이 써야되고, 무엇보다도 버그가 발생하였을 때 찾는 것이 매우 힘듧니다.

하지만 우리의 Unit 클래스 처럼 TMP 를 적절하게 활용하면 런타임에서 찾아야 하는 오류를 컴파일 타임에서 미리 다 잡아낼 수 도 있고, 런타임 시에 수행해야 하는 연산들도 일부 컴파일 타임으로 옮길 수 있습니다.

만약에 TMP 를 직접 작성할 일이 있다면 이미 TMP 를 그나마 편하게 수행하기 위해 만들어진 boost::MPL 라이브러리가 있습니다. 이 라이브러리를 활용하신다면 비교적 쉽게 TMP 코드를 짤 수 있을 것입니다!

다음 강좌에서는 C++ 의 또다른 막강한 무기인 표준 라이브러리 (STL) 에 대해 알아보도록 하겠습니다!

생각 해보기

문제 1

컴파일러가 auto 키워드에 들어갈 타입을 추측하는 방법은 템플릿에서 들어갈 타입을 추측하는 방법과 같습니다. 여기를 클릭해서 읽어보세요!

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <씹어먹는 C++ - <9 - 4. 템플릿 메타 프로그래밍 2>> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 53 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요

    댓글을 불러오는 중입니다..