[Effective C++] Chapter 7. 템플릿과 일반화 프로그래밍
Table of Contents
이펙티브 C++ 책을 읽고 공부한 노트입니다.
Item 41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터 #
- 클래스와 템플릿은 모두 인터페이스와 다형성을 제공한다. 하지만 세부적인 차이점이 있다.
클래스의 인터페이스와 다형성 #
- 클래스의 경우, 인터페이스가 명시적(explicit interface) 이어서 함수의 시그니처(함수 이름, 매개변수 타입, 반환 타입 등)를 중심으로 구성되어 있다.
- 그리고 다형성은 런타임(runtime polimorphism) 에 가상 함수를 통해 나타난다.
class Widget
{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
};
void doProcessing(Widget& w)
// w는 Widget 타입이므로 Widget 인터페이스를 지원해야 한다.
// 그리고 이 인터페이스는 Widget 소스 코드를 보면 알 수 있다. (명시적 인터페이스)
// Widget 타입 인터페이스를 지원하지 않으면 컴파일이 안된다.
{
if (w.size() > 10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
// Widget의 멤버 함수 중 몇 개는 가상 함수이다.
// 그래서 런타임에 w의 동적 타입을 기반으로 해서
// 특정 함수 호출이 이루어진다. (런타임 다형성)
temp.swap(w);
}
}
템플릿의 인터페이스와 다형성 #
- 반면 템플릿의 경우, 인터페이스가 암시적(implicit interface) 이어서 유효 표현식(expression)에 기반을 두어 구성된다.
- 그리고 다형성은 컴파일(complie-time polymorphism) 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타난다.
template<typename T>
void doProcessing(T& w)
{
// w는 T 타입이다.
// 아래 if문의 결과를 bool로 낼 수 있어야 하며,
// normalize, swap 멤버 함수를 지원해야 하는 타입이다. (암시적 인터페이스)
// 이것들을 T 타입이 지원하지 않으면 컴파일이 안된다.
if (w.size() > 10 && w != someNastyWidget)
// 또한 컴파일 도중에 템플릿이 인스턴스화된다.
// 호출되는 함수는 인스턴스화된 함수 템플릿에 어떤 템플릿 매개변수가 들어가느냐에 따라 달라진다. (컴파일 타임 다형성)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}
Item 42: typename
의 두 가지 의미를 제대로 파악하자 #
템플릿 매개변수를 선언할 때 #
- 템플릿 매개변수를 선언할 때
class
와typename
은 완전히 똑같은 의미를 가진다.
template<class T> class Widget;
template<typename T> class Widget;
중첩 의존 타입 이름을 식별할 때 #
- 의존 이름(dependent name)
- 템플릿 매개변수에 종속된 템플릿 내의 이름이다.
- 중첩 의존 이름(nested dependent name)
- 의존 이름이 어떤 클래스 안에 중첩되어 있는 경우이다.
- 중첩 의존 타입 이름(nested dependent type name)
- 중첩 의존 이름인데 타입을 참조한다는 뜻이다.
- 비의존 이름(non-dependent name)
- 템플릿 매개변수와 상관 없는 이름이다.
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
// iter는 C::const_iterator 타입이다.
// 따라서 템플릿 매개변수인 C에 따라서 iter의 타입이 달라진다. (의존 이름)
C::const_iterator iter(container.begin());
// 에러!
++iter;
// value는 템플릿 매개변수 C와 상관없이 int인 타입이다. (비의존 이름)
int value = *iter;
std::cout << value;
}
}
- 중첩 의존 이름은 기본적으로 타입이 아닌 것으로 해석된다.
template<typename C>
void print2nd(const C& container)
{
C::const_iterator * x;
// C::const_iterator가 타입이 아니면 어떻게 될까?
// 예를 들면 const_iterator라는 이름을 가진 정적 데이터 멤버가 C에 들어있을 수도 있다.
// 그러면 위 식은 곱셈이 될 수도 있다.
// 그래서 C++은 이 모호성을 해결하기 위해 중접 의존 이름은 기본적으로 타입이 아닌 것으로 본다.
}
typename
을 붙여서 중첩 의존 이름이 타입이라고 알려줄 수 있다.
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
// typename을 붙인다.
typename C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
- 예외
- 기본 클래스 리스트, 멤버 초기화 리스트 내의 기본 클래스 식별자에서는
typename
을 붙여주면 안 된다.
- 기본 클래스 리스트, 멤버 초기화 리스트 내의 기본 클래스 식별자에서는
template<typename T>
class Derived : public Base<T>::Nested // 기본 클래스 리스트
{
public:
explicit Derived(int x) : Base<T>::Nested(x) // 멤버 초기화 리스트 내의 기본 클래스 식별자
{
typename Base<T>::Nested temp;
}
}
- 긴 타입 이름을 간편하게 사용하기 위해
typedef
를 사용할 수 있다.
template<typename IterT>
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
// (특성 정보 클래스에 속한 value_type 등의 멤버 이름은
// 그 멤버 이름과 똑같이 typedef 이름을 짓는게 관례이다.)
}
Item 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 #
- 어떤 템플릿으로부터 파생 클래스가 인스턴스화 될 때, 컴파일러는 기본 클래스에 대해 아무것도 모르는 것으로 가정한다.
class CompanyA
{
void sendClearText();
void sendEncrypted();
};
template<typename C>
class MsgSender
{
public:
void sendClear()
{
C c;
c.sendClearText();
}
void sendSecret()
{
C c;
c.sendEncrypted();
}
};
template<typename C>
class LoggingMsgSender : public MsgSender<C>
{
public:
void sendClearMsg()
{
sendClear();
// 컴파일 에러!
// LoggingMsgSender는
// MsgSender의 어떤 인스턴스에서 파생되었는지를 모른다.
// 즉, C가 무엇인지 모르므로
// MsgSender<C>가 어떤 형태인지 알 방법이 없다.
}
};
class CompanyB
{
void sendEncrypted();
};
// CompanyB가 LoggingMsgSender의 템플릿 매개변수 C가 되면 어떻게 되겠는가?
// sendClear에서는 CompanyB의 sendClearText를 부를텐데
// CompanyB는 sendClearText 함수가 없다.
// 따라서 컴파일 에러가 옳은 판단을 한 것이라 볼 수 있겠다.
- 완전 템플릿 특수화(total template specialization)
template<>
class MsgSender<CompanyB>
// MsgSender 템플릿이 CompanyB 타입에 대해 특수화되었고,
// 이 템플릿의 매개변수들이 하나도 빠짐없이 구체적인 타입으로 정해진다.
{
public:
void sendSecret()
{
// ...
}
};
템플릿화된 기본 클래스의 함수를 호출하는 법 #
- (1)
this->
를 붙인다.
template<typename C>
class LoggingMsgSender : public MsgSender<C>
{
public:
void sendClearMsg()
{
this->sendClear();
// sendClear 함수가 상속되는 것으로 가정한다.
}
};
- (2)
using
선언을 사용한다.
template<typename C>
class LoggingMsgSender : public MsgSender<C>
{
public:
using MegSender<C>::sendClear;
// 컴파일러에게 sendClear 함수가 기본 클래스에 있다고
// 가정하라고 알려준다.
void sendClearMsg()
{
sendClear();
}
};
- (3) 기본 클래스의 함수라는 점을 명시적으로 지정한다.
- 하지만 이런식으로 명시적 한정을 하면, 가상 함수 바인딩이 무시될 수 있다.
template<typename C>
class LoggingMsgSender : public MsgSender<C>
{
public:
void sendClearMsg()
{
MsgSender<C>::sendClear();
// sendClear 함수가 상속되는 것으로 가정한다.
}
};
Item 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 #
- 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어지고, 템플릿 매개변수에 종속되지 않은 템플릿 코드는 코드 비대화(code bloat) 의 원인이 된다.
비타입 템플릿 매개변수로 생기는 코드 비대화의 경우 #
template<typename T, std::size_t n>
// 비타입 템플릿 매개변수인 n이 있다.
class SquareMatrix
{
public:
void invert();
};
int main()
{
SquareMatrix<double, 5> sm1;
// n이 5인 SquareMatrix 인스턴스가 만들어진다.
sm1.invert();
SquareMatrix<double, 10> sm2;
// n이 10인 SquareMatrix 인스턴스가 만들어진다.
sm2.invert();
}
- 해결 방법
- 공통적인 부분을 기본 클래스로 만들고, 그것을 호출한다.
- 데이터는 클래스의 멤버로 만들어서 관리하면 되겠다.
// 독립적인 기본 클래스를 만들어 둔다.
template<typename T>
class SquareMatrixBase
{
private:
std::size_t size;
T * pData; // 데이터를 포인터로 가리킨다.
protected:
SquareMatrixBase(std::size_t n, T * pMem) : size(n), pData(pMem) {}
void setDataPtr(T * ptr) { pData = ptr; }
void invert(std::size_t matrixsize);
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>
{
private:
using SquareMatrixBase<T>::invert;
T data[n * n]; // 실제 데이터
public:
SquareMatrix : SquareMatrixBase<T>(n, data) {} // 기본 클래스에 데이터에 대한 포인터를 보낸다.
void invert() { this->invert(n); }
// 기본 클래스를 호출한다.
// 이렇게 하면 n이 달라도 오직 한 가지의 SquareMatrixBase 클래스를 공유하게 된다.
};
타입 매개변수로 생기는 코드 비대화의 경우 #
-
예를 들어,
vector<int>
와vector<long>
의 경우- 대부분의 플랫폼에서
int
,long
의 이진 표현구조가 동일하므로, 두 멤버 함수는 똑같게 나올 것이다.
- 대부분의 플랫폼에서
-
해결 방법
- 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들은 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.
- 예를 들어, 포인터 타입의 경우 타입미정(untyped) 포인터(
void
포인터)로 동작하는 버전을 호출하는 식으로 만들 수 있겠다. vector
,deque
,list
등이 이렇게 구현되어 있다.
Item 45: “호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! #
- 포인터는 암시적 변환(implicit conversion)을 지원한다.
- 이런 식의 타입 변환을 사용자 정의 스마트 포인터로 흉내 내려면 어떻게 해야할까?
class Top {};
class Middle : public Top {};
class Bottom : public Middle {};
int main()
{
// 이렇게 호환되는 모든 타입끼지 변환되도록 하고 싶다.
Top * pt1 = new Middle; // Middle* -> Top*
Top * pt2 = new Bottom; // Bottom* -> Top*
const Top * pct = pt1; // Top* -> const Top*
}
- 변환 가능한 모든 타입의 개수만큼 생성자를 만들기에는 무리다. 무제한으로 늘어날 것이다.
- 그렇다면 템플릿을 써서 일반화 복사 생성자(generalized copy constructor) 를 만들어보자.
- 하지만 어떤 타입이든 다 변환이 되면 안 되고 호환되는 타입들끼리만 변환이 되야 할 것이다.
- 따라서
shared_ptr
에서 쓰는 방법을 따라서get
멤버 함수를 가지고 포인터를 반환하게 만든 후, 생성자에서 이것을 이용해 변환에 제약을 줘볼 수 있겠다.
template<typename T>
class SmartPtr
{
private:
T * heldPtr;
public:
// 이렇게 같은 템플릿을 써서 인스턴스화되지만
// 타입이 다른 객체로부터 원하는 객체를 만들어주는 생성자를
// (SmartPtr<U>로 부터 SmartPtr<T>를 만들어내는)
// 일반화 복사 생성자라고 한다.
template<typename U>
SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) {}
// other의 포인터로 이 포인터를 초기화한다.
// 따라서 U*에서 T*로 암시적 변환이 가능할 때만 컴파일 에러가 나지 않는다.
T * get() const { return heldPtr; }
};
int main()
{
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct = pt1;
}
shared_ptr
은 어떻게 복사 생성 연산과 대입 연산을 하고 있을까?- 주의할 점은 일반화 복사 생성자와 일반화 복사 대입 연산자는 보통의 복사 생성자와 보통의 복사 대입 연산자가 아니므로,
- 보통의 복사 생성자와 보통의 복사 대입 연산자도 선언해 줘야 한다는 점이다.
- 안 그러면
shared_ptr
으로부터shared_ptr
로 생성될 때 컴파일러는 자동으로 보통의 복사 생성자를 하나 만들어버린다.
template<class T> class shared_ptr
{
public:
// 보통의 복사 생성자
shared_ptr(shared_ptr const & r);
// shared_ptr로부터 shared_ptr 생성자 호출이 가능하다.
template<class Y>
shared_ptr(shared_ptr<Y> const & r);
// 모든 기본 제공 포인터로부터 shared_ptr 생성자 호출이 가능하다.
template<class Y>
explicit shared_ptr(Y * p);
// 암시적 변환을 허용하지 않기 위해 explicit이 붙었다.
// weak_ptr로부터 shared_ptr 생성자 호출이 가능하다.
template<class Y>
explicit shared_ptr(weak_ptr<Y> const & r);
// auto_ptr로부터 shared_ptr 생성자 호출이 가능하다.
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r);
// auto_ptr은 const가 없다.
// 왜냐하면 복사연산으로 객체가 수정되면 복사된 쪽 하나만 유효해야 되기 때문이다.
// 보통의 복사 대입 연산자
shared_ptr& operator=(shared_ptr cosnt & r);
// shared_ptr에서 shared_ptr로 대입이 가능하다.
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const & r);
// auto_ptr에서 shared_ptr로 대입이 가능하다.
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r);
};
Item 46: 타입 변환이 바람직할 경우 비멤버 함수를 클래스 템플릿 안에 정의해 두자 #
- 모든 매개변수에 대해 암시적으로 타입 변환이 되도록 하기 위해서는?
- 비멤버 함수를 사용해야 했다 (Item 24)
- 하지만 템플릿인 경우에는 어떻게 될까?
template<typename T>
class Rational
{
public:
Rational(const T & numerator = 0, const T & denominator = 1);
const T numerator() const;
const T denominator() const;
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
// ...
}
int main()
{
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // 컴파일 에러!
// 컴파일러는 2의 T가 무엇인지 알 방도가 없다.
// 왜냐하면 템플릿 인자 추론과정에서는
// 암시적 타입 변환이 고려되지 않기 때문이다.
}
- 해결 방법
- 프렌드 함수를 이용해서 템플릿으로의 성격을 주지 않고 함수를 만들 수 있겠다.
Rational<T>
가 인스턴스화 될 때T
의 정확한 정보를 알 수 있으므로 프렌드 선언이 가능하다.- 프렌드 함수는 일반 함수이므로, 컴파일러는 암시적 변환 함수를 적용시킬 수 있게 된다.
template<typename T>
class Rational
{
public:
// 프렌드 함수
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
// 클래스 안에서 정의가 되었으므로 암시적으로 인라인 함수가 된다.
}
};
- 참고로 클래스 템플릿 내부에서는 템플릿의 이름을 그 템플릿 및 매개변수의 줄임말로 쓸 수 있다.
- 즉,
Rational<T>
안에서는Rational
이라고만 써도Rational<T>
로 먹힌다.
- 즉,
template<typename T>
class Rational
{
public:
// 위 코드와 같은 코드이다.
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
};
- 인라인 함수에 의한 영향력을 최소화하려면, 클래스 바깥에서 정의된 도우미 함수만 호출하는 식으로 바꿀 수도 있겠다.
template<typename T>
class Rational; // 전방 선언
// 도우미 함수 템플릿 선언
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);
template<typename T>
class Rational
{
public:
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
return doMultiply(lhs, rhs);
// 도우미 함수만 호출한다.
}
};
// 도우미 함수 템플릿 정의
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
Item 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 #
- 반복자의 종류
종류 | 설명 |
---|---|
입력 반복자 (input iterator) |
전진만 가능, 딱 한 번 읽기만 가능 |
출력 반복자 (output iterator) |
전진만 가능, 딱 한 번 쓰기만 가능 |
순방향 반복자 (forward iterator) |
전진만 가능, 여러번 읽기 쓰기 가능 |
양방향 반복자 (bidirectional iterator) |
순방향 반복자 + 뒤로 가기 가능 |
임의 접근 반복자 (random access iterator) |
양방향 반복자 + 임의의 거리만큼 앞뒤로 이동을 상수 시간 안에 가능 |
- C++ 표준 라이브러리에는 이 다섯 개의 반복자 범주를 식별하는 데 쓰이는 태그(tag) 구조체가 정의되어 있다.
struct input_iterator_tag();
struct output_iterator_tag();
struct forward_iterator_tag() : public input_iterator_tag;
struct bidirectional_iterator_tag() : public forward_iterator_tag;
struct random_access_iterator_tag() : public bidirectional_iterator_tag;
- STL은 컨테이너(container), 반복자(iterator), 알고리즘(algorithm) 외에 유틸리티(utility)라고 불리는 템플릿도 몇 개 들어있다.
- 유틸리티 안에 있는 템플릿 중 하나가
advance
라는 것인데, 이것은 지정된 반복자를 지정된 거리만큼 이동시킨다. - 반복자의 종류에 따라 구현할 수 있는 방법이 다르겠다.
- 왜냐하면 임의 접근 반복자는 상수 시간 안에 지정된 거리로 바로 이동 가능하지만, 나머지 반복자는 한 칸씩 이동해서 선형 시간 안에 지정된 거리로 이동이 가능하기에.
- 그렇다면 반복자 타입을 어떻게 판별해서 분기할 수 있을까?
- 유틸리티 안에 있는 템플릿 중 하나가
- 특성정보(traits)
- 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭하는 개념이다.
- 특성정보가 되기 위해서는 기본제공 타입과 사용자 정의 타입에서 모두 돌아가야 한다.
- 기본제공 타입에 대해서 쓸 수 있어야 한다는 말은, 그 안에 정보를 넣을 수 없으므로, 즉 특성정보는 그 타입 외부에 존재해야 한다는 결론이 나온다.
- 표준적인 방법은 템플릿과 템플릿의 특수화버전에 특성정보를 넣어서 제공하는 것이다.
- 관례적으로
struct
로 구현한다. 그리고 특성정보 클래스라고 부른다.
- 관례적으로
template < ... >
class deque
{
public:
// deque의 이터레이터는 임의 접근 반복자이다.
class iterator
{
public:
// (1) 사용하는 반복자 타입을 iterator_category라는 이름의 typedef로 한다.
typedef random_access_iterator_tag iterator_category;
}
};
// 특성정보 클래스
template<typename IterT>
struct iterator_traits
{
// (1) 그리고 똑같이 재생한다.
typedef typename IterT::iterator_category iterator_category;
}
// (2)포인터 타입을 따로 처리하기위해
// 부분 템플릿 특수화 버전을 제공한다.
template<typename IterT*>
struct iterator_traits<IterT*>
{
typedef random_access_iterator_tag iterator_category;
}
- 그렇다면
advance
의 코드는 어떻게 구현하면 될까?- 그 안에서 런타임에
typeid
를 사용해서 타입을 비교하는 건 컴파일 에러가 발생한다. (Item 48) - 그리고 애초에 컴파일 타임에 알 수 있는 타입 정보를 굳이 런타임에 비교해야 할까?
- 그 안에서 런타임에
- 오버로딩을 사용해서 타입에 대한 평가를 컴파일 도중에 실시하고 조건 처리를 할 수 있겠다.
// 임의 접근 반복자의 경우 사용하는 doAdvance 구현
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
iter += d;
}
// 양방향 반복자의 경우 사용하는 doAdvance 구현
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)
{
if (d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
// 입력 반복자의 경우 사용하는 doAdvance 구현
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
{
if (d < 0) { throw std::out_of_range("Negative distance"); }
while(d--) ++iter;
}
// 그러면 advance는 위의 오버로딩 함수들을 사용하면 된다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
// iter의 반복자 범위에 적합한 doAdvance의 오버로딩 버전을 호출한다.
}
Item 48: 템플릿 메타프로그래밍, 하지 않겠는가? #
- 템플릿 메타프로그래밍(template metaprogramming: TMP)
- 템플릿에 기반한 프로그래밍을 함으로써 컴파일 타임에 작업을 하는 것이다.
- 런타임이 아니라 컴파일 타임에 이루어지므로, 선행 에러 탐지와 높은 런타임 효율을 거머쥘 수 있다.
- Item 47의
advance
에서typeid
를 사용해서 타입을 런타임에 비교할 경우 컴파일 에러가 났다.- 특성정보를 활용해서 TMP를 사용하면 훨씬 효율적으로 구현할 수 있었다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iteratortag))
{
iter += d; // 컴파일 에러!
// iter가 std::list<int>::iterator 이라면
// list는 std::bidirectional_iterator_tag이므로
// 애초에 +=를 지원하지 않아서 컴파일 자체가 안 된다.
}
else
{
if (d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
}
int main()
{
std::list<int>::iterator iter;
advance(iter, 10);
}
- 또한 TMP을 사용한 루프는 재귀식 템플릿 인스턴스화(recursive template instantiation) 를 사용한다.
- 꼬리에 꼬리를 물고 만들어지는 템플릿 인스턴스화 버전마다 자체적으로
value
의 사본을 갖게 되고, 각각의value
에는 값이 담긴다.
- 꼬리에 꼬리를 물고 만들어지는 템플릿 인스턴스화 버전마다 자체적으로
template<unsigned n>
struct Factorial
{
enum { value = n * Factorial<n-1>::value };
};
template<0>
struct Factorial
{
enum { value = 1 };
};
int main()
{
std::cout << Factorial<5>::value;
// 120을 런타임 계산 없이 출력
}
- TMP가 C++ 프로그래밍에서 실력 발휘하는 예
- (1) 치수 단위(dimensional unit)의 정확성 확인
- 과학 기술 분야의 응용프로그램을 만들 때, 컴파일 도중에 치수 단위(예를 들면 질량, 거리, 시간 등)가 똑바로 쓰였는지 알 수 있다.
- (2) 행렬 연산의 최적화
- 행렬 연산의 경우 예를 들어, 네 개의 행렬을 곱하면 네 개의 임시행렬과 네 번의 루프가 순차적으로 이루어진다. TMP을 사용하면 컴파일 시간에 계산이 가능하겠다.
- (3) 맞춤식 디자인 패턴 구현의 생성
- 따로따로 정책(policy)들을 나타내는 템플릿을 만들어 놓고, 임의대로 조합해서 사용자 취향에 맞는 동작을 하는 패턴으로 구현할 수 있다.