[Effective C++] 48. 템플릿 메타프로그래밍, 하지 않겠는가?
템플릿 메타프로그래밍이란?
컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 일을 뜻한다. 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>::iterator iter;
...
advance(iter, 10); //advance 템플릿 함수 호출
위 코드를 컴파일러가 돌린다고 가정했을 때 다음과 같은 advance가 생긴다.
void advance(list<int>::iterator& IterT, int d)
{
if (typeid(std::iterator_traits<list<int>::iterator>::iterator_category)
== typeid(std::random_access_iterator_tag))
{
iter += d; //임의 접근 반복자에 대해서 반복자 산술 연산
}
else
{
if (d >= 0) //다른 종류의 반복자에 대해서 ++혹은 --연산의 반복
{
while (d--)
++iter;
}
else
{
while (d++)
--iter;
}
}
}
보면 지금의 경우는 list <int>::iterator에 +=를 쓰려고 한 것인데, 이것은 양방향 반복자라 +=연산을 지원하지 않는다.
그리고 typeid점검이 실패하기 때문에 += 줄까지도 실행될 수 없다.
템플릿 메타프로그래밍 예제
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()
{
std::cout << Factorial<5>::value << endl; // 120을 런타임 계산 없이 출력
std::cout << Factorial<10>::value << endl; // 3628800을 런타임 계산 없이 출력
}
위 코드에서 루프를 도는 위치는 템플릿 인스턴스인 Factorial <n>의 내부에서 또 다른 템플릿 인스턴스인 Factorial <n-1>을 참조하는 곳이다. 그리고 Factorial <0>에서 재귀를 끝낸다.
진짜 루프가 있었다면 이 값은 루프가 한 번 돌 때마다 갱신되겠지만, TMP는 루프 대신에 재귀식 인스턴스화를 사용하기 때문에 템플릿 인스턴스화 버전마다 자체적으로 value의 사본을 갖게 되고 value에는 루프를 한 번 돌 때 만들어지는 값을 갖고 있다.
C++ 프로그래밍에서 TMP를 제대로 활용할 수 있는 예
1. 치수 단위(dimensional unit)의 정확성 확인
TMP를 사용하면 프로그램 안에서 쓰이는 모든 치수 단위의 조합이 제대로 됐는지 계산 시간에 상관없이 컴파일 동안에 볼 수 있다. 선행 에러 탐지(early error detection)에 TMP를 사용할 수 있는 사례이다.
또, 분수식 지수 표현을 지원한다. 이런 표현이 가능하려면 컴파일러가 확인할 수 있도록 컴파일 도중에 분수의 약분이 되어야 한다. 예를 들자면 time1/2와 time4/8은 약분하면 같으므로 컴파일러는 똑같이 받아들여야 한다.
2. 행렬 연산의 최적화
operator* 등의 어떤 연산자 함수는 연산 결과를 새로운 객체에 담아 반환해야 한다.
아래의 예시를 봐보자.
BigMatrix m1, m2, m3, m4; // 행렬 생성
BigMatrix result = m1 * m2 * m3 * m4; // 행렬 곱
곱셈 결과를 보통 방법으로 계산하려면 n-1개의 임시 행렬이 생겨야 한다.
그뿐 아니라, 행렬 원소들 사이에 곱셈을 해야 하므로 n-1의 루프가 순차적으로 만들어질 것이다. 이런 값 비싼 연산에 TMP를 응용한 기술인 표현식 템플릿(expression template)을 사용하면 임시 객체를 없애는 건 물론, 루프까지 합쳐 버릴 수 있다. 이로써 메모리도 적게 먹으면서 속도는 빠른 소프트웨어가 탄생한다.
3. 맞춤식 디자인 패턴 구현의 생성
전략(strategy) 패턴, 감시자(Observer) 패턴, 방문자(Visitor) 패턴 등의 디자인 패턴은 그 구현 방법이 여러 가지 일 것이다. TMP를 사용한 프로그래밍 기술인 정책 기반 설계(policy-based design)라는 것을 사용하면, 따로따로 마련된 설계상의 선택(정책 : policy)을 나타내는 템플릿을 만들어낼 수 있다. 이렇게 만들어진 정책 템플릿은 서로 임의대로 조합되어 사용자의 취향에 맞는 동작을 갖는 패턴으로 구현되는 데 쓰인다.
이 기술의 예로, 몇 개의 스마트 포인터 동작 정책을 하나씩 구현한 각각의 템플릿을 만들어 놓고, 이들을 사용자가 마음대로 조합하여 수백 가지의 스마트 포인터 타입을 컴파일 도중에 생성할 수 있게 한다.
이 기술은 생성식 프로그래밍(generative programming)의 기초이다.
요약
- 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환하는 효과를 낸다. 따라서 TMP를 쓰면 선행 에러 탐지와 높은 런타임 효율을 손에 거머쥘 수 있다.
- TMP는 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있으며, 또한 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 쓸 수 있다.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지 파악해 두자 (0) | 2022.07.16 |
---|---|
[Effective C++] 49. new 처리자의 동작 원리를 제대로 이해하자 (0) | 2022.07.16 |
[Effective C++] 47. 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 (0) | 2022.07.15 |
[Effective C++] 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2022.07.15 |
[Effective C++] 43. 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2022.07.08 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지 파악해 두자
[Effective C++] 50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지 파악해 두자
2022.07.16 -
[Effective C++] 49. new 처리자의 동작 원리를 제대로 이해하자
[Effective C++] 49. new 처리자의 동작 원리를 제대로 이해하자
2022.07.16 -
[Effective C++] 47. 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자
[Effective C++] 47. 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자
2022.07.15 -
[Effective C++] 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
[Effective C++] 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
2022.07.15