위 글의 커멘트에서 이야기가 나왔길레 간단하게 써보는 C++ Template Metaprogramming에 대한 소개입니다. C++의 메타프로그래밍은, C++자체가 실행시간 타입에 대한 정보를 자바처럼 들고 다니는 것이 아니므로 컴파일타임에 모든 것이 이루어집니다. 이를 일단 기억하시고, 또다른 하나는 template에는 꼭 타입만 쓰는 것이 아니란 사실을 기억하면 모든 준비가 끝입니다. 이 글에서는 가장 유명한 예제인 피보나치 수열을 만들어보겠습니다.
먼저, C++에서는 다음과 같이 enum을 사용하면 컴파일 타임에 값을 알 수 있습니다.
strcut Foo { enum { val = 1 }; }; ... cout << Foo::val << endl; [/code] C++의 메타프로그래밍에서는 recursion 부터 정의합니다. [code lang="cpp"] template<int i> struct Fibo { enum { val = Fibo<i-1>::val + Fibo<i-2>::val }; };
Fibo<i-1>::val, Fibo<i-2>::val을 항상 알 수 있죠. 왜냐면 그 값이 컴파일 타임에 이미 결정이 나니까요. 다음, recursion의 base를 정의합니다.
template<> struct Fibo<0> { enum { val = 1 }; };
여기서 template<> 은 template<i> 라고 쓴 기본형에서 쓰던 int i가 필요 없음을 의미합니다. 다음, Fibo<0>은 우리가 지금 int i 가 0을 가질 때의 경우를 다룬다는 의미입니다. 이런 것을 템플릿의 부분 특화(partial specialization)이라고 하죠. 결국 우리는 Fib<0>::val을 정의했습니다. 마찬가지로 Fib<1>도 정의할 수 있겠죠. 이렇게 해주고 나면 이제 Fib<i-1>::val + Fib<i-2>::val을 계산할 모든 준비가 끝난 것입니다.
#include <iostream> using namespace std; template<int i> struct Fibo { enum { val = Fibo<i-1>::val + Fibo<i-2>::val }; }; template<> struct Fibo<0> { enum { val = 1 }; }; template<> struct Fibo<1> { enum { val = 1 }; }; int main() { cout << Fibo<2>::val << endl; return EXIT_SUCCESS; } [/code] 이처럼 C++의 메타프로그래밍은 컴파일시간에 생성된 코드를 이용하는 방식으로 이루어집니다. 이제 Curiously Recurring Template Pattern(CRTP)에 대한 좀더 실제적인 예를들겠습니다. 이 패턴의 기본형은 다음과 같습니다. [code lang="cpp"] template<typename T> class Base { }; class Derived: public Base<Derived> { }
이제 CRTP란 이름에 대한 감이 오실것입니다. 자기가 자신을 상속받는게 이상하단거죠… 여기서는 역시나 유명한 예로 인스턴스 개수 세기를 해보겠습니다. 이를 위해, 먼저 다음과 같은 클래스를 정의합니다.
template<typename T> class Counter { static int cnt_; public: Counter() { Counter<T>::cnt_++; } Counter(const Counter<T>&) { Counter<T>::cnt_++; } ~Counter() { Counter<T>::cnt_--; } static int getCnt() { return Counter<T>::cnt_; } }; template<typename T> int Counter<T>::cnt_ = 0;
여기서는 Counter라는 클래스를 정의했고, 카운터는 cnt_ 라는 변수를 씁니다. cnt_ 는 2군데서 증가가 가능한데 첫번째는 잘 아시는 생성자이고, 두번째는 복사 생성자입니다. 이 복사 생성자는,
Foo *f = new Foo();
처럼 생성과 동시에 그 내용을 다른 곳에 넣을 때 호출됩니다. 반면 Counter()는
Foo f;
와 같이 그냥 객체를 생성할 때 불리죠. 마지막에 클래스 밖에 위치한
template<typename T> int Counter<T>::cnt_ = 0;
는 원래 static 변수는 클래스 외부에 선언해야한다는 C++의 규칙에 따라서, 외부에 선언된 것입니다. 위의 class Counter { … } 는 Counter에 대한 정의(definition)이므로 변수가 실제로 선언(declaration)되지는 않습니다. 그래서 static 변수는 이처럼 외부에 실제로 선언해주어야합니다.
이제 이 Counter는 다음과 같이 사용됩니다.
class Foo:public Counter<Foo> { }; class Bar:public Counter<Bar> { };
그러면 C++의 template은 2개의 클래스를 먼저 생성합니다. 첫번째는 Counter<Foo>이고, 두번째는 Counter<Bar> 입니다. 이 두개 클래스의 정의는 Counter<T>을 그대로 복사해서 채워넣게 되죠. 즉, 실제로 코드가 2개 클래스를 위해 2개가 나오는 것입니다. 그래서 Foo와 Bar는 cnt_를 서로 따로 가지게됩니다. 이것이 CRTP의 핵심적인 내용입니다. 실제 예는 다음과 같습니다.
#include
using namespace std;
template
class Counter
{
static int cnt_;
public:
Counter() { Counter
Counter(const Counter
~Counter() { Counter
static int getCnt() { return Counter
};
template
int Counter
class Foo:public Counter
class Bar:public Counter
int main()
{
Foo *f = new Foo();
Foo *f2 = new Foo();
Bar *b = new Bar();
cout << "f: " << Foo::getCnt() << endl; cout << "b: " << Bar::getCnt() << endl; delete f; cout << "f: " << Foo::getCnt() << endl; cout << "b: " << Bar::getCnt() << endl; delete f2; cout << "f: " << Foo::getCnt() << endl; cout << "b: " << Bar::getCnt() << endl; delete b; cout << "f: " << Foo::getCnt() << endl; cout << "b: " << Bar::getCnt() << endl; return EXIT_SUCCESS; } [/code] 실행결과는 다음과 같습니다. [code lang="cpp"] mkseo@mkseo:~/tmp$ ./a.out f: 2 b: 1 f: 1 b: 1 f: 0 b: 1 f: 0 b: 0 mkseo@mkseo:~/tmp$ [/code] p.s. 저는 자바의 closure 도입에 적극 찬성하며, 제가 찬성하든 말든 closure가 도입될 것으로 보입니다. 제임스 고슬링이 스펙에 참여하고 있는 듯;; 특히 final로 선언되지 않은 변수도 참조할 수 있다는 특징은 기존에 익명클래스로는 할 수 없었던 많은 부분을 해결해 줄 듯합니다. 문제는 아직까지는 제안된 문법이 너무 지저분해 보인다는 것....