C++ 템플릿(2)

은수·2022년 6월 6일

cpp study

목록 보기
9/21

일반화 프로그래밍 (generic programming)

템플릿을 통해서 타입이 마치 인자 인것 처럼 사용하는 것
템플릿 인자로는 타입 뿐만이 아니라 특정한 조건을 만족하는 값들도 올 수 있음.

Array<int, 5> Array<int, 3>

위의 두 개 클래스는 다른 클래스임. , 다른 템플릿 인자로 인스턴스화 되었기 때문! 컴파일러는 Array<int, 5> 와 Array<int, 3> 를 위해 각기 다른 코드를 생성하며 다른 클래스의 객체들을 만들어요

#include <iostream>
#include <typeinfo>

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() {
  // one 타입과 two 타입은 1과 2의 값을 나타내는 타입
  typedef Int<1> one;
  typedef Int<2> two;

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

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

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

위 add 클래스의 템플릿은 인자로 두 개의 타입을 받아서 그 타입의 num 멤버를 더해서 새로운 타입인 result 를 만들어 냄.

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

실제 덧셈을 수행하는 부분. add 클래스를 함수라고 생각한다면 그 계산 결과를 내부 result 타입으로 반환하는 것!

  • one과 two를 더한 것을 나타내는 타입이 result로 정의되고, 이를 three라고 부름
  • 여기서 3은 프로그램이 실행되면서 계산되는 것이 아니라, 컴파일 시에 컴파일러가 three::num을 3으로 치환함.
  • 즉, add가 수행되는 시기는 컴파일을 할 때이고, 런타임 시에는 단순히 결괄르 보여주는 것!

템플릿 메타 프로그래밍 (Template Meta Programming - TMP)

  • 템플릿을 사용하면 객체를 생성하지 않더라도, 타입에 어떠한 을 부여할 수 있고, 또 그 타입들을 가지고 연산 을 할 수 있음.
  • 또한 타입은 반드시 컴파일 타임에 확정되어야 하므로, 컴파일 타임에 모든 연산이 끝남.
  • 이렇게 타입을 가지고 컴파일 타임에 생성되는 코드로 프로그래밍을 하는 것 = 메타 프로그래밍(meta programming)
  • cpp의 경우 템플릿을 가지고 이런 작업을 하기 때문에 템플릿 메타 프로그래밍(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 << "6! = 1*2*3*4*5*6 = " << Factorial<6>::result << std::endl;
}

// 실행 결과
// 6! = 1*2*3*4*5*6 = 720
template <>
struct Factorial<1> {
  static const int result = 1;
};

위의 구문을 통해 Factorial<1> 타입의 경우만 따로 result=1로 만들어주어 재귀적 구조가 끝날 수 있게 해줌. 출력 결과인 720은, 단순히 컴파일러가 만들어낸 Factorial<6> 이라는 type을 나타내고 있을 뿐임.


TMP 를 왜 쓰는가?

  • 어떠한 C++ 코드도 템플릿 메타 프로그래밍 코드로 변환할 수 있음
  • 또한, 템플릿 메타 프로그래밍으로 작성된 코드는 모두 컴파일 타임에 모든 연산이 끝나기 때문에 프로그램 실행 속도를 향상 시킬 수 있다는 장점이 있음

but,

  • 템플릿 메타 프로그래밍으로 프로그램 전체를 구현하는 일은 없음
  • why?
    - 템플릿 메타 프로그래밍은 매우 복잡함
    - 템플릿 메타 프로그래밍으로 작성된 코드는 버그를 찾는 것이 매우 힘듦('컴파일' 타임에 연산하는 것이기 때문에 디버깅이 불가능 하고, C++ 컴파일러에 특성 상 템플릿 오류 시에 엄청난 길이의 오류 발생)

따라서 TMP 를 이용하는 경우는 꽤나 제한적이지만, 많은 C++ 라이브러리들이 TMP 를 이용해서 구현되었음. (Boost 라이브러리) 또한, TMP 를 통해서 컴파일 타임에 여러 오류들을 잡아낼 수 도 있고 (Ex. 단위나 통화 일치 여부등등) 속도가 매우 중요한 프로그램의 경우 TMP 를 통해서 런타임 속도도 향상 시킬 수 있음.


int gcd(int a, int b) {
  if (b == 0) {
    return a;
  }

  return gcd(b, a % b);
}

두 수의 최대공약수를 구하기 위한 유클리드 호제법

#include <iostream>

template <int X, int Y>
struct GCD {
  static const int value = GCD<Y, X % Y>::value;
};

template <int X>
struct GCD<X, 0> {
  static const int value = X;
};

int main() {
  std::cout << "gcd (36, 24) :: " << GCD<36, 24>::value << std::endl;
}

위의 코드를 TMP로 수정


using 키워드

typedef Ratio_add<rat, rat2> rat3;
using rat3 = Ratio_add<rat, rat2>;

typedef 대신 위와 같이 using 이라는 키워드를 사용할 수 있음.

typedef void (*func)(int, int);
using func = void (*)(int, int);

함수 포인터의 경우 만일 void 를 리턴하고 int, int 를 인자로 받는 함수의 포인터의 타입을 func 라고 정의하기 위해서는 typedef 로 첫번째 줄과 같이 작성했어야 했지만, 이를 using을 사용하면 직관적으로 나타낼 수 있음.


- auto 키워드

auto는 초기화 값에 따라 알아서 데이터 타입을 정해주는 키워드

auto 키워드는 템플릿의 사용으로 복잡해진 타입 이름들을 간단하게 나타낼 수 있는 획기적인 방법. 짧은 이름의 타입일 경우 그냥 써주는 것이 좋지만, 복잡한 타입 이름의 경우, 그 타입을 쉽게 추측할 수 있다면 auto 키워드를 활용하는 것도 좋음.

#include <iostream>
#include <typeinfo>

int sum(int a, int b) { return a + b; }

class SomeClass {
  int data;

 public:
  SomeClass(int d) : data(d) {}
  SomeClass(const SomeClass& s) : data(s.data) {}
};

int main() {
  auto c = sum(1, 2);  // 함수 리턴 타입으로 부터 int 라고 추측 가능
  auto num = 1.0 + 2.0;  // double 로 추측 가능!

  SomeClass some(10);
  auto some2 = some;

  auto some3(10);  // SomeClass 객체를 만들까요?

  std::cout << "c 의 타입은? :: " << typeid(c).name() << std::endl;
  std::cout << "num 의 타입은? :: " << typeid(num).name() << std::endl;
  std::cout << "some2 의 타입은? :: " << typeid(some2).name() << std::endl;
  std::cout << "some3 의 타입은? :: " << typeid(some3).name() << std::endl;
}

컴파일러는 최대한 단순한 방법으로 추론하기 때문에 auto some3(10); 를 int 변수로 만들었음.

  • auto 변수는 선언과 동시에 초기화를 해줘야 함.
  • auto 변수는 초기화 이후에 타입 변경이 불가능함.

0개의 댓글