C++ 아이콘 제작자: Darius Dan - Flaticon
- 타입의 경우 반드시 컴파일 타임에 확정되어야 하니 컴파일 타임에 모든 연산이 끝남
- 이 때 동안 타입은 값을 가지지 않고 그저 타입만을 나타냈다.
template <int N>
struct Int {
static const int num = N;
};
template <typename T, typename U>
struct add {
typedef Int<T::num + U::num> result;
};
int main() {
typedef Int<1> one; -> 그냥 타입인데 값이 있는 타입으로 볼 수 있다.
typedef Int<2> two;
typedef add<one, two>::result three;
std::cout << "Addtion result : " << three::num << std::endl;
}
- 여기서 흥미로운 점은 3이라는 출력 값이 프로그램이 실행되면서 계산되는 것이 아니라
컴파일 시에 three::num를 3으로 이미 결정되어 있다- 타입은 반드시 컴파일 타임에 확정되어야 하므로, 컴파일 타임에 모든 연산이 끝난거다.
==> 템플릿 메타 프로그래밍.
- 위 예제 처럼 객체를 생성하지 않고 타입에 어떠한 값을 부여할 수 있고 그 타입으로 연산을 한다.
- 타입으로 연산한다 했지만 예시 처럼 컴파일 타임에 연산하기 위해서는 컴파일할 때 컴파일러가 템플릿의 인자를 정확하게 알고 있어야 하므로 ==> 컴파일 시점에서 알 수 있는 상수 값이여야 한다.
왜 TMP를 쓰는가?
사실 어떠한 c++코드도 TMP코드로 변환할 수 있다(코드가 길어지겠지)
TMP코드는 모두 컴파일 타임에 모든 연산이 끝나기 때문데 프로그램 실행 속도를 향상 시킬 수 있다는 장점이 있다. (컴파일 시간 늘어남)
그리고 프로그램을 실행 했을 때 치명적인 오류가 나면 안되는 프로젝트를 할 때
런타임 오류가 아닌 컴파일 오류로 오류를 잡아내기 위해 TMP를 활용하여 미리 찾을 수 있다.
그렇다고 TMP로 모든 로직을 구현하지는 않는다
일단 매우 복잡하고 TMP로 작성된 코드는 버그를 찾는 것이 매우 힘들다.
기본적으로 컴파일 타임에 연산하는 것이기 때문에 디버깅이 불가능하고
C++컴파일 특성상 오류 발생시 엄청난 길이의 오류를 보인다.
따라서 컴파일 타임에 여러 오류들을 잡아내고 속도가 중요한 프로그램의 경우 런타임 속도를 향상시킨다.
많은 라이브러리들은 이미 TMP로 구현되어 있다.
int factorial(int n) { // 일반적인 재귀 함수
if (n == 0)
return 1;
return n * factorial(n - 1);
}
template <int N> // 템플릿 메타프로그래밍
struct Factorial {
enum { value = N * Factorial<N - 1>::value };
};
template <>
struct Factorial<0> { // 이렇게 따로 재귀를 탈출하는 함수를 만들어야 하는 단점이 있음
enum { value = 1 };
};
// Factorial<4>::value == 24
// Factorial<0>::value == 1
첫 번째 예제는 프로그래밍이 실행될 때 계산하고
두 번째는 컴파일할 때 값을 구하면서 계산된다.
==> 여기서Factorial<>::value
가 컴파일 시점에 계산되기 위해서 위에서 말한 컴파일 타임에 정의되어야 한다.
이를 위해 템플릿을 사용하는 것이다.
템플릿 인자로 넘겨주는 타입의 경우 앞서 말해 반드시 컴파일 타임때 모든 연산이 끝나기 때문이다.
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 + R2::num * R1::den, R1::den* R2::den> type;
};
int main(){
typedef Ratio<1, 2> rat1;
typedef Ratio<2, 1> rat2;
typedef _Ratio_add<rat, rat2>::type rat3;
}
여기서 ::tpye 을 붙이기 귀찮다면
template <class R1, class R2> struct Ratio_add : _Ratio_add<R1, R2>::type {};
처럼 _Ratio_add<R1, R2>::type 를 상속 받는 Ratio_add 클래스를 만들어 버리면 Ratio_add는 마치 Ratio 타입 처럼 사용할 수 있다.
C++11부터 typedef 대신에 좀 더 직관적인 using 키워드를 사용
typedef Ratio_add<rat, rat2> rat3;
using rat3 = Ratio_add<rat, rat2>; ==> 동일한 의미또 함수 포인터의 경우 만일 void 를 리턴하고 int, int 를 인자로 받는 함수의 포인터의 타입을 func 라고 정의하기 위해서는
typedef void (*func)(int, int);
처럼 작성하지만
using으로 더 보기 좋게 만들 수 있다.using func = void (*)(int, int);
template <typename T>
int func() {
T::t* p;
}
class A {
const static int t; };
class B {
using t = int; };
- A클레스에 대해 func함수를 특수화 하면 t는 int값이 되어 t 곱하기 p로 동작 // p가 무엇인지 생각x
- B클래스에 대해 특수화를 시키면 int형 포인터 p를 선언하는 꼴이 된다.
이렇게 되면 컴파일러가 두 상황을 명확히 파악하기 위해 T::t가 타입인지 값인지 알려줘야한다.
이렇게 템플릿 인자에 따라 타입이 달라질 수 있는 것을 의존 타입이라고 한다.
타입이라는 것을 알려주기 위해 간단하게 typename 키워드만 앞에 붙여주면 된다.
==> 값의 경우는 뭘 안 붙여도 괜찮다. 기본적으로 값으로 생각하기 때문이다.
개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!