logo

English

이곳의 프로그래밍관련 정보와 소스는 마음대로 활용하셔도 좋습니다. 다만 쓰시기 전에 통보 정도는 해주시는 것이 예의 일것 같습니다. 질문이나 오류 수정은 siseong@gmail.com 으로 주세요. 감사합니다.

Callback in C++ 와 Delegate 차이점

by digipine posted Nov 01, 2017
?

Shortcut

PrevPrev Article

NextNext Article

Larger Font Smaller Font Up Down Go comment Print
?

Shortcut

PrevPrev Article

NextNext Article

Larger Font Smaller Font Up Down Go comment Print

d play();

  void stop();

};

 

class Button

{

public:

  typedef void (RootObject::*Callback)();

 

  Button(Callback callback, RootObject * obj)

    : callback_(callback), obj_(obj)

  {

  }

 

  void click()

  {

    (obj_->*callback_)();

  }

 

private:

  Callback callback_;

  RootObject * obj_;

 

};

 

void main()

{

  CDPlayer cd;

 

  Button cd_play_button(&CDPlayer::play, &cd);

  Button cd_stop_button(&CDPlayer::stop, &cd);

 

  cd_play_button.click(); // invoke cd.play();

  cd_stop_button.click(); // invoke cd.stop();

}

 

콜백이 호출되어지는 대상 클래스는 반드시 공통 베이스 클래스로 부터 직적/간접적으로 상속받아야합니다. 즉 소스 코드의 수정이 필요합니다. 가장 큰 문제는 실전 프로그램에서는 대상 클래스(CDPlayer)가 공통 베이스 클래스(RootObject)를 직적/간접적으로 여러번 상속받게 되는 경우가 빈번히 발생할 수 있다는 사실입니다. 이런 경우 다이아몬드 형태의 상속 관계가 이루어 질 수 있고 가상 상속을 사용하지 않는한 클래스 상속관계가 모호하다는 컴파일 에러가 발생하게 됩니다. 가상 상속을 사용하여 해결한다고 할지라도 개체 간의 결합도가 상당히 높게 증가하기 때문에 최근에 권장되고 있는 설계 관점에서 매우 바람직하지 못한 상황에 봉착하게 됩니다. 아주 간단한 콜백 관계의 경우에만 제한적으로 사용할 수 있는 방법입니다.

  

2) 추상 베이스 클래스(ABC: Abstract Base Class) 또는 인터페이스(ABI: Abstract Base Interface)

 

class IButton

{

public:

  virtual void click() = 0;

};

 

class CDPlayer

{

public:

  void play();

  void stop();

};

 

template<class T>

class Button : public IButton

{

public:

  typedef void (T::*Callback)();

 

  Button(Callback callback, T * obj) : callback_(callback), obj_(obj)

  {

  }

 

  void click()

  {

    (obj_->*callback_)();

  }

 

private:

  Callback callback_;

  T * obj_;

 

};

 

void main()

{

  CDPlayer cd;

 

  Button<CDPlayer> cd_play_button(&CDPlayer::play, &cd);

  Button<CDPlayer> cd_stop_button(&CDPlayer::stop, &cd);

 

  cd_play_button.click(); // invoke cd.play();

  cd_stop_button.click(); // invoke cd.stop();

}

 

 

추상 베이스 클래스(ABC)와 인터페이스(ABI)는 의미상 약간 다른 부류이긴 하지만 콜백 관점에서 볼때는 거의 유사하다고 할 수 있습니다. 따라서 여기서는 두 가지를 동등하다고 가정하겠습니다.

 

위의 예제에서 Button<CDPlayer> 은 'CDPlayer'라는 호출이 되어지는 대상 객체를 알고 있어야 한다는 것을 명백하게 (템플릿 파라미터로 주어짐) 보여줍니다. 따라서 이는 함수를 호출하는 클래스(Button<CDPlayer>)가 함수흘 호출 받는 클래스(CDPlayer)에 높은 결합도를 갖는 것을 의미합니다.

 

또한 Button<CDPlayer> 클래스는 추상 베이스 클래스 IButton 을 반드시 상속 받아야 합니다. 즉 기존의 코드가 특정한 클래스를 상속받도록 수정되어야 한다는 것을 의미합니다.

 

이 경우 입력으로 주어지는 템플릿 파라미터가 다른 경우, 즉 Button<X> 와 Button<Y>는 전혀 다른 객체로 간주되기 때문에 서로 값을 비교하거나 대입하는 것이 불가능합니다. 즉 값에 의한 (by-value) 연산이 불가능하다는 것을 의미하며 이것은 Button<X> 개체를 범용적인 방법으로는 접근이 불가능 하다는 것을 의미하며 이는 Button<T>를 STL 컨테이너에 직접 대입하여 사용하는 것이 불가능하게 만듭니다. 굳이 Button<X>의 리스트를 사용하고자 하는 경우라면 서로 다른 타입의 객체를 저장할 수 있도록 특별히 설계된 커스텀 리스트를 제작해야합니다.

 

따라서 play_button을 클릭하였을 때 단순히 CDPlayer 만이 아닌 DVDPlayer, TV 등등 서로 다른 객체를 동시에 play 하고자 하는 경우(multicast)에 ABC 또는 ABI를 이용한 위의 방법은 구현하기가 매우 어려워진다는 문제점을 내포하게 됩니다.

 

마지막으로 ABC 또는 ABI는 가상 함수의 오버헤드를 가집니다. 인터페이스가 한 두 개 정의되어 있고 각각의 인터페이스에 가상 멤버 함수 역시 한 두개만 정의되어 있는 경우라면 문제가 되지 않겠지만 매우 복잡한 여러개의 인터페이스가 주어지고 각각의 인터페이스 또한 많은 수의 멤버함수를 정의하고 있는 경우라면 가상 함수에 의한 오버헤드가 프로그램 성능에 영향을 줄 수 도 있습니다.

 

 

3) 콜리 믹스인 클래스 (Callee Mix-In)

 

 class Button

{

public:

  class Notifiable

  {

  public:

    virtual void notify() = 0;

  };

 

  Button(Notifiable * callee) : callee_(callee)

  {

  }

 

  void click()

  {

    callee_->notify();

  }

 

private:

  Notifiable * callee_;

 

};

 

class CDPlayer

{

public:

  void play();

  void stop();

};

 

class MyCDPlayer : public CDPlayer, public Button::Notifiable

{

public:

  void notify()

  {

    play();

  }

};

 

void main()

{

  MyCDPlayer cd;

 

  Button cd_play_button(&cd);

 

  button.click();  // invoke cd.play();

}

 

위의 방법은 타입 안정하며 Button 과 CDPlayer의 결합도가 낮지만 (Button의 객체를 정의할 때 CDPlayer의 타입을 템플릿 인자로 제공할 필요가 없습니다.) 실전에서 실질적으로 사용하는데는 많은 문제를 가지고 있습니다.

 

첫 번째 문제는 콜백을 호출하는 Button의 결합도는 낮췄지만 콜백이 호출되는 MyCDPlayer의 겹할도가 다중 상속으로 인해서 높아졌습니다. 만약 복잡한 관계의 여러개의 콜백을 사용하고자 할 경우 MyCDPlayer은 더욱 복잡한 다중 상속 관계를 가지게 되고 결합도 역시 매우 높아지게 됩니다.

 

두 번째 문제는 만약 CDPlayer 클래스가 소스 레벨에서 접근이 불가능하다면 CDPlayer로 부터 상속하는 것이 불가능해지기 때문에 이 방법은 전혀 사용할 수 없게 됩니다. 믹스인 클래스 접근 방법의 가장 큰 문제점이라고 할 수 있습니다.

 

마지막으로 위의 예제에서 이미 드러났듯이 Button이 cd.play()를 호출하는 것은 가능하지만 cd.stop()을 호출하는 것은 불가능합니다. 즉 한가지 notify 만을 호출 하는 것이 가능합니다. cd.stop()을 호출하고자 하는 경우 Button2 와 같이 또 다른 별개의 클래스를 작성해야 한다는 문제점을 가지게 됩니다.

 

4) delegate

 

class CDPlayer

{

public:

  void play();

  void stop();

};

 

class Button

{

public:

 

  typedef delegate0<void> Callback;

 

  Button(Callback callback) : callback_(callback)

  {

  }

 

  void click()

  {

    callback_();

  }

 

private:

  CallBack callback_;

 

};

 

void main()

{

  CDPlayer cd;

 

  Button cd_play_button(make_delegate(&CDPlayer::play, &cd));

  Button cd_stop_button(make_delegate(&CDPlayer::stop, &cd));

 

  cd_play_button.click(); // invoke cd.play();

  cd_stop_button.click(); // invoke cd.stop();

}

 

delegate를 사용한 콜백은 앞서 언급되어진 기존의 C++ 콜백들의 단점들이 모두 해결된 좀더 진화된 C++ 콜백 메카니즘이라고 할 수 있습니다. 복잡한 콜백 관계인 경우에도 쉽게 콜백의 추가/수정이 용이하며 범용적인 특성으로 인해 기존의 STL 컨테이너나 알고리즘을 이용하여 복잡한 콜백 관계라도 쉽게 구현할 수 있습니다. 콜백을 호출하는 클래스(Caller, Button)의 입장에서 콜백이 호출되는 클래스(Callee, CDPlayer)에 대한 어떠한 타입 정보를 필요로 하지 않으며 이러한 클래스들이 특별한 상속관계를 가지도록 요구하지도 않기 때문에 개체의 결합도 역시 매우 낮습니다.

 

delegate 이전에 가장 빈번히 사용되어졌던 그리고 현재도 가장 많이 사용되어지고 있는 콜백 메카니즘은 추상 베이스 클래스(인터페이스)를 이용한 방법입니다. 기본적으로 delegate로 할 수 있는 콜백은 추상 베이스 클래스(인터페이스) 방법으로 똑같이 할 수 있습니다만 위에서 이미 설명 되었듯이 추상 베이스 클래스를 사용하는 방법의 경우 개체의 결함도가 증가하며 이러한 콜백 메카니즘이 제대로 동작할 수 있도록 지원하는 부수적인 코딩량 역시 증가하게 됩니다.

 

다음은 제가 어떤 사이트에서 본 delegate의 단점이라고 기술된 내용입니다.

 

class Department

{

public:

  typedef delegate0<void> Fireable;

 

  Department(Fireable fireable) : fireable_(fireable)

  {

  }

 

  void FireEmployee()

  {

    fireable_();

  }

 

private:

  Fireable fireable_;

 

};

 

class Military

{

public:

  typedef delegate0<void> Fireable;

 

  Military(Fireable fireable) : fireable_(fireable)

  {

  }

 

  void FireMissle()

  {

    fireable_();

  }

 

private:

  Fireable fireable_;

 

};

 

class Missle

{

public:

  void fire() { cout << "missle fired" << endl; }

};

 

class Employee

{

public:

  void fire() { cout << "employee fired" << endl; }

};

 

void main()

{

  Employee e1;

  Missle e2;

 

  // Department department(make_delegate(&e1.fire, &e1));

  Department department(make_delegate(&e2.fire, &e2));

 

  department.FireEmployee();

}

 

- Output -

missle fired

 

위의 코드는 main()에서 실수로 e1 대신에 e2를 Department에 입력으로 주었다고 가정했을 때 컴파일러가 에러(?)를 발견하지 못합니다. 따라서 실재로는 부서에서 고용인을 해고하려고 하였는데 결과는 미사일이 발사됩니다. 이 예제를 제시한 사이트에서는 이러한 delegate의 특징을 타입 안정하지 못하다고 해석하였습니다. 또한 이 사이트에서는 다음과 같은 방법을 인터페이스를 사용한 방법이라고 설명하면서 delegate 방법보다 나은 대안으로써 제시하였습니다.

 

class Department

{

public:

  class IFireable

  {

  public:

    void fire() = 0;

  };

 

  void FireEmployee(IFireable * fireable)

  {

    fireable->fire();

  }

};

 

class Military : public IFireable

{

public:

  class IFireable

  {

  public:

    void fire() = 0;

  };

 

  void FireMissle(IFireable * fireable)

  {

    fireable->fire();

  }

};

 

class Missile : public Military::IFireable

{

public:

  void Fire() { cout << "missle fired" << endl;

 

};

 

class Employee : public Department::IFireable

{

public:

  void Fire() { cout << "employee fired" << endl;

 

};

 

void main()

{

  Employee e1;

  Missile e2;

 

  Department department;

 

  // department.fire(e1);

  department.fire(e2);  // compile error!

}

 

일단 위의 방법은 콜리 믹스인 클래스로 분류된 방법과 동일하다고 볼 수 있습니다. 따라서 위에서 이미 설명되어진 단점들을 그대로 내포하고 있습니다. 제 개인적인 견해로는 delegate의 위와 같은 특징은 오히려 개체의 결합도를 낮추는 바람직한 효과라고 생각되어지는데 오히려 이 특징을 타입 안정하지 못하다고 해석할 수도 있다는 것을 알았습니다. 실재로 위의 사이트는 C# delegate FAQ 사이트로써 C++ delegate 경우에는 위와 같은 실수를 할 가능성이 훨씬 적습니다.

 

main() 함수에서 다음의 코드는 (C#의 원래 코드와 유사하게 일부러 조작한 코드입니다.)

 

// Department department(make_delegate(&e1.fire, &e1));

Department department(make_delegate(&e2.fire, &e2));

 

실재로 C++ 에서는 위의 코드 보다는 오히려 다음과 같은 코드를 사용하기 때문입니다.

 

// Department department(make_delegate(&Employee::fire, &e1));

Department department(make_delegate(&Missle::fire, &e2));

 

이러한 상황에서 컴파일러가 에러를 발생시켜주지는 않지만 비교적 명백하게 로직 에러가 발생한다는 사실을 인지할 수 있습니다.

 

마지막으로 delegate 가 진정한 C++ 콜백이라는 문구를 보시고서 delegate를 사용하고자 하였다가 좌절하시는 경우는 대부분 C 스타일 콜백만을 받는 Window API 에 delegate를 사용하고자  했는데 이것이 불가능하기 때문이라고 알고 있습니다. C 스타일 콜백을 받도록 설계되어진 Window API에 delegate 를 입력으로 주는 것은 기본적으로 가능하지 않습니다. 그러나 스마트 포인터 기법에서 응용되어지는 프락시 임시 객체를 이용한 기법을 응용하면서 TLS를 이용하여 제한적으로 delegate를  C 스타일 콜백으로 전달해주는 방법을 구상하고 있는 중입니다.

 

class delegate_adapter1

{

public:

  typedef void (stdcall__ * Callback)();

 

  template<class T>

  class ThreadLocalStorageManager

  {

  public:

    ThreadLocalStorageManager() : dwTLS_(::TlsAlloc())

    {

    }

 

    ~ThreadLocalStorageManager()

    {

      ::TlsFree(dwTLS_);

    }

 

    T * get_tls_obj()

    {

      return static_cast<T *>(::TlsGetValue(dwTLS_));

    }

 

    BOOL set_tls_obj(T * obj)

    {

      return ::TlsSetValue(dwTLS_, obj);

    }

 

  private:

    DWORD dwTLS_; 

 

  };

 

  // singleton

  static ThreadLocalStorageManager<delegate_adapter1> tls_man_;

 

  class proxy

  {

  public:

    proxy(delegate_adapter1 * dga)

    {

      tls_man_.set_tls_obj(dga_);

    }

 

    ~proxy()

    {

      tls_man_.set_tls_obj(0);

    }

 

    operator Callback ()

    {

      return &static_callback;

    }

 

  };

 

  friend class proxy;

 

  explicit delegate_adapter1(delegate1<voidconst & dg) : dg_(dg)

  {

  }

 

  proxy get_c_style_callback()

  {

    return proxy(this);

  }

 

  static void stdcall_ static_callback();

  {

    tls_man_.get_obj()->dg_();

  }

 

private:

  delegate1<void> dg_;

 

};

 

ThreadLocalStorageManager<delegate_adapter1> delegate_adapter1::tls_man_;

 

typedef void (stdcall__ * Win32Callback)();

 

void SomeWin32API(Win32Callback callback);

 

class CDPlayer

{

public:

  void play();

  void stop();

 

};

 

void main()

{

  CDPlayer cd;

 

  delegate1<void> dg_play(&CDPlayer::play, &cd);

  delegate1<void> dg_stop(&CDPlayer::stop, &cd);

 

  SomeWin32API(delegate_adapter1(dg_play).get_c_style_callback());

  SomeWin32API(delegate_adapter1(dg_stop).get_c_style_callback());

}

 

TLS(Thread Local Storage)를 사용하기 때문에 멀티쓰레드 환경에서도 동기화 문제를 따로 고려해줄 필요가 없지만 비동기호출을 하는 콜백의 경우에는 사용할 수 없습니다. proxy 임시 변수는 get_c_style_callback()이 호출되는 라인에서만 유효하기 때문입니다. 하지만 대부분의 일반적인 콜백의 경우에서는 별도의 코드 수정없이 delegate 를 쉽게(그리고 자동으로) C 스타일 콜백으로 변경할 수 있습니다. 아직 완전하게 구현되지 않은 상태지만 위와 같은 형태의 어뎁터 클래스를 포함하고 몇 가지 기능이 수정/추가된 fast delegate의 다음 버전을 준비하고 있는 중입니다.

 

 
TAG •
?

List of Articles
No. Subject Author Date Views
20 C++ 컴파일 오류(error): variable 'std::istringstream sstream' has initializer but incomplete type digipine 2017.11.02 9
19 fopen 파일 열기 모드 옵션 정리 digipine 2017.11.02 8
18 make -j 옵션으로 컴파일 속도 최적화 하기 digipine 2017.11.01 2
17 [Linux] Pthread 사용법, Thread 동기화 총정리 digipine 2017.11.01 3
16 [linux] zlib build 방법 digipine 2017.11.01 2
15 Linux C 언어로 Shell 명령어 실행하기 digipine 2017.11.01 3
14 Introduce to Singly-linked List file digipine 2017.11.01 2
13 [shared lib] so 동적 라이브러리 만들기와 사용법 - 리눅스 digipine 2017.11.01 2
12 Unix C/C++ Input and Output Function Reference digipine 2017.11.01 2
11 wchar_t에 대하여 digipine 2017.11.01 3
10 C 에서 Overloading 구현 digipine 2017.11.01 4
9 C를 이용한 객체지향 프로그래밍 digipine 2017.11.01 2
» Callback in C++ 와 Delegate 차이점 digipine 2017.11.01 2
7 Solaris에서 pmap을 이용하여 백그라운드 프로세스 메모리 크기 구하기 digipine 2017.10.29 2
6 Solaris 10에 개발 Tool (gcc,vim,gdb) 설치 digipine 2017.10.29 2
5 brute-force 알고리즘을 이용한 패턴 위치 찾기 digipine 2017.10.29 3
4 MD5 파일 변조 검사 관련 소스 (리눅스/Windows) digipine 2017.10.29 2
3 STL MAP 예제로 공부하기 digipine 2017.10.29 5
2 C++에서 extern의 역할, 기능 digipine 2017.10.29 4
1 [C/C++] 현재시간 구하기 digipine 2017.10.28 3
Board Pagination Prev 1 Next
/ 1