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 키워드만 앞에 붙여주면 된다.
==> 값의 경우는 뭘 안 붙여도 괜찮다. 기본적으로 값으로 생각하기 때문이다.
개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!