C++ Template Metaprogramming의 소개

Tags:

자바 7은 클로져를 지원할 듯

위 글의 커멘트에서 이야기가 나왔길레 간단하게 써보는 C++ Template Metaprogramming에 대한 소개입니다. C++의 메타프로그래밍은, C++자체가 실행시간 타입에 대한 정보를 자바처럼 들고 다니는 것이 아니므로 컴파일타임에 모든 것이 이루어집니다. 이를 일단 기억하시고, 또다른 하나는 template에는 꼭 타입만 쓰는 것이 아니란 사실을 기억하면 모든 준비가 끝입니다. 이 글에서는 가장 유명한 예제인 피보나치 수열을 만들어보겠습니다.

먼저, C++에서는 다음과 같이 enum을 사용하면 컴파일 타임에 값을 알 수 있습니다.

strcut Foo
{
    enum { val = 1 };
};

...

cout << Foo::val << endl;
&#91;/code&#93;

C++의 메타프로그래밍에서는 recursion 부터 정의합니다.

&#91;code lang="cpp"&#93;
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;
}
&#91;/code&#93;

이처럼 C++의 메타프로그래밍은 컴파일시간에 생성된 코드를 이용하는 방식으로 이루어집니다. 이제 Curiously Recurring Template Pattern(CRTP)에 대한 좀더 실제적인 예를들겠습니다. 이 패턴의 기본형은 다음과 같습니다.

&#91;code lang="cpp"&#93;
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::cnt_++; }
Counter(const Counter&) { Counter::cnt_++; }
~Counter() { Counter::cnt_–; }
static int getCnt() { return Counter::cnt_; }
};

template
int Counter::cnt_ = 0;

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로 선언되지 않은 변수도 참조할 수 있다는 특징은 기존에 익명클래스로는 할 수 없었던 많은 부분을 해결해 줄 듯합니다. 문제는 아직까지는 제안된 문법이 너무 지저분해 보인다는 것....

Comments

17 responses to “C++ Template Metaprogramming의 소개”

  1. 大山 Avatar

    오랜만에 다시 봐도, C++는 역시나 복잡하군요. ^^;; 그래도 덕분에 템플릿을 대강이나마 이해했습니다. 설명 감사드립니다~

  2. 이원구 Avatar

    좋은 설명 잘 읽었습니다. ^^ 그런데 몇가지 맞지 않는 부분이 있는 것 같아 적어봅니다.

    먼저 template <> Fibo<0> 는 partitial specialization이 아니라 explicit specialization입니다. 의미상으로는 full specialization이라고 하는게 맞겠으나 표준에서 explicit specialization이라고 부르고 있죠.

    두번째로 Foo* f = new Foo;에서는 Foo의 복사 생성자가 불리지 않습니다. 복사 생성자는 다음과 같은 경우에 사용됩니다.

    Foo* f1 = new Foo; // 기본 생성자
    Foo f2(*f1); // 복사 생성자
    Foo f3 = *f1; // 복사 생성자 (사실 이 라인은 바로 위의 라인과 동일합니다.)
    

    마지막으로 Counter클래스를 정의할때 글에서와 같이 Counter로 써도 되지만 Counter만 써도 컴파일러가 template parameter를 가지는 Counter로 인식합니다. 굳이 T 타입이 아닌 다른 타입을 쓰는 경우가 아니라면 Counter라고 쓰는 것이 좀 더 코드를 깔끔하게 만들어 줍니다.

    template 
    class Counter
    {
        static int cnt_;
    public:
        Counter() { ++cnt_; }
        Counter(const Counter&) { ++cnt_; }
        ~Counter() { --cnt_; }
        static int getCnt() { return cnt_; }
    };
    
    template  Counter::cnt_;
    

    참고로 cnt_ 는 static storage이므로 프로그램 시작시에 자동으로 zero-initialize됩니다. 물론 = 0를 사용하여 명시적으로 초기화하는 것이 코드를 읽기 쉽게 만든다는 데는 동의하지만 왠지 필요없는 표현으로 군더더기같은 느낌이 들어서 개인적으로는 static storage 객체의 경우 초기화 문법을 잘 사용하지 않는답니다. :-)

  3. MKSeo Avatar
    MKSeo

    이원구님.. 역시 전문가의 손길이 닿으니까 모든게 더 잘 이해되네요. 정말 감사합니다. (__)

  4. CN Avatar

    storage 모델은 C언어나 C++이나 동일하나 보군요. the c++ programming langauge에 local variable같은 단어를 써서 실망했었는데 스트로스트롭도 기술적으로 옳은 단어만 쓰는 것이 아닌가 봅니다.

  5. 이원구 Avatar

    C 표준문서는 잘 보진 않는데 storage duration의 타입에 대해 찾아보니 용어만 조금 다르고 C++과 같군요.
    C는 static, automatic, allocated라고 부르고 C++에서는 static, automatic, dynamic이라고 부르네요.
    근데 local variable이라는 단어에 무슨 문제라도? ^^;

    (lang=”cpp” 문법을 써도 <> 는 &lt;>이라고 써야 제대로 보이는군요. html과 template은 어울릴 수 없는 것인지… -_-;)

  6. MKSeo Avatar
    MKSeo

    아 네… < 문제는 플러그인의 한계인 듯;;;

  7. CN Avatar

    상당수의 C 언어 문법에 관심이 있는 사람들은 local variable이란 단어가 가진 불명확함 때문에 사용하지 않습니다. storage duration이나 scope등의 여러 속성이 연관될 때 local variable과 같은 단어는 아무것도 설명하지 못하고 혼란만 주기 때문입니다. 저는 C++에선 템플릿에만 관심이 있어서 문법을 제대로 모른 채로 스트로스트롭의 말로 짐작만 하고 있었군요. 반성해야겠습니다.

  8. MKSeo Avatar
    MKSeo

    local variable 이 왜 문제인가요? 저는 맨 처음에 공부할 때 global, local variable 이라고 공부했었거든요. 지금도 그런줄로만 알고 있어요..

  9. CN Avatar

    흔히 많은 서적이 static storage duration을 전역변수로 automatic storage duration을 지역변수로 표현하면서 block scope역시 동일한 것으로 표현하고 있습니다. 하지만 block scope를 가지면서 static storage duration을 가지는 등의 상황은 매우 흔합니다. storage duration과 scope, linkage는 개별적으로 동작하는 직교적인 것입니다. 그래서 공식적으로 C언어에서 local variable이나 global variable과 같은 용어는 사용하지 않습니다. 여기에 대한 내용들은 뉴스그룹에 전웅씨의 글을 검색해보시면 더 도움이 되실 것 같습니다. 전 이런 부분들이 고려되지 못한 점이 the c++ programmin language의 최악의 부분이라고 생각합니다. 왜 사람들이 그 책을 바이블이라고 신봉하는지 이해하기 힘드네요.

  10. CN Avatar

    처음 언급된 block scope를 scope로 변경해야 합니다. 글의 오류가 있네요 (…)

  11. 이원구 Avatar

    C99 표준 문서에서는 block scope라는 이름을 사용하고 있으나 C++98 표준 문서에서는 local scope라는 이름을 사용하고 있습니다. 또한 local scope에 선언된 변수를 local variable이라고 부르고 있고요.

    만약 어떤 변수를 나타내기 위해 항상 storage duration, scope, linkage들을 모두 고려해야 한다면 글쓸 때나 말할 때 얼마나 힘들어질까요? 우리가 흔히 부르는 local variable이나 global variable이라는 것을 표현해야 할때마다 local scope variable with automatic storage duration 혹은 global namespace scope variable with static storage duration, external and C language linkage 라고 해야 한다고 하면 동의하실런지 모르겠습니다.

    따라서 제 생각엔 글의 문맥상에 무리가 없다면 (예를 들어 storage duration에 대해 설명하고 있는 글등이 아니라면) local variable이나 global variable이라는 용어의 사용에는 전혀 문제가 없다고 생각되며 더군다나 이런 용어의 사용이 그 책의 가치를 떨어뜨린다고 생각하진 않습니다. 요샌 워낙 좋은 C++ 책들이 많이 나와 그 책의 실용성이 좀 떨어지긴 했지만 D&E 책과 더불어 C++의 제작 배경을 엿볼 수 있는 무시할 수 없는 위치를 차지하고 있는 책이라고 생각합니다.

  12. CN Avatar

    모든 수식어를 한번에 말할 필요가 없습니다. scope나 storage duration 둘 중 하나를 말하면 대부분의 경우에 대화가 충분히 가능합니다. 그냥 local scope다. automatic storage duration이다. 정도로 말하면 되는 문제라고 생각합니다. local scope만 local variable로 한다고 한정되어져 있다면 기술적으로 정확한 표현은 가능하겠습니다만 여전히 사용자에게는 혼란의 여지가 있다고 생각합니다.

  13. MKSeo Avatar
    MKSeo

    @CN: 설명 감사합니다. 단순히 local, global 이라는 이름만으로 변수가 얼마나 존재하는가, 그리고 어떤 변수를 접근할 수 있는가를 한번에 표현하기 어렵단 말씀이군요..

    이해는 합니다만, 지역/전역 변수라는 이름으로 인해 많은 혼란이 초래될 거 같지는 않습니다.

    사실상 독자는 설명을 그렇게 열심히 읽지 않고, 오히려 코드에서 코드로 점프하며 책을 본다고 생각하기 때문에요.. 더구나 사실 이해를 해칠리는 없는게, 예를들어서 코드의 한 부분에서 어느 변수를 참조 가능한가의 문제가 C의 경우엔 한가지 규칙 – 안에있는 블럭은 밖에 있는 블럭을 볼 수 있다 – 만으로 항상 돌아가기때문에 혼란까지는 발생할 거 같지는 않다고 생각됩니다. 더구나 변수의 lifetime도 굉장히 명확하죠. C에서 복잡한건 사실 포인터겠죠;;

    물론 C를 serious 하게 다루어야할 시점이 되면 정확한 의미의 구분의 필요가 있겠습니다.

    그런데 han.* 뉴스그룹은 안가보다가 말씀하셔서 한번 가보았는데 사람들이 참 많아보이더군요. 예전엔 하이텔 소동회 같은데서 C언어 질문을 주고 받았는데 이젠 그 사람들이 뉴스그룹으로 간건가, 하는 생각이 들었습니다..

  14. 이원구 Avatar

    C 표준은 자세히 보질 않아 건너뛰고 C++ 관점에서 보자면 local variable은 local scope variable을, global variable은 global namespace scope variable을 말한다고 생각하면 혼란이 없을 것 같습니다.

    그런데… 3:21am… 4:10am… 두분은 잠은 언제? @_@

  15. MKSeo Avatar
    MKSeo

    오후형인간이요;;;;;;; ㅎㅎ 그래서 제가 포스팅에서 시간 항목을 뺀건데, 커멘트도 빼야겠군요 -_-;

  16. Sam Kong Avatar
    Sam Kong

    제 생각에 local/global이라는 말 자체가 상대적인 것 같습니다.
    예를 들어서 월드 뉴스에 대해 국내 뉴스는 글로벌과 로컬의 관계죠.
    그렇지만 미국 내에서 방영되는 뉴스에서 미국의 국내 뉴스와 어떤 한 주(state)의 뉴스를 나눈다면 이번에는 국내 뉴스가 글로벌이고 주의 뉴스가 로컬이 되겠죠.
    그런데 state가 local이라 하더라도 그리 작은 범위는 아니거든요.
    보통 미국에서 로컬 도로라고 하면 고속도로에서 빠져나온 작은 도로를 의미하곤 하죠.
    이처럼 로컬과 글로벌은 상대적이라는 생각이 듭니다.
    그래서 어떤 절대적인 의미를 가지고 있다기보다는 context 속에서 파악해야 할 것 같아요.

    그러므로 제대로 사용하려면 대략 이런 식이겠죠.
    Variable x is local to function foo.
    Variable y is global among function a and function b.

    각 언어마다 scope의 종류가 틀리기 때문에 일일이 정확한 용어를 사용한다는 게 쉬운 일은 아닐 것 같습니다.
    예를 들어, C#처럼 순수객체지향언어에서 글로벌 변수란 의미가 없거든요.
    그렇지만 일상적으로는 이렇게 말합니다.
    “로컬 변수 대신 글로벌로 변수 하나 만들어서 해봐~”
    이럴 경우 글로벌 변수란 보통 멤버변수를 의미하지요.
    메소드들 간에 공유할 수 있으니까요.
    그래서 약간은 fuzzy하지만 로컬/글로벌이란 용어가 일반적으로 사용되는 것 같아요.
    물론 정확성을 요구하는 문서에서야 명확한 용어를 사용해야겠지만요.

  17. CN Avatar

    C언어에 대한 scope에 대한 글을 정리해서 적어보았습니다.
    http://blog.cnrocks.net/article-192/scope-in-c

    이 글에 트랙백을 보내는게 맞는 건지 틀린 건지 알 수 없어서 그냥 코멘트만 걸어봅니다. :-)

Leave a Reply

Your email address will not be published. Required fields are marked *