KoreanFoodie's Study

C++ 기초 개념 9-3 : 템플릿 메타 프로그래밍 본문

Tutorials/C++ : Beginner

C++ 기초 개념 9-3 : 템플릿 메타 프로그래밍

GoldGiver 2022. 1. 16. 20:33

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

템플릿 클래스 인스턴스는 같은 타입일까?

템플릿 클래스로 만든 두 클래스에서 인자만 바꾼다면, 해당 인스턴스들은 같은 타입일까? 다음 코드를 보자.

#include <iostream>
#include <typeinfo>

template <typename T, int N>
class Array {

	T data[N];

public:

	Array() {}
	Array(T (&arr)[N]) {
		for (int i = 0; i < N; ++i) {
			data[i] = arr[i];
		}
	}

	T* get_array() { return data; }
	int size() { return N; }

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

};



int main() {

	int arr3[3] = {1, 2, 3};

	Array<int, 3> Arr3(arr3);
	Array<int, 3> Arr3_2(arr3);

	if (typeid(Arr3) == typeid(Arr3_2)) {
		std::cout << "arr3 and arr3_2 are same" << std::endl;
	} else std::cout << "arr3 and arr3_2 are different" << std::endl;


	if (typeid(Array<int, 3>) == typeid(Array<int, 5>)) {
		std::cout << "Array<int, 3> and Array<int, 5> are same" << std::endl;
	} else std::cout << "Array<int, 3> and Array<int, 5> are different" << std::endl;
}

 

위의 코드처럼, Array<int, 3> 와 Array<int, 5> 는 다른 타입인 것을 확인할 수 있다. 왜냐하면 다른 템플릿 인자로 인스턴스화 되었기 때문이다.

이제 이런 속성을 활용한 Int wrapper 클래스를 만들어 활용해 보자.

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

typedef Int<1> one;
typedef Int<2> two;

 

위의 코드에서 static 이 붙은 이유는, 객체가 컴파일 타임에서 초기화되기 위해서는 static 이 붙어야 하기 때문이다. 그리고 const 를 추가하여 불변하지 않는 값임을 명시했다.

아래의 one 과 two 는 객체가 아닌 타입으로, 이를 활용하여 int 변수를 다루는 것처럼 연산자를 만들 수 있다.

template <typename T, typename U>
struct add {
	typedef Int<T::num + U::num> result;
};

int main() {

	typedef Int<1> one;
	typedef Int<2> two;

	one a;
	two b;

	typedef add<one, two>::result three;

	std::cout << "Adddition result : " << three::num << std::endl;
}

 

위의 코드를 보면, three 는 add<one, two>::result 로 치환되고, 이는 다시 Int<one::num + two::num> 으로 치환되므로, 자동적으로 Int<3> 으로 정의되게 된다. 따라서 출력은 다음과 같이 나오게 된다.

Adddition result : 3

 

 

템플릿 메타 프로그래밍

템플릿을 이용하면 객체를 생성하지 않더라도 타입을 부여할 수 있고, 또 그 타입들을 가지고 연산을 할 수 있다.

또한 타입은 컴파일 타임에 정해져야 하므로, 컴파일 타임에 연산이 완료된다. 이렇게 타입을 가지고 컴파일 타임에 생성되는 코드로 프로그래밍을 하는 것을 메타 프로그래밍(meta programming) 이라고 한다. C++ 의 경우 템플릿을 가지고 이러한 작업을 하기 때문에 템플릿 메타 프로그래밍, 줄여서 TMP라고 부른다.

#include <iostream>

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() {
	std::cout << "5*4*3*2*1 = " << factorial<5>::result << std::endl;
}

 

위의 코드를 실행시키면 120 이라는 값이 잘 출력된다. 하지만 실제로 120 이라는 값을 가지고 있는 변수는 메모리에 저장된 녀석이 아닌, 컴파일러가 만들어낸 factorial<5> 라는 타입일 뿐이다. for 과 if 문을 사용하는 코드들 역시 템플릿 메타 프로그래밍으로 구현할 수 있다.

 

 

TMP 를 쓰는 이유

모든 C++ 코드는 템플릿 메타 프로그래밍 코드로 변환할 수 있다. 이는 프로그램 속도를 향상시킬 수 있다. 하지만 단점 또한 존재하는데, 먼저

1. 컴파일 시간이 매우 길어진다(컴파일 타임에 연산을 다 끝마쳐야 하므로)

2. 코드가 매우 길어진다.

3. 디버깅이 어렵다(컴파일 타임에 연산이 되고, 템플릿 오류시 오류 길이가 길다)

하지만 위의 단점들에도 불구하고, 적절하게 사용하면 런타임에서의 프로그램 부하를 줄일 수 있다. 실제로 Boost 라이브러리나 속도가 매우 중요한 프로그램의 경우 이를 활용하고 있다.

유클리드 호제법을 TMP 로 구현한 예제를 보자.

#include <iostream>

template <int a, int b>
struct GCD {
	static const int result = GCD<b, a % b>::result;
};

template <int a>
struct GCD<a, 0> {
	static const int result = a;
};

int main() {
	std::cout << "GCD(12, 16) : " << GCD<12, 16>::result << std::endl;
}

 

위의 GCD 클래스를 이용해 Ratio 클래스를 만들어 보자.

template <int N, int D=1>
struct Ratio {
	typedef Ratio<N, D> type;
	static const int num = N;
	static const int den = D;
};

 

typedef Ratio<N, D> type; 으로 '자기 자신을 가리키는 타입'을 넣어 주었다. 이는 마치 클래스에서의 this 와 비슷한 역할이다. 이제 덧셈을 수행하는 템플릿을 만들어 보자.

template <int N, int D=1>
struct Ratio {
	typedef Ratio<N, D> type;
	static const int num = N;
	static const int den = D;
};

template <class r1, class r2>
struct _Ratio_Add {
	typedef Ratio<r1::num*r2::den + r1::den*r2::num, r1::den*r2::den> type;
};

template <class r1, class r2>
struct Ratio_Add : _Ratio_Add<r1, r2>::type {};


int main() {

	typedef	Ratio<1, 2> rat1;
	typedef Ratio<3, 4> rat2;
	
	// typedef _Ratio_Add<rat1, rat2> rat3; 를 간소화
	typedef Ratio_Add<rat1, rat2> rat3;


	std::cout << "1/2 + 3/4 = " << rat3::num << "/" << rat3::den << std::endl;;

}

 

 

위에서 Ratio_Add 는 _Ratio_Add 뒤에 :: type 을 붙이는 것을 없애기 위해 만든 클래스이다.

또한 C++ 11 부터는 typedef 대신, 조금 더 직관적인 using 이라는 키워드를 사용할 수 있다.

// 아래 두 개는 같은 뜻
typedef Ratio_Add<rat1, rat2> rat3;
using rat3 = Ratio_Add<rat1, rat2>;

// 아래 두 개는 같은 뜻
typedef void (*func)(int, int); // 함수 포인터 정의
using func = void (*)(int, int);

 

 

GCD 를 이용해서 Ratio 를 조금 다듬어 보자. 참고로, 덧셈을 제외한 나머지 사칙 연산도 덧셈과 마찬가지의 방식으로 만들어 주기만 하면 된다.

#include <iostream>

template <int a, int b>
struct GCD {
	static const int result = GCD<b, a % b>::result;
};

template <int a>
struct GCD<a, 0> {
	static const int result = a;
};

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

public:
	using type = Ratio<N,D>;
	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 + r1::den*r2::num, r1::den*r2::den>;
};

template <class r1, class r2>
struct Ratio_Add : _Ratio_Add<r1, r2>::type {};


int main() {

	using rat1 = Ratio<1, 2>;
	using rat2 = Ratio<3, 4>;
	
	// typedef _Ratio_Add<rat1, rat2>::type rat3; 를 간소화
	using rat3 = Ratio_Add<rat1, rat2>;

	std::cout << "1/2 + 3/4 = " << rat3::num << "/" << rat3::den << std::endl;;

}

 

 

문제 1

N 번째 피보나치 수를 나타내는 TMP 를 만들어 보자.

int main() {
  std::cout << "5 번째 피보나치 수 :: " << fib<5>::result << std::endl;  // 5
}
더보기
template <int N>
struct fibonacci {
	static const int val = fibonacci<N-1>::val + fibonacci<N-2>::val;
};

template <>
struct fibonacci<1> {
	static const int val = 1;
};

template <>
struct fibonacci<2> {
	static const int val = 2;
};

int main() {
	std::cout << "fibonacci<10> = " << fibonacci<10>::val << std::endl;
}

 

 

문제 2

TMP 를 사용해서 어떤 수가 소수인지 아닌지 판별하는 프로그램을 만들어 보자!

더보기

(모법답안은 다음 강좌에. 아래 코드는 내가 짠 코드이다.)

#include <iostream>

template <int N, int i>
struct SqrtM {
	enum {
		val = (i*i <= N && (i + 1)*(i + 1) > N) ? i : SqrtM<N, i - 1 >::val
	};
};

template<int N>
struct SqrtM<N, 0> {
	enum {
		val = 0
	};
};

template <int N>
struct Sqrt {
	enum {
		val = SqrtM<N, N / 2>::val
	};
};

template <>
struct Sqrt<1> {
	enum {
		val = 1
	};
};

template <>
struct Sqrt<0> {
	enum {
		val = 0
	};
};

template <int N, int i>
struct _is_prime {
	static const bool val = (N % i == 0) ? false : _is_prime<N, i-1>::val;
};

template <int N>
struct _is_prime<N, 1> {
	static const bool val = true;
};

template <int N>
struct is_prime : _is_prime<N, Sqrt<N>::val> {};

template <>
struct is_prime<1> { static const bool val = false;  };

int main() {
	//std::cout << "Sqrt<25> = " << Sqrt<25>::val << std::endl;
	std::cout << std::boolalpha;
	std::cout << "is_prime<2> = " << is_prime<2>::val << std::endl;
	std::cout << "is_prime<13> = " << is_prime<13>::val << std::endl;
	std::cout << "is_prime<17> = " << is_prime<17>::val << std::endl;
	std::cout << "is_prime<38> = " << is_prime<38>::val << std::endl;
}

 

Comments