'Fast C++'에 해당되는 글 1건

  1. 2011.05.31 Fast C++ delegate
2011.05.31 10:33
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=7287&ref=7287

Fast C++ delegate

최재욱(zzzz.ooo 'at' gmail.com)

난이도 : 중급 이상(?)

 

Don Cluston 은 멤버 함수 포인터에 관하여 코드 프로젝트에 올린 자신의 글 "Member Function Pointers and the Fastest Possible C++ Delegates" 에서 멤버 함수 포인터의 특징과 동작 원리에 대해서 플랫폼 별로 매우 자세히 설명을 합니다. 간단히 요약하자면 멤버 함수 포인터는 데이터 포인터와 다르기 때문에 void * 에 대입되어 질 수 없고 따라서 멤버 함수 포인터를 저장하기 위해서는 특별한 처리를 통하여만 합니다. Don Cluston 은 자신이 분석한 플랫폼별 멤버 함수 포인터의 동작 원리에 따라서 이를 저장하는 방법을 구현하여서 가장 빠른 C++ delegate 라고 소개하였습니다. 실제로 Don Cluston 의 fastest delegate는 멤버 함수 포인터를 두 줄의 기계어 코드로 변환하기 때문에 가장 빠른 C++ delegate 라는데에 의심할 여지가 없습니다만 이를 구현하는데 있어서 reinterpret_cast<> 를 사용하여 컴파일러를 속이는 비 표준적인 방법을 사용합니다. 물론 멤버 함수 포인터에 대한 해박한 지식을 바탕으로 현재 널리 사용되어 지고 있는 대부분의 상용 컴파일러에서 돌아가는, 어떤 의미에서 호환성이 극대화된 구현이지만 Don Cluston 자신도 이 방법을 "horrible hack"이라고 불렀듯이 표준적인 방법은 아니라고 할 수 있습니다. (그럼에도 불구하고 이미 많은 오픈 소스 프로젝트에서 Don Cluston의 delegate가 사용되어지고 있다고 합니다.)

 

얼마 지나지 않아서 코드 프로젝트에 Don Cluston의 구현에 응답이라도 하듯이 Sergey Ryazanov 는 "The Impossibly Fast C++ Delegates" 라는 글을 통해서 C++ 표준에 완벽하게 부합하는 fast delegate의 구현을 선보입니다. Sergey Ryazanov의 구현은 템플릿화된 static 멤버 함수를 이용하면서 이 템플릿 인자로 멤버 함수 포인터를 사용하는 방법을 사용합니다. 그러나 Sergey Ryananov 의 방법은 진정 C++ 표준에 부합하면서도 실재로는 그다지 호환성이 없는 구현입니다. 템플릿화된 static 멤버 함수와 템플릿 인자로 멤버 함수 포인터를 사용하는 핵심적인 두 가지 기능이 C++ 표준이지만 최근에 발표된 새로운 컴파일러가 아닌 VC6 같은 오래된 컴파일러에서 지원이 되지 않기 때문입니다.

 

이제 제 글에서는 C++ 표준에 완벽하게 부합하면서 VC6 등의 오래된 컴파일러에서도 문제없이 돌아가는 fast delegate의 구현을 소개하고자 합니다.

 

C# 에서 더욱 잘 알려진 delegate란, 호출이 가능한 개체 (callable entity: 함수 호출 연산자가 있는 세 가지 종류의 개체 - 함수(free function), 멤버 함수(member function), 함수 객체(functor, function object)를 내포하는 함수 객체(functor)라고 생각할 수 있습니다. boost::function, boost::bind 그리고 boost::mem_fn 를 적절히 조합하면 C++ delegate 로 사용이 가능합니다. 널리 알려진 사실이지만 C++ 표준 위원회에서는 boost::function 들을 차기 표준 채택하기로 이미 결정 했다고 합니다.

 

fast delegate란, boost::function들과는 달리 내부적으로 힙 메모리 상에 호출이 가능한 개체를 저장하기 위한 메모리 공간을 할당하는 대신에 스택 메모리 상에 이러한 개체를 저장하는 방식을 채택하여 보다 빠른 delegate의 생성/복사/해제를 수행할 수 있도록 구현한 delegate 를 의미합니다. 부하가 크게 걸리는 힙 메모리 영역을 사용하는 대신에 상대적으로 빠르고 가벼운 스택 메모리를 사용하기 때문에 생기는 이러한 속도의 이득은 이벤트 드리븐 시뮬레이터 또는 대량의 delegate 를 STL 컨테이너에 저장하는 등등의 경우에 일반 delegate를 사용하는 것보다 커다란 효과를 얻을 수가 있습니다. std::map 과 연동하여 동적 메세지 맵을 가지는 윈도 프레임워크를 제작하는 것도 괜찮은 아이디어가 아닐까 생각해 봅니다.

 

이미 코드 프로젝트나 다른 프로그래밍 사이트들에는 다양한 fast delegate의 구현들이 있습니다. 그럼에도 불구하고 제가 또 다른 fast delegate의 구현을 제시해서 도대체 뭐가 어떻게 다르다는 걸까요? 솔직히 말해서 저의 fast delegate에 구현된 기능들은 대부분 다른 delegate의 구현에서 볼 수 있는 기능들 뿐이고 특출난 기능은 없다고 보여집니다...

  1. fast delegate (대부분의 경우에 힙 메모리 대신 스택 메모리 사용)
  2. 세 가지 호출이 가능한 개체 사용 가능 (free function, member function, functor)
  3. C++ 표준에 완벽히 부합하면서 매우 호환성 있음 (현재 VC6, VC71, DEV-C++ 4.9.9.2 (Mingw/gcc 3.4.2) 에서 동작)
  4. STL 컨테이너에서 사용 가능. copy-construtible & assignable
  5. delegate 간에 동일/대/소 비교가 가능함 (STL set 컨테이너 등에서도 사용이 가능)
  6. cv 지시자 (const) 완벽하게 적용
  7. 비표준인 플랫폼별 호출 규약 (__stdcall, __fastcall, __cdecl, pascal) 지원
  8. Preferred Syntax & Portable Syntax 모두 지원하면서 두 가지 문법을 혼합해서 사용도 가능
  9. 유연한 형검사
  10. 디버그 지원 - 컴파일 시 정적 경고
  11. 멤버 함수에 바인딩되는 객체의 포인터를 저장하거나 객체를 복사하여 별도의 복사본을 내부적으로 유지하고 복사본에 멤버 함수를 호출하는 것이 가능함
  12. 멤버 함수에 바인딩되는 객체 대신에 객체를 가리키는 스마트 포인터를 사용하여 자동 메모리 관리가 가능함
  13. 사용자 정의 메모리 할당자 (allocator)를 사용할 수 있음
  14. 사용자가 적절한 매크로 상수를 정의함으로써 fast delegate의 특징을 컴파일 시 결정지을 수 있음

 

하지만 위의 기능을 하나의 delegate 구현에서 모두 지원하면 꽤 그럴싸한 fast delegate 라고 생각되어지는 군요. 그럼 제가 구현한 fast delegate에 대해서 설명드립니다.

 

기본 구현 원리

멤버 함수 포인터의 특징이나 동작 원리에 대해서는 Don Cluston의 글을 읽어보십시오. Do Cluston의 글 보다 더 자세히 멤버 함수를 설명한 문헌을 아마 찾기는 어려울거라 생각합니다. 제가 설명하기에는 워낙 분량이 많은데다가 어줍잖은 번역보다는 직접 읽어보시는게 이해하시는데 오히려 더 나을거라 생각됩니다.

 

그럼 멤버 함수의 특징에 대해서 어느정도 알고 계신다는 전제하에서 Sergey Ryazanov의 구현을 살펴봅니다. 멤버 함수 포인터를 저장하기 위해서는 멤버 함수가 속하는 클래스의 타입정보를 동시에 저장해야 합니다. 그렇지만 범용적인 delegate (즉 특정 클래스에 종속되지 않는 delegate)를 구현하기 위해서는 특정 클래스의 타입 정보를 delegate 클래스의 템플릿 인자로 지정할 수는 없습니다. 따라서 멤버 함수 포인터를 delegate 에 바인딩할 때가 멤버 함수가 속하는 클래스의 타입 정보를 알 수 있는 유일한 순간이라고 생각할 수 있습니다. 따라서 멤버 함수의 바인딩을 수행하는 delegate의 멤버 함수는 저장되는 멤버 함수 포인터의 함수 호출 정보(리턴 타입, 입력 인자 타입, 클래스 타입)를 템플릿 인자로 받는 템플릿화 된 함수 입니다. 아래 Sergey Ryazanov 의 구현에서는 delegate from_method 가 이러한 역할을 수행하는 함수에 해당 합니다.

 

class delegate

{

public:

  delegate()

    : object_ptr(0)

    , stub_ptr(0)

  {}

 

  template < class T, void (T::*TMethod)(int) >

    static delegate from_method(T* object_ptr)

  {

    delegate d;

    d.object_ptr = object_ptr;

    d.stub_ptr = &method_stub< T, TMethod >; // #1

    return d;

  }

 

  void operator()(int a1) const

  {

    return (*stub_ptr)(object_ptr, a1);

  }

 

private:

  typedef void (*stub_type)(void* object_ptr, int);

 

  void* object_ptr;

  stub_type stub_ptr;

 

  template < class T, void (T::*TMethod)(int) >

    static void method_stub(void* object_ptr, int a1)

  {

    T* p = static_cast< T* >(object_ptr);

    return (p->*TMethod)(a1); // #2

  }

};

 

앞서 이미 언급했듯이 여기서 사용되어진 템플릿화된 static 멤버 함수는 C++ 표준에 완벽하게 부합하지만 VC6 같은 오래된 컴파일러에게 커다란 혼란을 초래합니다. 제 경험으로 부터 이러한 경우 템플릿화된 static 멤버 함수를 템플릿 클래스의 static 멤버 함수로 변경하면 VC6 에서도 문제없이 돌아간다는 것을 알고 있었기에 이를 바탕으로 위의 소스 코드를 다음과 같이 변경할 수 있었습니다.

 

class delegate

{

public:

  delegate()

    : object_ptr(0)

    , stub_ptr(0)

  {}

 

  template < class T, void (T::*TMethod)(int) >

    static delegate from_method(T* object_ptr)

  {

    delegate d;

    d.object_ptr = object_ptr;

    d.stub_ptr = &delegate_stub_t< T, TMethod >::method_stub; // #1

    return d;

  }

 

  void operator()(int a1) const

  {

    return (*stub_ptr)(object_ptr, a1);

  }

 

private:

  typedef void (*stub_type)(void* object_ptr, int);

 

  void* object_ptr;

  stub_type stub_ptr;

 

  template < class T, void (T::*TMethod)(int) >

  struct delegate_stub_t

  {

    static void method_stub(void* object_ptr, int a1)

    {

      T* p = static_cast< T* >(object_ptr);

      return (p->*TMethod)(a1); // #2

    }

  };

};

 

이제 한 가지 문제점은 해결되었지만 여전히 멤버 함수 포인터를 템플릿 인자로 전달하는, C++ 표준에 부합하지만 호환성이 떨어지는 또 다른 한 가지 문제점을 해결해야만 합니다. 멤버 함수 포인터를 템플릿 인자로 전달하는 대신에 어떤 범용적으로 사용될 수 있는 형태로 변환하여 delegate 내부에 저장해야만 합니다. Don Cluston의 글을 읽어 보셨다면 멤버 함수 포인터는 그 크기가 4 바이트에서 20 또는 24 바이트 정도까지 다양하게 변할 수 있다는 사실을 알 수 있습니다. 따라서 임의의 멤버 함수 포인터를 스택 메모리에 저장하고자 한다면 크기가 20 또는 24 바이트 보다 더 큰 배열 버퍼를 사용하는 방법을 고려할 수 있습니다. 혹시 Rich Hickey 의 글 "CALLBACKS IN C++ USING TEMPLATE FUNCTORS" 를 읽어본 적이 있으시다면 제가 지금 막 하고자 하는 방식이 10년 전에 (1994) 벌써 유사하게 구현되었다는 사실을 알 수 있으실 겁니다. 이러한 방식의 한 가지 문제점은 24 바이트 크기의 배열 버퍼를 사용하기로 결정하였다면 멤버 함수 포인터의 크기가 4 또는 8 바이트인 실재 프로그래밍 환경을 고려하였을 때 delegate 하나 당 16 ~ 20 바이트 정도의 메모리를 이유없이 낭비해야 한다는 점입니다. 대신에 낭비대는 공간을 줄이고자 12 바이트나 16 바이트의 크기의 배열 버퍼를 사용하기로 결정하였다면 프로그램에 다음과 같은 경고문을 참부해야할 지 도 모릅니다.

 

  "이 프로그램은 멤버 함수 포인터의 크기가 16 바이트 또는 그 이하인 경우에만 제대로 동작합니다." 라는 경고문을 넣어야 할 것 같습니다. 아무래도 파리 날리기 딱 좋은 경고문인 듯 합니다.

 

코드 프로젝트에 올라온, delegate 구현 하고자 하는 프로그래머라면 반드시 필독해야 하는, 또 다른 글 "Yet Another Generalized Functors Implementation in C++"에서 에서 Aleksei Trunov는 메타 메타 템플릿을 사용하여 위와 같은 문제점을 해결할 수 있는 방법을 제시합니다. 이렇게 여기 저기서 좋은 특징들 만을 따와서 조합하면 꽤 괜찮은 결과물이 나오지 않을까 생각하게 되었습니다.

 

// partial specialization version

template < bool t_condition, typename Then, typename Else > struct If;

template < typename Then, typename Else > struct If < true, Then, Else > { typedef Then Result; };

template < typename Then, typename Else > struct If < false, Then, Else > { typedef Else Result; };

 

// nested template structure version

template < bool t_condition, typename Then, typename Else >

struct If

{

  template < bool t_condition_inner > struct selector;

  template < > struct selector < true > { typedef Then Result; };

  template < > struct selector < false > { typedef Else Result; };

 

  typedef typename selector < t_condition >::Result Result;

 

};

 

메타 메타 템플릿에서 가장 널리 사용되는 기법 중의 하나인 If 를 사용하면 멤버 함수 포인터의 크기가 4 ~ 8 바이트인 대부분의 경우에 내부 배열 버퍼에 멤버 함수 포인터를 저장하고 만약 멤버 함수 포인터의 크기가 내부 배열 버퍼의 크기보다 큰 경우에만 힙 메모리에 해당 멤버 함수 포인터를 저장할 수 있는 크기의 메모리를 할당하여 사용하는 방법이 가능해 집니다. 무엇보다도 어떤 방식으로 멤버 함수 포인터가 delegate 에 저장되는 지는 컴파일 시에 컴파일러가 결정해줍니다.

 

class delegate

{

public:

  delegate()

    : object_ptr(0)

    , stub_ptr(0), fn_ptr_(0), is_by_malloc(false)

  {}

 

  ~delegate()

  {

    if(is_by_malloc)

    {

      is_by_malloc = false;

      ::free(fn_ptr_); fn_ptr_ = 0;

    }

  }

 

  template < class T >

  struct fp_by_value

  {

    inline static void Init_(delegate & dg, T* object_ptr, void (T::*method)(int))

    {

      typedef void (T::*TMethod)(int);

      dg.is_by_malloc = false;

      new (dg.buf_) TMethod(method);

    }

    inline static void Invoke_(delegate & dg, T* object_ptr, int a1)

    {

      typedef void (T::*TMethod)(int);

      TMethod const method = *reinterpret_cast < TMethod const * > (dg.buf_);

      return (object_ptr->*method)(a1);

    }

 

  };

 

  template < class T >

  struct fp_by_malloc

  {

    inline static void init_(delegate & dg, T* object_ptr, void (T::*method)(int))

    {

      typedef void (T::*TMethod)(int);

      dg.fn_ptr_ = ::malloc(sizeof(TMethod));

      dg.is_by_malloc = true;

      new (dg.fn_ptr_) TMethod(method);

    }

    inline static void invoke_(delegate & dg, T* object_ptr, int a1)

    {

      typedef void (T::*TMethod)(int);

      TMethod const method = *reinterpret_cast < TMethod const * > (dg.fn_ptr_);

      return (object_ptr->*method)(a1);

    }

 

  };

 

  template < class T >

  struct select_fp_

  {

    enum { condition = sizeof(void (T::*)(int) <= size_buf) };

    typedef fp_by_value<T>  Then;

    typedef fp_by_malloc<T> Else;

 

    typedef typename If < condition, Then, Else >::Result type;

 

  };

 

  template < class T >

    void from_method(T* object_ptr, void (T::*method)(int), int)

  {

    select_fp_<T>::type::Init_(*this, object_ptr, method);

 

    this->object_ptr = object_ptr;

    stub_ptr = &delegate_stub_t < T >::method_stub;

  }

 

  void operator()(int a1) const

  {

    return (*stub_ptr)(*this, object_ptr, a1);

  }

 

private:

  enum { size_buf = 8 };

  typedef void (*stub_type)(delegate const & dg, void* object_ptr, int);

 

  void* object_ptr;

  stub_type stub_ptr;

 

  union

  {

    void * fn_ptr_;

    unsigned char buf_[size_buf];

  };

  bool is_by_malloc;

 

  template < class T >

  struct delegate_stub_t

  {

    inline static void method_stub(delegate const & dg, void* object_ptr, int a1)

    {

      T* p = static_cast< T* >(object_ptr);

      return select_fp_<T>::type::invoke_(dg, p, a1);

    }

 

  };

 

};

 

이제 C++ 표준에 완벽히 부합하면서도 VC6 과 같이 오래된 컴파일러에서도 문제 없이 돌아가는 호환성이 탁월한(?) fast delegate를 구현할 수 있게 되었습니다. 저장되어진 멤버 함수 포인터를 호출하는데 있어서 두 단계의 간접 경로가 생겼지만 inline 확장이 컴파일러에 의해서 적절하게 수행된 후에는 Sergey Ryazanov 의 fast delegate 에 거의 필적할 만한 속도를 성취할 수 있습니다.

 

이 뿐만 아니라 추가적으로 멤버 함수 포인터의 바이너리 표현을 내부적으로 저장하고 있기 때문에 delegate의 동일/대/소 비교 연산이 가능해졌습니다. 이는 비교 연산을 요구하는 std::set 등의 STL 컨테이너에서 delegate를 문제없이 사용할 수 있게 되었다는 것을 의미합니다.

 

멤버 함수 포인터를 저장하기 위해서 사용되는 내부 배열 버터의 크기는 FD_BUF_SIZE_IN_COUNTOF_PVOID 매크로를 적절하게 정의함으로써 변경이 가능합니다. 기본 설정으로 8 바이트를 사용합니다. 저의 경우 주로 MSVC에서 작업을하고 virtual 상속을 거의 사용하지 않기 때문에 99%의 경우에 힙 메모리를 사용하지 않게 됩니다. 단일 상속인 경우 fast delegate 하나 당 4 바이트의 손실이 있겠지만 얻어지는 속도 이득에 비하면 수용할 수 있을만한 수준의 손실입니다. (게다가 Aleksei Trunov의 의하면 멤버 함수 포인터를 저장하기 위해서 힙 메모리를 할당하여 사용한다고 하여도 allocation granuality 등등의 이유로 요구 되어진 크기의 메모리보다 실재로는 더 큰 메모리 공간이 할당되어 질 수 있고 사용되어 지지 않는 메모리 공간 손실이 생길 수 있다고 합니다. )

 

객체 복사 메니저 함수

 

버젼 1.10 에서 멤버 함수 포인터가 호출되어지는 대상 객체를 바인딩 할 때 (argument binding) 객체의 포인터/레퍼런스를 저장하던 기존의 방식에 추가하여 대상 객체를 내부적으로 복사하는 저장하는 기능을 추가하였습니다.  이렇게 대상 객체를 복사하여 내부적으로 저장하는 경우에 멤버 함수의 호출은 내부적으로 복사되어진 객체를 대상으로 호출되어 집니다. 앞서 언급되었듯이 멤버 함수가 속한 클래스의 타입 정보를  알 수 있는 때는 멤버 함수 포인터와  대상 객체가 바인딩 되어서 delegate에 대입되는 순간 뿐입니다. 따라서 멤버 함수 포인터와 대상 객체의 복사본을 포함하고 있는 delegate 가 다른 delegate 로  대입 되어지거나 이 delegate가 소멸되는 경우에는 내부에 저장되어진 대상 객체의 복사본이 어떠한 타입의 객체인지 알 수가 없습니다. 따라서 앞에서 함수 호출을 위해서 사용되어진 것과 비슷한 방식으로 주첩된 템플릿 클래스의 static 멤버 함수를 이용하여 delegate 내부에 저장된 대상 객체 복사본의 복사 또는 소멸을 담당하는 'stub' 함수를 제공해야 합니다.

 

  typedef void * (*obj_clone_man_type)(delegate &, void const *);

  obj_clone_man_type obj_clone_man_ptr_;

 

  template<typename T>

  struct obj_clone_man_t

  {

    inline static void * typed_obj_manager_(delegate & dg, void const * untyped_obj_src)

    {

      T * typed_obj_src =const_cast<T *>(static_cast<T const *>(untyped_obj_src)); typed_obj_src;

 

      if(dg.obj_ptr_)

      {

        T * typed_obj_this = static_cast<T *>(dg.obj_ptr_);

        delete typed_obj_this;

        dg.obj_ptr_ = 0;

      }

 

      if(0 != typed_obj_src)

      {

        T * obj_new = new T(*typed_obj_src);

        dg.obj_ptr_ = obj_new;

      }

 

      T * typed_obj = static_cast<T *>(dg.obj_ptr_); typed_obj;

      return typed_obj;

    }

 

  };  // template<typename T> struct obj_clone_man_t

 

  obj_clone_man_ptr_ = &obj_clone_man_t<T>::typed_obj_manager_;

 

위에서 정의된 obj_clone_man_type 형의 복사 메니저 함수 포인터를 다음과 같은 형식으로 사용하여서 delegate 내부에 저장된 대상 객체 복사본의 복사 또는 소멸을 타입 안전하게 수행할 수 있습니다.

 

fd::delegate dg1, dg2;

 

// copy the internally cloned object of dg2 into dg1

(*dg1.obj_clone_man_ptr_)(dg1, dg2.obj_ptr_);

 

// destroy the internally cloned object of dg1

(*dg1.obj_clone_man_ptr_)(dg1, 0);

 

delegate 내부에 저장해야 하는 대상 객체 복사본의 크기는 사전에 알 수 없기 때문에 힙 메모리의 사용이 반드시 필요하게 되었습니다. (대상 객체의 크기는 작게는 몇 바이트에서 크게는 수 십 또는 수 백바이트까지 또는 그 이상이 될 수 있겠죠). 이는 속도를 중시하여 힙 메모리의 사용을 회피하고자 하는 fast delegate의 디자인 규약에 어긋납니다. 따라서 최고 속도의 fast delegate를 사용하고자 하는 경우라면 함수 호출이 이루어지는 대상 객체를 delegate 내부에 복사하여 저장하는 방법 대신에 단지 대상 객체의 포인터 (레퍼런스)만을 저장하는 방식을 사용하면 됩니다. (또는 뒤에서 설명되어질 사용자 정의 메모리 할당자를 사용하는 방법을 고려해볼 수 도 있습니다.)

 

멤버 함수 호출의 대상 객체를 delegate 내부에 복사하여 저장하는 방법을 도입한 가장 중요한 이유는 스마트 포인터를 delegate와 연동하여 사용할 수 있게 하기 위해서입니다. 대상 객체의 포인터(레퍼런스)만을 delegate 내부에 저장하는 경우에는 이 delegate가 호출되어지는 순간에 delegate 내부에 저장된 포인터가 가리키는 대상 객체가 호출이 가능한 상태인지를 프로그래머가 보장해야 합니다.

 

C# 과는 달리 C++에는 내장된 garbage collector의 개념이 없기 때문에 프로그래머가 대상 객체의 생성/소멸 주기를 관리해야만 하고 행여 이미 소멸된 대상 객체를 가리키는 포인터를 통하여 delegate 를 호출하게 된다면 실행 에러를 발생시키게 됩니다. 그렇다고 해서 마냥  멤버 함수가 호출되는 대상 객체를 통째로 복사하여 delegate 내부에 저장하고자 한다면 복사된 객체와 원본 객체간의 동기화 문제도 있고 대상 객체의 크기가 수백 바이트가 넘는 커다란 클래스의 객체 였다면 실행 성능에도 커다란 장애가 될 수 있습니다.

 

하지만 C++ 에는 스마트 포인터가 있습니다. 스마트 포인터를 delegate 내부에 복사할 수 있다면 해당 스마트 포인터 객체가 복사 또는 소멸되는 과정을 통해서 대상 객체의 생성/소멸 주기가 스마트(!)하게 관리되어 질 수 있으며 스마트 클래스의 크기는 보통 10 바이트 이내의 작은 크기이기 때문에 복사과정에서도 커다란 성능 저하는 발생하지 않습니다. (물론 스마트 포인터를 저장할 공간을 힙 메모리에서 할당/제거한다면 힙 메모리 사용으로 인한 문제점은 고스란히 가져가게 되겠지만 수백 바이트의 클래스 객체를 복사하는 것보다는 훨씬 빠르겠죠). 눈치 채셨겠지만 위에서 구현된 복사 메니저 함수를 통해서 스마트 포인터를 fast delegate에서 사용하기 위한 기본적인 구현은 벌써 이뤄진 상태입니다.

 

임의의 스마트 포인터를 멤버 함수 포인터의 대상 객체를 바인딩하기 위해서는 추가로 다음 두 가지 요구사항을 충족해야 합니다. (boost::mem_fn 에서 아이디어를 가져왔습니다.)

 

1. 스마트 포인터 클래스에 대한 레퍼런스 또는 const 레퍼런스를 입력 인자로 받고 스마트 포인터 클래스에 저장되어지는 타입의 포인터를 돌려주는 template<typename T> T get_pointer(smart_ptr<T> (const) & p);  함수를 접근 가능한 namespace 내에서 정의 해야합니다.

 

2. 스마트 포인터 클래스는 element_type 이라는 public 인터페이스 (typedef)을 정의해야 합니다. (std::auto_ptr, boost 스마트 포인터들, loki::smartPtr 모두 element_type 을 정의하고 있습니다. 이 사항은 C++ 표준이 요구하는 조건일지도 모르겠습니다.)

 

기본적으로 포인터와 std::auto_ptr<T> 에 대한 get_pointer() 함수를 제공하고 있습니다.

 

namespace fd

{

 

template<class T> inline

T * get_pointer(T * p)

{

  return p;

}

 

template<class T> inline

T * get_pointer(std::auto_ptr<T> & p)

{

  return p.get();

}

 

}  // namespace fd

 

boost::shared_ptr 는 boost:;shared_ptr가 속한 boost namespace에 get_pointer() 를 제공하고 있기 때문에 Koenig Lookup (Arguement-dependent lookup)을 구현한 VC71+, GCC3.x.x.x 등등의 컴파일러에서는 추가적인 수고 없이 바로 fast delegate와 연동하여 사용이 가능합니다. 그런데 VC6은 C++ 표준에 부합하지 못하여 이를 구현하지 못하고 있기 때문에 다음과 같이 get_pointer() 를 fd namespace에 정의해주어야 합니다.

 

#if defined(TEST_BOOST_COMPATIBLE) && defined(FD_MS_VC6)

// Even thogh get_pointer is defined in boost/shared_ptr.hpp, VC6 doesn't seem to

// implement Koenig Lookup (argument dependent lookup) thus can't find the definition,

// So we define get_pointer explicitly in fd namesapce to help the poor compiler

namespace fd

{

  template<class T>

    T * get_pointer(boost::shared_ptr<T> const & p)

  {

    return p.get();

  }

}

#endif  // #if defined(TEST_BOOST_COMPATIBLE) && defined(FD_MS_VC6)

 

이로써 fast delegate의 기본 구현 원리는 대략 설명한 것 같습니다.

fast delegate 사용법

Preferred 문법 & Portable 문법

#include "delegate.h"

 

// Preferred Syntax

fd::delegate < void (intchar *) > dg;

 

#include "delegate.h"

 

// Portable Syntax

fd::delegate2 < voidintchar * > dg;

 

Preferred 문법은 Template Partial Specialization을 지원하는 최신의 컴파일러에서만 동작합니다. VC6과 같은 오래된 컴파일러들에서는 Portable 문법을 사용할 수 있습니다. 구현 과정에서 Preferred 문법과 Portable 문법을 같이 사용할 수 있도록 작성하였기 때문에 두 문법을 혼합하여도 전혀 문제가 되지 않습니다. (물론 컴파일러가 두 가지 문법을 다 지원하는 경우에만 혼용이 가능하겠죠). 따라서 이후로 예제들은 모두 Portable 문법의 형식으로 작성합니다.

 

세 가지 호출 가능한 개체 (three callable entities)

 

함수 호출 연산자 ( operator () )를 가지는 세 가지 개체를 모두 지원합니다.

 

  • 프리 함수 (Free function)
  • 멤버 함수 (Member function)
  • 함수 객체 (Functor, Function object)

 

사용법이 boost::function & boost::bind 조합과 대부분 유사하지만 함수 객체(functor) 의 경우에만 조금 다릅니다. 앞으로 예제에서 사용할 테스트 클래스/함수들은 다음과 같습니다.

 

// ======================================================================

// example target callable entities

// ======================================================================

 

class CBase1

{

public:

  void foo(int n) const { }

  virtual void bar(int n) { }

  static void foobar(int n) { }

  virtual void virtual_not_overridden(int n) { }

};

 

// ------------------------------

 

class CDerived1 : public CBase1

{

  std::string name_;

 

public:

  explicit CDerived1(char * name) : name_(name) { }

  void foo(int n) const

  { name_; /*do something with name_ or this pointer*/ }

  virtual void bar(int n) { name_; /*do something with name_ or this pointer*/ }

  static void foobar(int n) { }

  void foofoobar(CDerived1 * pOther, int n)

  { name_; /*do something with name_ or this pointer*/ }

};

 

// ------------------------------

 

void hello(int n) { }

void hellohello(CDerived1 * pDerived1, int n) { }

 

// ------------------------------

 

struct Ftor1

// stateless functor

  void operator () (int n)

  { /*no state nor using this pointer*/ }

};

 

struct Ftor2

// stateful functor

  string name_;

  explicit Ftor2(char * name) : name_(name) { }

  void operator () (int n)

  { name_; /*do something with name_ or this pointer*/ }

};

 

 

프리 함수 (Free function)

// copy-constructed

fd::delegate1 < voidint > dg1(&::hello);

fd::delegate1 < voidint > dg2 = &CBase1::foobar;

 

dg1(123);

dg2(234);

 

// assigned

fd::delegate < voidint > dg3;

dg3 = &CDerived1::foobar;

 

dg3(345);

 

멤버 함수 (Member function) - 멤버 함수 어댑터 (Member function adapter)

CBase1 b1; CDerived1 d1("d1");

 

// copy-constructed

fd::delegate2 < void, CBase1 *, int > dg1(&CBase1::foo); // pointer adapter

fd::delegate2 < void, CBase1 &, int > dg2 = &CBase1::bar; // reference adapter

 

dg1(&b1, 123);

dg2(b1, 234);

 

// assigned

fd::delegate2 < void, CDerived1 *, int > dg3;

dg3 = &CDerived1::foo;

 

dg3(&d1, 345);

 

멤버 함수를 위와 같이 fd::delegate2 < void, CBase1 *, int >. 로 선언하여 사용하기 보다 fd::delegate1 < voidint > 의 형식으로 사용하기를 원한것이라면 뒤에 설명되어질 멤버 함수 바인딩 부분을 보시면 됩니다.

 

// excerpted from boost::function online document

template < typename P >

operator()(cv-quals P& x, Arg1 arg1, Arg2 arg2, ..., ArgN argN) const

{

  return (*x).*mf(arg1, arg2, ..., argN);

}

 

위와 같은 형식으로 멤버 함수를 호출할 수 있다는 것은 매우 흥미로운 사실입니다. boost::function 을 사용하면서 멤버 함수 포인터도 위와 같은 형식으로 호출할 수 있지 않을까 하는 착각을 했을 정도 입니다. (실재로 멤버 함수 포인터를 위와 같이 호출해본 경험이 있습니다. TT 물론 컴파일 에러가 발생합니다.) 위와 같은 기능을 위해서 내부적으로 상당히 복잡한 구현이 이루어졌습니다. 이와 같은 기능을 ' 멤버 함수 어댑터 ' 라고 부르겠습니다.

함수 객체 (Functor, Function object)

Ftor2 f2("f2");

 

// copy-constructed

bool dummy = true;

fd::delegate1 < voidint > dg1(f2, dummy);  // store the cloned functor internally

fd::delegate1 < voidint > dg2(&f2, dummy);  // store only the pointer to the functor

 

dg1(123);  // (internal copy of f2).operator ()(123);

dg2(234);  // (&f2)->operator ()(234);

 

// assigned ( special operator <<= )

fd::delegate1 < voidint > dg3, dg4, dg5, dg6;

dg3 <<= f2;      // store the cloned functor internally

dg4 <<= &f2;      // store only the pointer to the functor

dg5 <<= Ftor1();  // store the cloned functor internally

dg6 <<= &Ftor1(); // store only the pointer to the functor which is temporary

 

dg3(345);  // (internal copy of f2).operator ()(345);

dg4(456);  // (&f2)->operator () (456);

dg5(567);  // (internal copy of temporary Ftor1).operator ()(567);

dg6(678);  // (&temporary Ftor1 that has been already destroyed)->operator ()(678); Runtime error!

 

함수 객체는 함수 호출 연산자 ( operator() )를 가지고 있는 데이터 형입니다. 앞서 설명한 멤버 함수 어댑터 기능과 뒤에서 설명되어질 멤버 함수 바인딩 기능이 구현되어 있다면 따로 함수 객체 지원을 하지 않더라도 다음과 같은 형식으로 펑터를 delegate 에 대입 하는 것이 가능합니다.

 

Ftor1 f1;

fd::delegate2 < void, Ftor1 *, int > dg1(&Ftor1::operator ());

dg1(&f1, 123);

 

위의 방식은 아무래도 프로그래머 친화적이지 못하기 때문에 함수 객체의 대입이 바로 가능하도록 지원하려고 했습니다. 문제는 delegate 자체도 함수 객체라는 사실입니다. 템플릿 특화와 오버로딩이 유동적이지 못한 VC6 과 같은 컴파일러를 지원을 고려하면서 복사 생성자나 함수 객체를 위한 대입 연산자 ( operator = )를 추가하게 되면 컴파일러에 대 혼동을 초래하였고 게다가 원본 함수 객체가 delegate 형인지 아니면 다른 일반적인 함수 객체 형인지 구별하는 것 역시 쉬운일이 아니었습니다. 따라서 최상의 선택은 delegate 를 입력으로 받는 복사 생성자나 대입 연산자와 조금 다른 함수 시그니쳐(추가적인 bool 더미 파라미터가 있는 복사 생성자와 operator <<=) 를 사용하여 delegate 형이 아닌 다른 일반 함수 객체를 delegate 에 대입하는 방법을 선택하게되었습니다.

 

함수 객체를 위한 복사 생성자 형태에서는 추가적인 bool 인자를 받습니다. (bool 인자는 내부적으로 무시되어집니다. 단지 다른 함수 시그니쳐를 제공하여 VC6 과 같은 컴파일러를 도와주는 목적으로 사용되어 졌을 뿐입니다.) 대입 연산자의 경우 operator <<= 를 사용하여 delegate 가 아닌 다른 일반적인 함수 객체를 delegate에 대입할 수 있도록 하였습니다.

 

함수 객체의 참조형을 입력으로 받는 경우에는 delegate 내부에 함수 객체의 복사본을 저장하게 됩니다. 함수 객체의 포인터를 입력으로 받는 경우에는 단지 함수 객체의 포인터만을 delegate 내부에 저장하기 때문에 프로그래머는 delegate 호출 시 내부적으로 저장된 포인터가 가리키는 함수 객체가 호출 가능한 상태라는 것을 보장해야만 합니다. 물론 수차례 언급되었듯이 함수 객체의 복사본을 내부에 저장하는 것보다 포인터만을 저장하는 방식이 delegate 의 잦은 생성/복사/소멸이 일어나는 경우에 훨씬 빠른 속도를 보장합니다.

멤버 함수 바인딩 (Member function arguement binding)

CBase1 b1;

 

// copy-constructed

fd::delegate1 < voidint > dg1(&CBase1::foo, b1);  // storing the cloned bound object internally

fd::delegate1 < voidint > dg2(&CBase1::foo, &b1); // storing the pointer to the bound object

dg1(123); // (internal copy of b1).foo(123);

dg2(234); // (&b1)->foo(123);

 

// bind member

fd::delegate1 < voidint > dg3, dg4;

dg3.bind(&CBase1::bar, b1);  // storing the cloned bound object internally

dg4.bind(&CBase1::bar, &b1); // storing the pointer to the bound object

dg3(345); // (internal copy of b1).bar(345);

dg4(456); // (&b1)->bar(456);

 

// fd::bind() helper function

fd::delegate1 < voidint > dg5 = fd::bind(&CBase1::foo, b1, _1);  // storing the cloned bound object internally

fd::delegate1 < voidint > dg6 = fd::bind(&CBase1::foo, &b1, _1); // storing the pointer to the bound object

dg5(567); // (internal copy of b1).foo(567);

dg6(678); // (&b1)->foo(678);

 

std::auto_ptr spb1(new CBase1);

fd::delegate1 < intint > dg1;

dg1.bind(&CBase1::foo, spb1);

dg1(123);  // get_pointer(internal copy of spb1)->foo(123);

 

boost::shared_ptr spb2(new CBase1);

fd::delegate1 < intint > dg2(&CBase1::foo, spb2);

dg2(234);  // get_pointer(internal copy of spb2)->foo(234);

bind() 도우미 함수 (boost 호환성/ boost 로부터의 손쉬운 이전)

fd::bind() 도우미 함수는 boost::function & boost::bind 를 사용했던 기존의 코드를 fast delegate를 사용하는 코드로 손쉽게 이전할 수 있도록 하기위해서 제공됩니다.

 

#include < boost/function.hpp >

#include < boost/bind.hpp >

 

using boost::function1;

using boost::bind;

 

CBase1 b1;

 

function1 < voidint > fn = bind( &CBase1::foo, &b1, _1 );

 

위와 같은 소스코드를 다음과 같이 간단하게 수정하기만 하면 손쉽게 fast delegate를 사용하도록 변경할 수 있습니다.

 

#include "delegate.h"

 

using fd::delegate1;

using fd::bind;

 

CBase1 b1;

 

delegate1 < voidint > fn = bind( &CBase1::foo, &b1, _1 );

 

단 위에서 fd::bind() 는 boost::bind() 와 같이 다양한 기능을 수행하지 않으며 인자  _1 은 단순히 위치 제공자 (Placeholder)일뿐 아무런 기능도 하지 않습니다.

make_delegate() 도우미 함수 (자동 형 유추 (automatic type deduction))

delegate 를 함수의 인자로 넘기는 경우 등등에 유용할 수 있는 함수 입니다. 따로 대상 함수의 형정보를 템플릿 파라미터로 넘겨주지 않아도 입력으로 부터 자동으로 관련 정보들을 유추해 낼 수 있습니다.

 

typedef fd::delegate1 < voidint > MyDelegate1;

typedef fd::delegate2 < void, CDerived *, int > MyDelegate2;

 

void SomeFunction1(MyDelegate1 myDg) { }

void SomeFunction2(MyDelegate2 myDg) { }

 

CBase1 b1; CDerived1 d1("d1");

 

// free function version

SomeFunction1(fd::make_delegate(&::hello));

SomeFunction2(fd::make_delegate(&::hellohello);

 

// member function adapter version

SomeFunction1(fd::make_delegate((CBase1 *)0, &CBase1::foobar));

SomeFunction2(fd::make_delegate((CDerived *)0, &CDerived1::foo));

 

// member function argument binding version

SomeFunction1(fd::make_delegate(&CBase1::foo, &b1));

SomeFunction2(fd::make_delegate(&CDerived1::foofoobar, &d1);

SomeFunction1(fd::make_delegate(&CBase1::foo, b1));

SomeFunction2(fd::make_delegate(&CDerived1::foofoobar, d1);

 

한참 앞서 제시한 예제에서 사용된 클래스의 정의를 보면 CBase1::virtual_not_overridden() 멤버 함수가 있습니다. CBase1 을 상속 받은 CDerived1 클래스에서는 virtual_not_overridden() 함수을 오버라이드하지 않았습니다. (virtual 이든 virtual 이 아니든 상관없이 해당 함수와 같은 이름의 함수가 상속 받은 클래스에는 없다는 사실이 중요합니다.) 이 경우에 우리는 CDerived1 형 객체 또는 포인터를 통해서 CDerived1::virtual_not_overridden() 함수를 호출하는 것이 문제가 없다는 것을 쉽게 알 수 있습니다. 그런데 이러한 함수가 템플릿 인자로 넘어가는 경우나  (make_delegate 와 같은 자동 유추 함수를 통해서) 자동으로 유추되어지는 경우에 재미있는 사실이 있습니다. 프로그래머가 CDerived1::virtual_not_overridden() 을 특정 템플릿 함수에 주었을 경우 컴파일러는 CDerived1::virtual_not_overridden 형이 아닌 CBase1::virtual_not_overridden 형으로 템플릿 인자를 받아들인다는 것입니다.

 

만약에 멤버 함수 어댑터형의  fd::delegate2<int, CDerived1 *, int> 형의 delegate를 자동으로 유추하기 위해서 make_delegate() 의 입력으로 &CDerived1::virtual_not_overriden 을 주었다면 실재로 유추되어 생성되는 delegate는  fd::delegate2<int, CDerived1 *, int> 형이 아니라  fd::delegate2<int, CBase1 *, int> 형이 된다는 사실입니다. 이러한 이유 때문에 멤버 함수 어댑터 형의 make_delegate() 첫 번째 인자로 저장하고자 하는 멤버 함수가 속해야 하는  클래스 타입 정보를 줄 수 있는 타입 포인터를 제공해야 합니다.

 

동일/대/소 비교 & 기타

typedef fd::delegate1 < voidint > MyDelegate;

CBase1 b1, b2; CDerived1 d1("d1");

 

// ----------------------------------------------------------------------

 

MyDelegate dg1(&CBase1::foo, &b1);

MyDelegate dg2 = &::hello;

 

if(dg1 == dg2)

cout << "dg1 equals to dg2" << endl;

else

cout << "dg1 does not equal to dg2" << endl;

 

if(dg1 > dg2)

{

  cout << "dg1 is greater than dg2" << endl;

  dg1(123);

}

else if(dg1 < dg2)

{

  cout << "dg2 is greater than dg1" << endl;

  dg2(234);

}

 

// ----------------------------------------------------------------------

 

MyDelegate dg3 = dg1;

MyDelegate dg4(&CBase1::foo, &b2);

 

// both function pointer and its bound callee object pointer stored in dg1 is the same as those stored in dg3

if(0 == dg1.compare(dg3))

{  // this test return true

  dg3(345);

}

if(0 == dg1.compare(dg3, true))

{  // this test return true as well

  dg3(456);

}

 

// ----------------------------------------------------------------------

 

// function pointer stored in dg1 is the same as that stored in dg4

// but their bound callee object pointers are not the same

if(0 == dg1.compare(dg4))

{  // this test return true

  dg4(567);

}

if(0 == dg1.compare(dg4, true))

{  // this test return fail

  dg4(678);

}

 

// ----------------------------------------------------------------------

 

if(dg2 != 0)

{  // this test return true

  cout << "dg2 is not empty" << endl;

}

 

if(dg2)

{  // this test return true

  cout << "dg2 is not empty" << endl;

}

 

if(!!dg2) 

// this test return true

  cout << "dg2 is not empty" << endl;

}

 

if(!dg2.empty()) 

// this test return true

  cout << "dg2 is not empty" << endl;

}

 

// ----------------------------------------------------------------------

 

dg1.swap(dg2);

 

MyDelegate(dg2).swap(dg1);  // dg1 = dg2;

 

MyDelegate().swap(dg1); // dg1.clear();

 

dg2.clear();

 

dg3 = 0;

 

// ----------------------------------------------------------------------

 

if(dg3.empty())

{

  try

  {

    dg3(789);

  }

  catch(std::exception & e) { cout << e.what() << endl; } // 'call to empty delegate' exception

}

 

// ----------------------------------------------------------------------

 

CBase1 * pBase = 0;

// binding null callee object on purpose

dg3.bind(&CBase1::foo, pBase);

try

{

  FD_ASSERT( !dg3.empty() );

  dg3(890);

}

// 'member function call on no object' exception

catch(std::exception & e) { cout << e.what() << endl; }

const 지시자 (const correctness)

CBase1 b1;

CBase1 const cb1;

 

// --------------------------------------------------

// argument binding

 

MyDelegate dg1(&CBase1::foo, &b1);

MyDelegate dg2(&CBase1::foo, &cb1);

MyDelegate dg3(&CBase1::bar, &b1);

// compile error! const member function can not be called on non-const object

// MyDelegate dg4(&CBase1::bar, &cb1);

 

dg1(123);

dg2(234);

dg3(345);

 

// --------------------------------------------------

// member function adapter

 

fd::delegate2<INT, CBase1 *, int> dg4(&CBase1::foo);

fd::delegate2<INT, CBase1 *, int> dg5(&CBase1::bar);

fd::delegate2<INT, CBase1 *, int const> dg6(&CBase1::foo);

// compile error! non-const member function can not be used for const member function adapter

// fd::delegate2<INT, CBase1 *, int const> dg7(&CBase1::bar);

 

dg4(&b1, 456);

// compile error! const object cannot be used non-const member function adapter

// dg4(&cb1, 456);

dg5(&b1, 567);

// compile error! const object cannot be used non-const member function adapter

// dg5(&cb1, 567);

dg6(&b1, 678);

dg6(&cb1, 678);

플랫폼 별 호출 규약 (platform specific calling conventions)

호출 규약은 C++ 표준 사항이 아니지만 많은 WIN32 API 와 COM API 가 __stdcall 호출 규약을 사용하고 있기 때문에 간단하게 무시할 수만은 없습니다. 구현 관점에서 보면 다양한 호출 규약을 지원하는 것은 똑같은 함수들을 각각의 호출 규약별로 호출 규약만 다르게 복사하는 단순한 과정일 뿐입니다. 호출 규약은 기본적으로 모두 비활설화 되어 있으며 특정한 호출 규약을 지원하고자 하는 경우에는 "delegate.h'를 직접 또는 간접적으로 include 하기 전에 다음에 나열된 매크로 중에서 필요한 매크로들을 정의하기만 하면 됩니다.

 

  • FD_MEM_FN_ENABLE_STDCALL - to enable __stdcall support for member function
  • FD_MEM_FN_ENABLE_FASTCALL - to enable __fastcall support for member function
  • FD_MEM_FN_ENABLE_CDECL - to enable __cdecl support for member function
  • FD_FN_ENABLE_STDCALL - to enable __stdcall support for free function
  • FD_FN_ENABLE_FASTCALL - to enable __fastcall support for free function
  • FD_FN_ENABLE_PASCAL - to enable Pascal support for free function

(주) 현재 호출 규약은 MSVC 환경에서만 제대로 동작합니다. 제가 gcc 를 잘 몰라서 gcc에서는 아직 제대로 동작하지를 않는군요.  혹시 잘 아시는 분이 도와주신다면 감사드리겠습니다.

 

유연한 형검사(type-check relaxation)

템플릿 인자로 넘어오는 형검사는 매우 엄격한데 이러한 엄격한 형검사는 때때로 지나친 제약이 될 수가 있습니다. 예를 들어서 GUI 프로그램에서 int (*) (int) 형 함수들과 int (*) (long) 형의 함수들을 섞어서 쓰는 경우는 흔하게 발생 할 수 있습니다. 이러한 함수들을 fd::delegate<int, int> 형 delegate에 저장하고자 했을 경우 컴파일러는 int (*) (long) 형의 함수들은 delegate에 저장될 수 없다고 컴파일 오류를 발생시킵니다. 비록 GUI 프로그램에서 int 와 long 은 자주 혼용되고 함수 오버로딩 등에서도 서로 호환성이 있다고는 하지만 int 와 long이 분명히 서로 다른 타입이기 때문이죠.

 

다른 fast delegate 들은 이렇게 엄격한 템플릿 인자 형검사의 제한을 가지고 있지만 제 delegate는 이러한 제한을 조금 유연하게 대처할 수 있도록 작성되었습니다. (다른 fast delegate들의 구현과는 달리 boost::function 의 경우에만 유연한 형검사가 이루어지고 있습니다.)

FD_DISABLE_TYPE_CHECK_RELAXATION 매크로가 정의 되지 않는 한 유연한 형검사 모드는 기본적으로 활성화 되어져 있습니다.

 

유연한 형검사 모드에서는 다음의 세 가지 조건을 충족하면 임의의 호출 가능한 개체를 delegate에 저장할 수 있습니다.

  1. 호출 가능한 개체의 함수 입력 인자의 숫자와 delegate의 입력 인자의 숫자가 동일하다.
  2. 각각의 delegate의 입력 인자는 그에 대응하는 호출 가능한 개체의 함수 인자형으로 암시적인 변환(implicit conversion or trivial conversion)이 가능하다.
  3. delegate 의 리턴 타입과 호출 가능한 개체의 리턴 타입은 상호 암시적인 변환이 가능하다.

 

위의 조건 중 하나라도 만족하지 않는 경우 컴파일러는 컴파일 시 경고/오류 메세지를 발생시킵니다.

 

CBase1 b1;

//

// int CBase1::foo(int) const;

// int CBase1::bar(int);

//

 

fd::delegate1 < intlong > dg1(&CBase1::foo, &b1);

 

dg1(123);

 

위의 delegate 정의는 이론적으로 다음의 함수 정의와 동등합니다.

 

CBase1 b1;

int fd_delegate1_dg1(long l)

{

  return b1.foo(l);

}

 

이 예제를 통해서 왜 세 가지 조건을 만족해야만 delegate 특정 함수를 대입 또는 바인딩이 가능하게 되는지 볼 수 있습니다.

 

 

// compile warning! : 'return' : conversion from '' to '', possible loss of data

fd::delegate1 < floatlong > dg2(&CBase1::bar, &b1);

 

위의 delegate 정의는 다음의 함수 정의와 동일합니다.

 

float fd_delegate1_dg2(long l)

{

  // compile warning! : possible loss of data

  return b1.bar(l);

}

 

'int' 형의 리턴 타입은 'float' 형의 리턴 타입으로 암시적인 변환이 가능하지만 'float' 형에서 'int' 형으로의 변환은 'a possible loss of data' 경고를 발생 시킵니다.

 

// compile error! : cannot convert parameter 3 from 'char *' to 'int'

fd::delegate1 < intchar * > dg3(&CBase1::foo, &b1);

 

위의 delegate 정의는 다음의 함수 정의와 동등합니다.

 

int fd_delegate1_dg3(char * ch)

{

  // compile error! : cannot convert parameter 'ch' from 'char *' to 'int'

  return b1.foo(ch);

}

 

'char *' 형은 'int' 형으로 암시적인 변환이 불가능하기 때문에 컴파일러는 오류를 발생시킵니다.

 

CDerived1 d1("d1");

//

// class CDerived1 : public CBase1 { };

//

fd::delegate2 < int, CDerived1 *, long > dg5(&CBase1::bar);

 

위의 delegate 정의는 다음의 함수 정의와 동등합니다.

 

int fd_delegate2_dg5(CDerived1 * pd1, long l)

{

  return pd1->bar(l);

}

 

'CDerived1' 형으로 부터 'CBase1' 형으로의 변환은 형격상(Upcasting)이기 때문에 항상 안전한 형변환이며 암시적으로 변환이 가능합니다.

디버그 지원 - 컴파일 시 정적 경고 (static assertion at compile-time)

함수 또는 멤버 함수가 delegate에 할당 또는 바인딩 되어질 때 컴파일러는 필요한 함수 호출 연산자 ( operator () ) 까지 컴파일 시에 생성합니다. 만약에 타입 불일치 경고나 오류가 발생하게 된다면 이러한 경고나 오류가 발생한 소스의 근원지를 찾는 것이 매우 힘듭니다. 메타 메타 템플릿 프로그래밍의 가장 큰 단점 중의 하나라고 할 수 있습니다. VC71 같은 영리한 컴파일러의 경우에는 자세한 템플릿 인자 정보를 제공하면서 수 단계의 템플릿 함수 과정을 거쳐서 문제를 일으킨 소스 위치를 찾는 것이 가능하지만 VC6 의 경우 한 단계 많게는 두 단계 정도 까지의 정보만을 제공하기 때문에 문제가 소스의 어느 부분에서 발생했는지 찾아내는 것이 매우 어려워집니다. 따라서 디버그 모드에서는 가능한한 타입 불일치가 발생 했을 경우 문제의 근원에서 최대한 가까운 위치에서 정정 경고를 발생하도록 FD_STATIC_ASSERT 나 FD_PARAM_TYPE_CHK 등등의 타입 확인 매크로를 소스의 여러 곳에 배치해두었습니다.

 

사용자 정의 메모리 할당자 (Custom memory allocator)

delegate는 마지막에서 두 번째 인자로 사용자 정의 메모리 할당자를 받을 수 있습니다. 기본적으로는 std::allocator<void>를 사용합니다. 메모리 할당자는 멤버 함수 포인터의 크기가 delegate 내부 배열 버퍼의 크기보다 큰 경우나 멤버 함수가 호출되는 대상 객체의 복사본을 내부에 저장하고자 할 때 사용되어집니다. 기본 메모리 할당자인 std::allocator<void>는 힙 메모리를 사용하기 때문에 아무래도 상대적으로 속도나 성능의 저하를 가져올 수가 있습니다. 고정된 대용량의 메모리를 미리 할당하고 필요에 의해서 부분 메모리르 할당해 주는 고정 블럭 메모리 할당자를 사용하게 되면 비약적인 성능의 향상을 이룰 수가 있습니다.

 

코드 프로젝트에 올려져 있는 메모리 할당자/관리에 관한 몇 몇 글을 바탕으로 고정 블럭 베모리 할당자인 fd::util::fixed_allocator 를 제공하고 있습니다. 아직 집중적으로 테스트 해보지는 않았지만 별 문제 없이 동작하고 있으며 기존 std::allocator<void> 에 비해서 적어도 수(십)배의 성능 향상이 있는 것으로 보여집니다.

글을 마치며

만약 가장 빠른 속도의 fast delegate를 원한다면 FD_DISABLE_CLONE_BOUND_OBJECT 를 정의하고 (추가적인 보너스로 delegate 하나 당 4 바이트의 메모리를 절약하게 됩니다.) 대상 객체의 포인터를 내부에 저장하는 delegate의 멤버 함수들만을 사용하면 됩니다. 만약 속도와 안전성 두 가지를 모두 원한다면 스마트 포인터와 사용자 정의 메모리 할당자를 조합하여 사용할 수 있습니다. delegate 의 특징은 사용자가 적절한 매크로를 정의함으로써 쉽게 변경할 수 있습니다.

 

delegate의 자세한 구현을 이해하고자 소스 코드를 들여다 보신다면 수 많은 매크로 정의에 기겁을 하실 수 있습니다. 특정 플랫폼의 템플릿 버그들을 해결하기 위해서 어쩔 수 없이 많은 매크로가 사용되어 졌습니다. 팁으로써 풀 버젼의 delegate 구현에서 매크로 확장 후 특정 버젼의 delegate를 추출하는 가이드 라인을 포함했습니다.

 

fast delegate 정의

namespace fd

{

  // ----------------------------------------------------------------------

 

  class bad_function_call;

  class bad_member_function_call;

 

  // ======================================================================

  //

  // class delegateN (Portable Syntax)

  //

  // ======================================================================

 

  template < typename R,typename T1,typename T2,...,typename TN,

    typename Alloocator = std::allocator < void > ,size_t t_countof_pvoid = 2 >

  class delegateN;

 

  // ----------------------------------------------------------------------

 

  // default c'tor

  delegateN< R, T1, T2, ..., TN >::delegateN();

 

  // ----------------------------------------------------------------------

 

  // copy c'tor for 0

  delegateN< R, T1, T2, ..., TN >::delegateN(implClass::clear_type const *);

 

  // ----------------------------------------------------------------------

 

  // copy c'tor

  delegateN< R, T1, T2, ..., TN >::delegateN(delegateN< R, T1, T2, ..., TN > const & other);

 

  // ----------------------------------------------------------------------

 

  // function copy c'tor

  delegateN< R, T1, T2, ..., TN >::delegateN(R (*fn)(T1, T2, ..., TN);

 

  // ----------------------------------------------------------------------

 

  // member function adapter copy c'tors

 

  //  ,where T1 can be trivially converted to either U * or U &

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T2, T3, ..., TN));

 

  //  ,where T1 can be trivially converted to one of  U * or U const * or U & or U const &

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T2, T3, ..., TN) const);

 

  // ----------------------------------------------------------------------

 

  // member function argument binding copy c'tors

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN), T & obj);

 

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN) const, T & obj);

 

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN), T * obj);

 

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN) const, T * obj);

 

  // ----------------------------------------------------------------------

 

  // functor copy c'tors

  templatetypename Functor >

    delegateN< R, T1, T2, ..., TN >::delegateN(Functor & ftor, bool/* dummy*/);

 

  templatetypename Functor >

    delegateN< R, T1, T2, ..., TN >::delegateN(Functor * ftor, bool/* dummy*/);

 

  // ----------------------------------------------------------------------

 

  // assignment from 0

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (implClass::clear_type const *);

 

  // ----------------------------------------------------------------------

 

  // assignment operator

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (delegateN< R, T1, T2, ..., TN > const & other);

 

  // ----------------------------------------------------------------------

 

  // function assignment operator

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (R (*fn)(T1, T2, ..., TN);

 

  // ----------------------------------------------------------------------

 

  // member function adapter assignment operators

 

  //  ,where T1 can be trivially converted to either U * or U &

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T2, ..., TN));

 

  //  ,where T1 can be trivially converted to one of  U * or U const * or U & or U const &

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T2, ..., TN) const);

 

  // ----------------------------------------------------------------------

 

  // member function argument binding assignment operators

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN), T & obj);

 

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN) const, T & obj);

 

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN), T * obj);

 

  delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN) const, T * obj);

 

  // ----------------------------------------------------------------------

 

  // functor assignment operators

  templatetypename Functor >

    delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator <<= (Functor & ftor);

 

  templatetypename Functor >

    delegateN< R, T1, T2, ..., TN > &

    delegateN< R, T1, T2, ..., TN >::operator <<= (Functor * ftor);

 

  // ----------------------------------------------------------------------

 

  // invocation operator

  result_type operator ()(T1 p1, T2 p2, ..., TN pN) const;

 

  // ----------------------------------------------------------------------

 

  // swap

  void delegateN< R, T1, T2, ..., TN >::swap(delegateN & other);

 

  // ----------------------------------------------------------------------

 

  // clear

  void delegateN< R, T1, T2, ..., TN >::clear();

 

  // ----------------------------------------------------------------------

 

  // empty

  bool delegateN< R, T1, T2, ..., TN >::empty() const;

 

  // ----------------------------------------------------------------------

 

  // comparison for 0

  bool operator == (implClass::clear_type const *) const;

 

  bool operator != (implClass::clear_type const *) const;

 

  // ----------------------------------------------------------------------

 

  // compare

  int compare(delegateN const & other, bool check_bound_object = falseconst;

 

  // comparison operators

  bool operator == (delegateN< R, T1, T2, ..., TN > const & other) const;

  bool operator != (delegateN< R, T1, T2, ..., TN > const & other) const;

  bool operator <= (delegateN< R, T1, T2, ..., TN > const & other) const;

  bool operator <  (delegateN< R, T1, T2, ..., TN > const & other) const;

  bool operator >= (delegateN< R, T1, T2, ..., TN > const & other) const;

  bool operator >  (delegateN< R, T1, T2, ..., TN > const & other) const;

 

  // ======================================================================

  //

  // class delegate (Preferred Syntax)

  //

  // ======================================================================

 

  templatetypename R,typename T1,typename T2,...,typename TN,

    typename Allocator = std::allocator< void >,size_t t_countof_pvoid = 2 >

  class delegate< R (T1, T2, ..., TN), Allocator, t_countof_pvoid >;

 

  //

  // the same set of member functions as fd::delegateN of Portable Syntax

  //

 

  // ======================================================================

  //

  // fd::make_delegate()

  //

  // ======================================================================

 

  // make_delegate for function

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    make_delegate(R (*fn)(T1, T2, ..., TN));

 

  // ----------------------------------------------------------------------

 

  // make_delegate for member function adapter

  templatetypename R,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T *, T2, ..., TN >

    make_delegate(T *, R (U::*mfn)(T2, ..., TN));

 

  templatetypename R,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T *, T2, ..., TN >

    make_delegate(T *, R (U::*mfn)(T2, ..., TN) const);

 

  // ----------------------------------------------------------------------

 

  // make_delegate for member function argument binding

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    make_delegate(R (U::*mfn)(T1, T2, ..., TN), T & obj);

 

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    make_delegate(R (U::*mfn)(T1, T2, ..., TN) const, T & obj);

 

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    make_delegate(R (U::*mfn)(T1, T2, ..., TN), T * obj);

 

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    make_delegate(R (U::*mfn)(T1, T2, ..., TN) const, T * obj);

 

  // ======================================================================

  //

  // fd::bind()

  //

  // ======================================================================

 

  // bind for member function argument binding

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    bind(R (U::*mfn)(T1, T2, ..., TN), T & obj, ...);

 

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    bind(R (U::*mfn)(T1, T2, ..., TN) const, T & obj, ...);

 

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    bind(R (U::*mfn)(T1, T2, ..., TN), T * obj, ...);

 

  templatetypename R,typename T1,typename T2,...,typename TN,typename U,typename T >

    delegateN< R, T1, T2, ..., TN >

    bind(R (U::*mfn)(T1, T2, ..., TN) const, T * obj, ...);

 

  // ======================================================================

  //

  // fd::get_pointer()

  //

  // ======================================================================

 

  templatetypename T >

    T * get_pointer(T * p);

 

  templatetypename T >

    T * get_pointer(std::auto_ptr< T > & p);

 

  // ----------------------------------------------------------------------

 

  namespace util

  {

    // ======================================================================

    //

    // custom memory allocators (policy driven)

    //

    // ======================================================================

 

    // fixed block memory allocator

    templatetypename T >

    class fixed_allocator;

 

    // standard memory allocator ( equivalent to std::allocator< T > )

    templatetypename T >

    class std_allocator;

 

  }  // namespace util

 

  // ----------------------------------------------------------------------

 

// namespace fd

참고문헌

  • [Hickey]. CALLBACKS IN C++ USING TEMPLATE FUNCTORS - summarizes existing callback methods and their weaknesses then describes a flexible, powerful and easy-to-use callback technique based on template functors. ('1994)
  • [Peers]. Callbacks in C++ - The article based on Rich Hickey's article to illustrate the concept and techniques used to implement callbacks
  • [Clugston]. Member Function Pointers and the Fastest Possible C++ Delegates - A comprehensive tutorial on member function pointers, and an implementation of delegates that generates only two ASM opcodes!
  • [Ryazanov]. The Impossibly Fast C++ Delegates - A implementation of a delegate library which can work faster than "the Fastest Possible C++ Delegates" and is completely compatible with the C++ Standard.
  • [Trunov]. Yet Another Generalized Functors Implementation in C++ - An article on generalized functors implementation in C++. Generalized functor requirements, existing implementation problems and disadvantages are considered. Several new ideas and problem solutions together with the compete implementation are suggested.
  • [boost]. "... One of the most highly regarded and expertly designed C++ library" boost::functionboost::bindboost::mem_fn.

(End.)

신고
Posted by 우엉 여왕님!! ghostkyow

티스토리 툴바