템플릿 메타 프로그래밍(TMP), Using, 의존 타입

하루공부·2024년 1월 19일
0

C++

목록 보기
8/25
post-thumbnail

C++ 아이콘 제작자: Darius Dan - Flaticon


템플릿 메타 프로그래밍(TMP)

  • 템플릿을 이용한 기법으로 컴파일러에게 프로그램 코드를 생성하도록 하는 방식
    ==> 컴파일 타임에 연산이 끝나는 프로그래밍
    ==> 컴파일 타임에 많은 연산을 하여 런타임의 연산을 줄여준다.
    • 타입의 경우 반드시 컴파일 타임에 확정되어야 하니 컴파일 타임에 모든 연산이 끝남
    • 이 때 동안 타입은 값을 가지지 않고 그저 타입만을 나타냈다.
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로 구현되어 있다.


  • 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가 컴파일 시점에 계산되기 위해서 위에서 말한 컴파일 타임에 정의되어야 한다.


  • 그럼 앞에서 계속 말한 컴파일 시점에서 알 수 있는 상수 값은 어떻게 정의하는가?

    이를 위해 템플릿을 사용하는 것이다.
    템플릿 인자로 넘겨주는 타입의 경우 앞서 말해 반드시 컴파일 타임때 모든 연산이 끝나기 때문이다.




using

  • 정수 아닌 유리수를 구하는 것을 구현해보자
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);

  • 위 예제는 객체를 만들어 더하는 함수를 작동한 것 같지만 사실은 생성된 객체는 1개도 없고 단순히 타입들을 컴파일러가 만들어낸 것



의존 타입

  • 컴파일러는 어떠한 식별자(이름)을 보았을 때 값인지 타입인지 결정해야 한다.
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 키워드만 앞에 붙여주면 된다.
    ==> 값의 경우는 뭘 안 붙여도 괜찮다. 기본적으로 값으로 생각하기 때문이다.


참조
공부한 내용 복습

개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

0개의 댓글