- 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환하는 효과를 낸다. 따라서 TMP를 쓰면 선행 에러 탐지와 높은 런타임 효율을 낼 수 있다.
- TMP는 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있으며, 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 쓸 수 있다.
템플릿 메타프로그래밍
템플릿 메타프로그래밍(template metaprogramming: TMP)은 컴파일 도중에 실행되는 템플릿 기반의 프로그래밍을 말한다.
템플릿 메타프로그램은 C++ 컴파일러가 실행시키는, C++로 만들어진 프로그램이다.
TMP프로그램이 실행을 마친 후엔 그 결과로 나온 출력물이 다시 보통의 컴파일 과정을 거친다.
C++은 템플릿 메타프로그래밍을 염두에 두고 설계되지는 않았다.
템플릿이 C++에 추가되면서 TMP의 유용성이 드러나면서 C++ 언어 및 표준 라이브러리에 TMP를 용이하게 만드는 확장요소가 추가되었다.
TMP에는 강점이 두 개가 있다.
첫째, TMP를 쓰면 다른 방법으로는 까다롭거나 불가능한 일을 쉽게 할 수 있다.
둘째, 템플릿 메타프로그램은 C++ 컴파일이 진행되는 동안에 실행되기 때문에, 기존 작업을 런타임 영역에서 컴파일 타임 영역으로 전환할 수 있다.
이러한 장점 덕분에, 일반적으로 프로그램 실행 도중에 검출되던 에러들을 컴파일 도중에 찾을 수 있다.
그리고 TMP를 써서 만든 C++ 프로그램이 확실히 모든 면에서 효율적일 수 있다.
컴파일 타임에 동작하기 때문에 실행 코드가 작아지고, 실행 시간도 짧아지며, 메모리도 적게 잡아먹는 것이다.
하지만 기존 작업을 런타임에서 컴파일 타임으로 전환하면서 컴파일 타임이 길어질 수도 있다.
예를 들어 보자.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if(iter가 임의 접근 반복자)
{
iter += d;
}
else
{
if(d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
}
이 유사코드를 진짜 코드로 만들려면 typeid를 쓰면 된다.
타입 정보를 꺼내는 작업을 런타임에 하는 것이다.
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; }
}
}
typeid 연산자를 쓰는 방법은 특성정보를 쓰는 방법보다 효율이 떨어진다.
타입 점검 동작이 컴파일 도중이 아니라 런타임에 일어나고, 런타임 타입 점검을 수행하는 코드는 어쩔 수 없이 실행 파일에 들어가야 하기 때문이다.
TMP가 보통 C++ 프로그램보다 효율이 나은지 보여주는 예이다.
typeid 방법은 성능 외에도 컴파일 문제를 일으킬 수 있다.
std::list<int>::iterator iter;
...
advance(iter, 10);
위의 코드를 컴파일하면 템플릿 매개변수인 IterT 및 DistT에 대해 iter의 타입과 10의 타입을 넣고 나면, 다음과 같은 advance가 생길 것이다.
void advance(std::list<int>::iterator& iter, int d)
{
if(typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) == typeid(std::random_access_iterator_tag))
{
iter += d; // Error
}
else
{
if(d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
}
+=연산의 지원은 임의 접근 반복자에서만 가능하고 list<int>::iterator는 양방향 반복자이기 때문에 에러가 발생한다.
TMP를 쓴다면 이런한 문제를 해결할 수 있다.
주어진 타입에 따른 코드가 별도의 함수로 분리될 것이고, 각각의 함수는 자신이 맡은 타입에 대한 연산만 수행한다.
TMP는 그 자체가 튜링 완전성을 갖고 있다.
범용 프로그래밍 언어처럼 어떤 것이든 계산할 수 있는 능력을 갖고 있는 것이다.
단, 필요한 구성요소가 보통의 C++에서 쓰이는 구문요소들과 다른 모습을 갖고 있다.
예를 들면, if...else 조건문을 나타내는 데는 통상의 if문이 아닌 템플릿 및 템플릿 특수화 버전을 사용한다.
루프를 보면 TMP의 동작 원리를 알아보기 쉽다.
TMP에는 반복 의미의 진정한 루프는 없기 때문에, 재귀를 사용해서 루프의 효과를 낸다.
이러한 재귀도 일반적인 재귀가 아닌데, TMP의 루프는 재귀 함수 호출을 만들지 않고 재귀식 템플릿 인스턴스화를 하기 때문이다.
예시를 통해 TMP의 계승 계산를 확인해 보자.
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<> // 특수한 경우: Factorial<0>의 값은 1
struct Factorial<0> {
enum { value = 1 };
};
이렇게 만들어진 템플릿 메타프로그램이 있으면 Factorial<n>::value를 참조함으로써 n의 계승을 바로 얻을 수 있다.
이 코드에서 루프를 도는 위치는 템플릿 인스턴스인 Factorial<n>의 내부에서 또 다른 템플릿 인스턴스인 Factorial<n-1>을 참조하는 곳이다.
재귀코드에는 종료조건이 있어야 하는데 현재는 Factorial<0>이다.
Factorial 템플릿은 구조체 타입이 인스턴스화되도록 만들어져 있다.
그리고 이렇게 만들어진 구조체 안에는 value라는 이름의 TMP 변수가 선언되어 있는데, enum hack을 이용한 것이다.
이 value 변수는 현재 계승된 계승 값을 담는 역할을 맡는다.
만약 진짜 루프가 있었다면, 계속되어 만들어지는 템플릿 인스턴스화 버전마다 자체적으로 value의 사본을 갖게 되고, 각각의 value에는 루프를 한 번 돌 때 만들어지는 그 값이 담기게 된다.
Factorial 템플릿은 다음과 같이 사용하면 된다.
int main()
{
std::cout << Factorial<5>::value;
std::cout << Factorial<10>::value;
}
TMP 활용
- 치수 단위의 정확성 확인
과학 기술 분야의 응용프로그램을 만들 때는 치수 단위가 조합되어야 한다.
예를 들면, 속도를 나타내는 변수에 질량을 나타내는 변수를 대입하면 에러가 발생한다.
하지만 거리 변수를 시간 변수로 나누고 그 결과를 속도 변수에 대입하는 것은 맞다.
TMP를 사용하면 프로그램 안에서 쓰이는 모든 치수 단위의 조합이 제대로 됐는지 컴파일 타임에 점검할 수 있다.
이러한 경우가 선행 에러 탐지에 TMP를 활용한 것이다.
- 행렬 연선의 최적화
operator*등의 어떤 연산자 함수는 연산 결과를 새로운 객체에 담아 반환해야 한다.
typedef SquareMatrix<double, 10000> BigMatrix;
BigMatrix m1, m2, m3, m4, m5;
...
BigMatrix result = m1 * m2 * m3 * m4 * m5;
곱셈 결과를 계산하려면 네 개의 임시 행렬이 생겨야 한다.
operator*를 한 번씩 호출할 때마다 반환되는 결과로 생기는 것이다.
그뿐 아니라, 행렬 원소들 사이에 곱셈을 해야 하므로 네 개의 루프가 순차적으로 만들어질 수밖에 없다.
이런 비싼 연산에 TMP를 사용하면 덩치 큰 임시 객체를 없애는 것은 물론이고 루프까지 합쳐 버릴 수 있다.
- 맞춤 디자인 패턴 구현의 생성
전략 패턴, 감시작 패턴, 방문자 패턴 등의 디자인 패턴은 구현 방법이 여러 가지일 수 있다.
TMP를 사용한 프로그래밍 기술인 정책 기반 설계라는 것을 사용하면, 따로따로 마련된 설계상의 선택을 나타내는 템플릿을 만들어낼 수 있다.
이렇게 만들어진 정책 템플릿은 서로 임의대로 조합되어 사용자의 취향에 맞는 동작을 갖는 패턴으로 구현되는 데 쓰인다.
예를 하나 들면, 몇 개의 스마트 포인터 동작 정책을 하나씩 구현한 각각의 템플릿을 만들어 놓고, 이들을 사용자가 마음대로 조합하여 여러 가지의 스마트 포인터 타입을 생성할 수 있게 하는 것이다.
이 기술은 생성식 프로그래밍의 기초가 된다.
TMP는 쉽지 않은 분야이다.
문법은 비직관적이고, 개발도구의 지원도 미약하다.
그럼에도 다양한 작업을 컴파일 타임에 하는 등 큰 이점이 있다.