KoreanFoodie's Study
C++ 기초 개념 9-3 : 템플릿 메타 프로그래밍 본문
모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 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;
}
'Tutorials > C++ : Beginner' 카테고리의 다른 글
C++ 기초 개념 10-1 : 벡터(vector), 리스트(list), 덱(deque) (0) | 2022.03.18 |
---|---|
C++ 기초 개념 9-4 : 템플릿 메타 프로그래밍 2 (의존 타입, auto) (0) | 2022.03.18 |
C++ 기초 개념 9-2 : 가변 길이 템플릿(variadic template), 파라미터 팩(parameter pack), Fold 형식(Fold expression) (0) | 2022.01.14 |
C++ 기초 개념 9-1 : C++ 템플릿, 함수 객체(Functor) (0) | 2022.01.12 |
C++ 기초 개념 7-2 : C++에서 파일 입출력 (istream, ofstream, stringstream) (0) | 2022.01.10 |