[C++] Template Summary – 1/3

이 문서는 제가 개인적으로 템플릿을 이용해 코딩할때 참고할 만한 자료를 기재해 놓은 것입니다. 템플릿에 대한 전반적인 내용이 아닌 정리라는 점을 염두해 주시길 바랍니다.

먼저 함수 템플릿의 예이다. Type에 상관 없이 두개의 인자를 받아 이 중 최대값을 반환하는 함수 템플릿의 정의.

tpl_decl.h 라는 파일안에 다음과 같은 코드를 정의 한다.

template T max_Tpl(T a, T b)
{
    return (a>b)?a:b;
}

typename 대신에 class를 써도 상관없지만, Type이라는 분명한 의미를 제공한다는 점에서 개인적으로 typename을 선호한다. 이 max_Tpl이라는 템플릿 함수는 헤더파일만…. 존재해야한다는 점이… 늘 걸리긴 했지만, 이제 이런 걸림은 그냥 포기하고 받아 들이기로 했다. 구현을 별도의 cpp 파일로 분리하기 위해 export 라는 예약어가 제공된다고 하나.. Visual C++ 2008에서도 예약어로만 선언되어 있을 뿐 아직 구현되어 지원하지 않는다.

위의 max_Tpl이라는 함수의 활용은 아래와 같다.

#include "tpl_decl.h"

int _tmain(int argc, _TCHAR* argv[])
{
    double max = max_Tpl(100.0, 200.0);

    ...

위의 코드에서 인자가 실수라는 점이 명확하므로 max_Tpl을 간단히 max_Tpl이라고 해도 되나, 템플릿 함수라는 점을 명확하게 하기 위해 을 붙이는 것을 개인적으로 선호한다.

정의한 max_Tpl이라는 함수에 대해 모든 Type에 대해 작동한다고 보장할 수는 없다. 그 하나의 경우로 문자열 타입에 대한 경우인데, 이 경우 문자열 타입에 대해서는 좀더 특별하게 그 구현을 제공해야한다. 그 구현은 마찬가지로 tpl_decl.h 파일에 아래의 코드를 추가한다.

template<> const char* min_Tpl<>(const char* a, const char *b)
{
    return strcmp(a, b)
}

이를 함수 템플릿 특수화라 한다.

정의되지 않은 클래스 맴버 함수 호출하기

당연이 인스턴스로 생성된 클래스의 맴버함수를 호출하기 위해서는 해당 클래스를 정의하고 있는 헤더 파일을 포함해야 하지만, 헤더 파일을 포함하지 않고 클래스의 맴버함수를 호출해야할 경우가 있다. 즉 정의되지 않는 클래스 인스턴스의 맴버 함수를 호출해야한다.

어떻게 할 수 있을까? 해답은 클래스 맴버 함수 포인터를 사용하는 방법이다.

먼저 사용하는 코드부분에서 호출해야하는 클래스를 선언하고 그 클래스에서 사용해야할 맴버 함수의 포인터를 선언한다.

C사용하는클래스의 헤더파일에…

class C사용될클래스;
typedef void (C사용될클래스::*맴버함수)(void);

그리고 C사용하는클래스의 맴버변수로써 C사용될클래스의 인스턴스와 맴버함수에 대한 변수를 추가한다.

private:
    맴버함수 funcPtr;
    C사용될클래스* p사용될클래스인스턴스;

public:
    void Set(맴버함수 funcPtr, C사용될클래스* pInstance) {
        this.funcPtr = funcPtr;
        p사용될클래스인스턴스 = pInstance;
}

이제 실제로 C사용될클래스의 멤버함수를 사용하는 방법은 아래와 같다.

((*p사용될클래스인스턴스).*funcPtr)();

여기서 주목할 것은 앞에서도 언급했지만 어디서도 C사용될클래스에 대한 헤더파일을 포함하지 않았다는 점이다. 즉, C사용하는클래스는 단지 C사용할클래스를 모르며, 단지 C사용할클래스에서 꼭 필요한 것만 알고 있다는 것이다.

꼼꼼한 독자라면 직감했겠지만, C사용하는클래스의 funcPtr과 p사용될클래스인스턴스는 어떤식으로 값을 설정해야하는가라는 문제가 생긴다.

이것은 C사용될클래스에서 C사용할클래스의 Set함수를 호출해주면 된다. 즉 C사용될클래스의 정의부 어디에선가 다음과 같은 형식의 코드(한가지 예일뿐..)를 호출한다.

p사용하는클래스 = new C사용하는클래스();
p사용하는클래스->Set(&C사용될클래스::Function, this);

사실, 이런 기술(정확히, 솔직이 말한다면 편법)이 필요한 이유는 두개의 클래스가 서로 상호참조를 하는 경우이다. 이런 경우는 서로의 헤더파일을 포함해서 쉽게 구현할 수 있는 경우가 대부분이지만, 만약…. 이 두개의 클래스가 서로 완전이 다른 개념의 개발 프로젝트인 경우에는 예외인데, 예를 들어서 하나의 클래스는 ATL 프로젝트에 있고, 다른 하나는 일반적인 Generic C/C++ 프로젝트인 경우, 단순이 헤더파일을 포함할 경우 서로의 개발환경을 이해하지 못하므로 엄청나게 많은 에러 메세지를 쏟아 낼 것이고, 머리속이 하얗게  되는 것을 경험할 것이다. 이 편법을 사용하지 않는 것이 제대로 개발하고 있다는 증거인지라, 사용하지 않기를 바라지만 꼭 필요한 경우라면 요긴할 것으로 판단되어 글로 정리하여 남긴다.

STL의 map 컨테이너 테스트

STL의 컨테이너 중에서 Hash 검색 알고리즘을 사용하고 있는 map 컨테이너를 테스트 해보았다. Hash 검색 알고리즘은 검색 항목의 개수에 상관없이 검색시간은 항상 일정하다라고 되어 있다. 바로 이것을 테스트해 보고자 했다.

N의 개수는 천만(10,000,000)개를 생성했으며 hash key는 unsigned long 타입으로 했다. 0, 2000000, 5000000, 7000000, 9999999 hash key 값을 검색하여 그곳에 -1을 넣었다. 과연 상수 시간이 나올 것인가? 나올 경우 시간은 얼마나 걸릴것인가…

아래는 그 결과다..


소요 시간은 분명 상수이긴 한데…. 0초다. 분명 0초는 아니겠지만 거의 0초가 걸린다. 사실 믿기지가 않는다. 아래는 테스트한 코드인데… 잘못된 부분이라도 있는건가? 옳바르다면 stl의 map 컨테이너는 정말 물건이다…

<#include 
#include <sys/timeb.h>
#include 

using namespace std;

void PerformanceTest(bool bStart, const char *szTitle="") {
    static timeb start;
    static timeb stop;

    if(bStart) {
        ftime(&start);
    } else {
        ftime(&stop);

        double gab;
        gab = 1.0e3 * difftime(stop.time, start.time);
        gab+=(double)(stop.millitm - start.millitm);

        double ReturnValue = gab/1.0e3;
        printf( "%s: %.10lf sec required.\n", szTitle, ReturnValue);
    }
}

map container_;

int _tmain(int argc, _TCHAR* argv[])
{
    PerformanceTest(true);
    for(unsigned long i=0; i<10000000; i++) {
        container_[i] = i;
    }
    PerformanceTest(false, "generating 10000000 data");

    printf("Now container has %ld items.\n\n", container_.size());

    for(size_t i=0; i<5; i++) {
        PerformanceTest(true);
        container_[0] = -1;
        PerformanceTest(false, "finding index 0:");

        PerformanceTest(true);
        container_[2000000] = -1;
        PerformanceTest(false, "finding index 2000000:");

        PerformanceTest(true);
        container_[5000000] = -1;
        PerformanceTest(false, "finding index 5000000:");

        PerformanceTest(true);
        container_[7000000] = -1;
        PerformanceTest(false, "finding index 7000000:");

        PerformanceTest(true);
        container_[10000000-1] = -1;
        PerformanceTest(false, "finding index 9999999:");
    }

    printf("\n[%ld %ld %ld %ld %ld]\n", 
        container_[0], 
        container_[2000000], 
        container_[5000000], 
        container_[7000000], 
        container_[9999999]
    );

    return 0;
}

Template을 이용한 Observer 패턴 – 2단계

이제 여기서부터는 개선의 여지를 찾아보겠습니다. 어떻게 하면 EventSrc 클래스가 담당하고 있는 Fire 함수를 Observed에게 전가시킬 수 있을까요? 그래서 EventSrc 클래스를 없앨 수 있겠는가? 그 해답은 함수자(Functor)에 있습니다.

먼저 연습겸해서 간단하게 함수자를 사용해서 1단계의 코드를 수정해 보도록 하겠습니다. 아래는 새롭게 추가한 함수자에 대한 클래스입니다.

class Functor {
private:
    int arg_;

public:
    Functor(int arg) {
        arg_ = arg;
    }

    void operator()(Observable *pOb) const {
        pOb->OnEvent(arg_);
    }
};

이 함수자를 사용하면, EventSrc 클래스의 Fire 함수가 아래처럼 무척 깔끔하게 작성됩니다.

void Fire(int a)
{
    std::for_each(m_listObserver.begin(), m_listObserver.end(), Functor(a));
}

하지만 이런 방식은 Fire 함수를 깔끔하게 만들어준다는 사소한 장점은 있지만, 다른 어플리케이션에 적용할때 매번 함수자를 각각의 경우에 맞게 새롭게 코딩해줘야 하는 수고로움이 더욱 많습니다. 또한 여전이 Observed에서는 자신이 관리하고 있는 Observer 객체들이 호출해야할 함수가 무엇인지를 알 길이 없습니다.

하지만 여기서 곰곰이 생각해 보면 이 함수자를 Observed 클래스의 inner class로 정의해보는 것에 대한 아이디어가 떠오릅니다. 게다가 이 함수자 역시 template로 정의해서 Observed 클래스가 관리하고 있는 Observer 객체들이 호출해야할 함수를 타입으로 받아 버린다면 Observed가 알아야할 Observer의 정보를 모두 Observed에게 넘겨줄 수 있게되어, Fire의 책임을 Observed가 맡을 수 있게 됩니다. 그러면 더 이상 EventSrc는 필요치 않게 되고요. 이러한 아이디어에 착안해서 새롭게 구현된 Observed 클래스는 다음과 같습니다.

template 
class Observed
{
public:
    class Firer1 {
    public:
        explicit Firer1(T_result (T::*pMember)(T_arg1), T_arg1 arg1) {
            m_pMemFunc = pMember;
            m_arg1 = arg1;
        }

        T_result operator()(T* pClass) const {
            return ((pClass->*m_pMemFunc)(m_arg1));
        }

    private:
        T_result (T::*m_pMemFunc)(T_arg1);
        T_arg1 m_arg1;
    };

public: // type define
    typedef std::list typeObservers;

protected: // private attribute
    typeObservers m_listObserver;
    T_result (T::*m_pMemFunc)(T_arg1);
}

public: // ctr & dtr
    Observed(T_result (T::*pMember)(T_arg1)) {
        m_pMemFunc = pMember;
    }
    virtual ~Observed() {}

public: // operator
    void RegisterObserver(T *pOb)
    {
        m_listObserver.push_back( pOb );
    }

    void UnRegisterObserver(T *pOb)
    {
        m_listObserver.remove(pOb);
    }

public: // Fire!
    void Fire(T_arg1 arg1)
    {
        std::for_each(m_listObserver.begin(), m_listObserver.end(), 
            Firer1(m_pMemFunc, arg1));
    }
};

굵은 청색 폰트로 된 것이 수정되거나 새롭게 추가된 것입니다. Observed가 관리하고 있는 Observer의 호출함수의 반환값과 인자, 그리고 호출함수에 대한 타입을 template 인자인 T_result와 T_arg1으로 받고, 호출함수는 생성자에서 받아 맴버변수인 m_pMemFunc에 저장하도록 하였습니다.

이렇게 새롭게 구현된 Observed를 이용해서 Client 측에서 사용하는 코드는 다음과 같습니다.

int _tmain(int argc, _TCHAR* argv[])
{
    Observed *pES = 
        new Observed(&Observable::OnEvent);

    Observable_A *pOA = new Observable_A();
    Observable_B *pOB = new Observable_B();

    pES->RegisterObserver(pOA);
    pES->RegisterObserver(pOB);

    pES->Fire(99);

    delete pOA;
    delete pOB;
    delete pES;

    return 0;
}

이제 마지막으로 하나 더 짚고 정리를 하겠습니다. 여기서 한가지 큰 문제가 있는데 그것은 Observed가 관리하고 있는 Observer의 호출해야할 함수의 인자 개수에 관한 문제입니다. 지금가지의 경우는 단지 하나의 인자만을 받는 경우지만 두개 이상의 인자를 받는 경우에 대한 처리도 필요하지요. 하지만 이것 역시 그리 어렵지 않게 해결할 수 있습니다. Observed의 inner class인 함수자의 이름이 Firer1인 이유는 하나의 인자를 받는 함수자이기 때문에 Firer 뒤에 1을 붙인 것입니다. 그렇다면 이제 2개의 인자를 받는 Firer2 함수자를 정의하고 인자를 2개를 받는 Fire 맴버함수를 하나더 만들어 두면 됩니다.

이제 Client는 1단계처럼 관리하고자 하는 Observer에 대해 EventSrc와 Observable 클래스 모두를 정의할 필요가 없이, Observable 클래스 단하나만 신경 쓰면 되게 되었습니다. 그리고 Observer에 대한 모든 관리에 대한 책임을 오직 Observed가 맡게 되어 SRP를 지키게 되었습니다.