템플릿 메타프로그래밍(template metaprogramming : TMP)은 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 일을 뜻한다. TMP 프로그램이 실행을 마친 후엔 그 결과로 나온 템플릿으로 부터 인스턴스화된 C++ 소스코드가 다시 보통의 컴파일 과정을 거치는 것이다.
템플릿 메타프로그래밍(TMP)의 장점
1.TMP를 쓰면 다른 방법으로는 까다롭거나 불가능한 일을 쉽게 할 수 있다.
2.C++ 컴파일이 진행되는 동안에 실행되기 때문에, 기존 작업을 런타임 영역에서 컴파일 타임 영역으로 전환할 수 있다.
런타임 영역을 컴파일 타임 영역으로 끌고오기 때문에 실행 도중 터졌던 문제를 컴파일 도중에 찾을 수 있다.
또한, 컴파일 타임에 동작을 다 하기 때문에 실행 코드가 작아지고, 실행 시간도 짧아지며, 메모리도 적게 잡아먹는다. 물론 컴파일 타임은 길어지긴 한다.
런타임 영역의 예시를 들어보자면
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterT>::iterator_category
== typeid(std::random_access_iterator_tag))
{
iter += d; //임의 접근 반복자에 대해서 반복자 산술 연산
}
else
{
if (d >= 0) //다른 종류의 반복자에 대해서 ++혹은 --연산의 반복
{
while (d--)
++iter;
}
else
{
while (d++)
--iter;
}
}
}
advance 템플릿 함수에 들어서면 typeid를 사용하여 템플릿 매개변수 IterT에 따라 조건문에 진입한다. 이 과정은 런타임에 발생한다. 생각해보면 advance 템플릿 함수를 템플릿 매개변수 IterT를 갖고 호출하는 시점은 런타임이다.
typeid 연산자를 쓰는 이 방법은 특성정보(traits)를 쓰는 방법보다 효율이 떨어진다(47장 참고).
list<int> l;
list<int>::iterator iter = l.begin();
advance(iter, 10); //advance 템플릿 함수 호출
위 코드처럼 advance 함수를 사용했을 때 advance 템플릿 함수의 코드는
void advance(list<int>::iterator& IterT, int 5)
{
if (typeid(std::iterator_traits<list<int>::iterator>::iterator_category)
== typeid(std::random_access_iterator_tag))
{
iter += 5; //임의 접근 반복자에 대해서 반복자 산술 연산
}
else
{
if (5 >= 0) //다른 종류의 반복자에 대해서 ++혹은 --연산의 반복
{
while (5--)
++iter;
}
else
{
while (5++)
--iter;
}
}
}
이런 코드가 될 것이다. 결론부터 말하면 에러가 발생한다.
list<int>::iterator는 양방향 반복자(47장 참고)이기 때문에 += 연산을 하지 못한다. += 연산은 임이 접근 반복자만 가능하기 때문이다. 또한, 조건문에서 list<int>::iterator에 대해 typeid 점검도 실패한다.
템플릿 메타프로그래밍(TMP)의 동작 원리
TMP는 그 자체가 튜링 완전성을 갖고 있다고 한다. 범용 프로그래밍 언어처럼 어떤 것이든 계산할 수 있다는 뜻이다. 변수 선언도 되고, 루프도 실행시킬 수 있으며, 함수를 작성하고 호출도 가능하다. 단, 이런 것들에 필요한 구문요소가 보통의 C++에서 쓰이는 구문요소들과 다른 모습을 갖고 있다는 점이 특이하다.
TMP에는 반복(iteration)은 재귀(recursion)를 사용하여 루프 효과를 낸다. 내가 평상시 알고 있는 재귀가 아니다. TMP의 루프는 재귀 함수 호출을 만들지 않고, 재귀식 템플릿 인스턴스화(recursive template istantiation)를 한다.
template<unsigned n>
struct Factorial
{
enum { value = n * Factorial<n - 1>::value };;
};
template<> //템플릿 특수화
struct Factorial<0>
{
enum { value = 1 };
};
int main()
{
cout << Factorial<5>::value << endl;
cout << Factorial<2>::value << endl;
system("Pause");
return 0;
}
위 코드 같은 템플릿 메타프로그램이 있으면 Factorial<n>::value를 참조함으로써 n의 계승을 바로 얻을 수 있다.
Factorial<n>::value의 내부에서 또 다른 템플릿 인스턴스인 Factorial<n-1>을 참조하고있다.
Factorial 템플릿은 구조체 타입이 인스턴스화되도록 만들어져 있고, 구조체 않의 value라는 이름의 TMP 변수가 선언되어 있는데, 나열자 둔갑술(enum hack)(2장 참고)이 사용된 것이다. 이 value 변수는 현재 계산된 계승 값을 담는 역할을 맡고 있다. 진짜 루프가 있었다면 이 값은 루프가 한 번 돌떄마다 갱신되겠지만, TMP는 루프 대신에 재귀식 인스턴스화를 사용하기 때문에 템플릿 인스턴스화 버전마다 자체적으로 value의 사본을 갖게 되고 value에는 루프를 한 번 돌 때 만들어지는 그 값을 갖고 있다.
템플릿 메타프로그래밍(TMP)가 효율적인 상황
1.치수 단위(dimensional unit)의 정확성 확인
TMP를 사용하면 프로그램 안에서 쓰이는 모든 치수 단위의 조합이 제대로 됐는지 계산 시간에 상관없이 컴파일 동안에 볼 수있다. 선행 에러 탐지(early error detection)에 TMP를 사용할 수 있는 사례이다.
분수식 지수 표현이 가능해진다. 이런 표현이 가능하려면, 컴파일러가 확인할 수 있도록 컴파일 도중에 분수의 약분이 되어야 한다. 예를 들자면 time1/2와 time4/8은 약분하면 같으므로 컴파일러는 똑같이 받아들여야 한다.
2.행렬 연산의 최적화
operator* 등의 어떤 연산자 함수는 연산 결과를 새로운 객체에 담아 반환해야 한다(21장 참고). 곱셀 결과를 보통 방법으로 계산하려면 다수의 임시 행렬이 생겨야한다.
Matrix m1, m2, m3, m4;
Matrix result = m1 * m2 * m3 * m4; //곱셈 연산 할 때 마다 임시 객체 1개씩
그뿐 아니라, 행렬 원소들 사이에 곱셍을 해야 하므로 다수의 루프가 순차적으로 만들어 질 것이다. 이런 값 비싼 연산에 TMP를 응용한 기술인 표현식 템플릿(expression template)을 사용하면 임시 객체를 없애는건 물론, 루프까지 합쳐 버릴 수 있다. 이로써 메모리도 적게 먹으면서 속도는 빠른 소프트웨어가 탄생한다.
3.맞춤식 디자인 패턴 구현의 생성
전략(strategy) 패턴, 감시자(Observer) 패턴, 방문자(Visitor) 패턴 등의 디자인 패턴은 그 구현 방법이 여러 가지 일 것이다. TMP를 사용한 프로그래밍 기술인 정책 기반 설계(policy-based design)라는 것을 사용하면, 따로따로 마련된 설계상의 선택(정책 : policy)을 나타내는 템플릿을 만들어낼 수 있다. 이렇게 만들어진 정책 템플릿은 서로 임의대로 조합되어 사용자의 취향에 맞는 동작을 갖는 패턴으로 구현되는 데 쓰인다.
이 기술의 예로, 몇 개의 스마트 포인터 동작 정책을 하나씩 구현한 각각의 템플릿을 만들어 놓고, 이들을 사용자가 마음대로 조합하여 수백 가지의 스마트 포인터 타입을 컴파일 도중에 생성할 수 있게 한다.
이 기술은 생성식 프로그래밍(generative programming)의 기초이다.
TMP 정리
사실 잘 모르겠다. 아직 내가 초보라 그런건지 모르니 일단 요약은 했으니, 좀 더 고수가 되면 수정해보자.
이것만은 잊지 말자
템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환하는 효과를 나타낸다. 따라서 TMP를 쓰면 선행 에러 탐지와 높은 런타임 효율을 얻을 수 있다.
TMP는 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는데 쓸 수 있으며, 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 사용할 수 있다.
'서적 정리 > Effective C++' 카테고리의 다른 글
52.위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자 (0) | 2022.01.03 |
---|---|
51.new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 (0) | 2022.01.02 |
50.new 및 delete를 언제 바꿔야 좋은 소리를 들을지 파악해 두자 (0) | 2022.01.01 |
49.new 처리자의 동작 원리를 제대로 이해하자 (0) | 2021.12.29 |
47.타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 (0) | 2021.12.25 |
46.타입 변환이 바람직한 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2021.12.25 |
45."호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! (0) | 2021.12.24 |
44.매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (0) | 2021.12.24 |
댓글