Skip to main content

[Effective C++] Chapter 7. 템플릿과 일반화 프로그래밍

이펙티브 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의 두 가지 의미를 제대로 파악하자 #

템플릿 매개변수를 선언할 때 #

  • 템플릿 매개변수를 선언할 때 classtypename은 완전히 똑같은 의미를 가진다.
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)들을 나타내는 템플릿을 만들어 놓고, 임의대로 조합해서 사용자 취향에 맞는 동작을 하는 패턴으로 구현할 수 있다.