본문 바로가기
NC University/Design Pattern with C++

1일차 - thiscall

by 날쑤 2016. 5. 19.

Intro

  예전 Advanced C++ 수업에서 일반 함수 포인터와 (클래스의) 멤버 함수 포인터의 차이점, 그리고 멤버 함수의 호출원리에 대해 배웠었다. 자세한 것은 예전 정리글을 참고하고, 여기서는 이것이 클래스 디자인에 어떻게 영향을 줄 수 있는지를 중점적으로 정리한다.

Review - member function pointer

  멤버 함수 포인터와 관련된 내용을 간단히 요약하면 다음과 같다.

  비정적(non-static) 멤버 함수는 실제로는 this 포인터를 첫번째 인자로 받기때문에 일반적인 함수 포인터로는 멤버 함수의 주소를 담을 수 없다는 것이었다. 반면에 정적(static) 멤버 함수는 this 포인터를 인자로 가지지 않기 때문에 일반 함수 포인터에 주소를 담는 것이 가능하다.

class Dialog 
{
public:
    void Close () {}  
    static void DoSomething () {}
};

int main (int argc, char* argv[])
{
    void(Dialog::*f1)() = &Dialog::Close;
    void(*f2)() = &Dialog::DoSomething;
    
    Dialog dlg; (dlg.*f1)();
    f2();
    
    return 0;
}

Example - 멀티 스레드 개념의 캡슐화

  Windows 시스템 프로그래밍에서 스레드를 생성하고 그 스레드에서 특정 함수를 수행하게 하려면 다음과 같이 코드를 작성한다. 주의해야 할 점은 CreateThread 함수의 3번째 인자로 전달하는 스레드 함수는 반드시 void형 포인터 하나만을 인자로 가져야한다는 것이며, 이 인자값은 CreateThread 함수의 4번째 인자로 전달할 수 있다.

#include <iostream>
#include <Windows.h>

using namespace std;

// Thread function
DWORD __stdcall ThreadFunc (void* p)
{
    DWORD ret = 0L;
    cout << "start ThreadFunc with p = " << p << endl;
    
    // Do something...
    
    return ret;
}

int main (int argc, char* argv[])
{
    HANDLE hThread = CreateThread(nullptr, 0, &ThreadFunc, "Argument", 0, nullptr);
    WaitForSingleObject(hThread, INFINITE);
    
    return 0;
}

  이제 위의 코드를 클래스를 이용해서 캡슐화시켜보자. 일단은 다음과 같은 형태의 코드를 생각할 수 있을 것이다.

#include <iostream>
#include <Windows.h>

using namespace std;

// 아래 클래스가 라이브러리 내부의 클래스라고 가정
class Thread 
{
public:
    HANDLE Run() 
    {
        return CreateThread(nullptr, 0, &ThreadFunc, "Argument", 0, nullptr);
    }

protected:
    virtual void Main () {} 

private:
    // DWORD __stdcall ThreadFunc (Thread* const this, void* p)
    DWORD __stdcall ThreadFunc (void* p)
    {
        Main();  //< 가상함수 호출, Main(this)
        return 0L;
    }
}; 

// 아래 클래스가 라이브러리 사용자 클래스임
class MyThread : public Thread 
{
protected:
    void Main () override { cout << "MyThread Main" << endl; }
};

int main (int argc, char* argv[])
{
    MyThread t;
    HANDLE hThread = t.Run();  //< 이 순간 스레드가 생성된 후 Main()을 실행해야 함
    
    WaitForSingleObject(hThread, INFINITE);
    
    return 0;
}

  위의 코드는 컴파일하면 오류가 발생한다. 그 이유는 주의할 점에서 언급했듯이 CreateThread 함수로 넘기는 함수는 void형 포인터 하나만을 인자로 갖는 함수여야 하는데, 위의 코드에서 넘기는 ThreadFunc 함수는 멤버 함수이므로 this 포인터를 첫번째 인자로 포함하기 때문이다. 이 문제를 해결하기 위해 ThreadFunc을 외부로 빼서 일반 함수로 만들 수도 있지만 이는 스레드를 캡슐화한다는 본래의 목적에 반한다. 그러므로 남은 선택지는 ThreadFunc을 정적 멤버 함수로 선언하는 것이다.

  그러나 정적 멤버 함수에서는 일반 멤버 변수 및 함수에 접근할 수 없기 때문에 ThreadFunc 내부에서 Main() 함수를 호출할 수 없다는 문제가 남게 된다. 그래서 추가적으로 사용하는 방법이 아래의 코드와 같이 this 포인터를 스레드 함수의 인자로 전달하는 것이다.

class Thread 
{
public:
    HANDLE Run()
    {
        return CreateThread(nullptr, 0, &ThreadFunc, this, 0, nullptr); 
    }

protected:  
    virtual void Main () {} 

private:
    // ThreadFunc은 this 포인터를 인자로 포함하지 않는다.
    // 대신 인자로 받은 p가 실제로는 this 포인터이다.
    static DWORD __stdcall ThreadFunc (void* p)
    {
        Thread* self = static_cast<Thread*>(p);
        self->Main();  //< Main(self)    
        return 0L;
    } 
};

Example - Clock (Timer)

  이번에는 강좌에서 제공되는 라이브러리를 이용해서 일정 주기마다 지정된 callback 함수를 호출하는 프로그램을 작성해보자. 아래의 코드에서 IoSetTimer 함수는 이러한 일을 하는 함수인데, 두번째 인자로 받는 callback 함수는 int형의 id값 하나만을 인자로 가져야 한다.

#include <iostream>
#include "ioacademy.h"

using namespace std;
using namespace ioacademy;

void Foo (int id) { cout << "Foo (id: " << id << ")" << endl; }

int main (int argc, char* argv[])
{
  int n1 = IoSetTimer(500, Foo);
  int n2 = IoSetTimer(1000, Foo);

  IoProcessMessage(); // 메시지 루프를 이용

  return 0;
}

  위 코드를 C++의 클래스로 랩핑하는 것은 앞의 예제와 비슷하지만 약간의 차이가 있다. 이전과 동일한 이유로 콜백 함수는 정적 멤버 함수로 선언되어야 하는데, 이번에는 this 포인터를 함수로 직접 전달하기가 어렵다. 콜백 함수의 타입이 정수형의 id값 하나만을 매개변수로 가지도록 정의되어 있기 때문이다.

  이러한 경우에는 클래스 외부(혹은 정적 멤버로)에 해당 타입 객체의 포인터를 저장하는 컨테이너를 두고, 콜백 함수에서는 이 컨테이너를 통해서 this 포인터에 접근하는 방법을 사용할 수 있다. 특히 이번 예제와 같이 객체에 대한 유일한 키 값이 정의되는 경우에는 std::map이 대상 컨테이너로서 적합하다. 앞에서 언급한 내용을 바탕으로 Clock 클래스를 구현해보자.

#include <iostream>
#include <map>
#include <string>

#include "ioacademy.h"

using namespace std;
using namespace ioacademy;

class Clock 
{
public:
  Clock (const string& name) : name_(name) {}
  
  void Start (int msec)
  {
    int id = IoSetTimer(msec, TimerCallback);
    clock_ptr_map_[id] = this;  //< id를 키 값으로 this 포인터를 map에 보관
  }
    
  // 타이머를 통해 호출될 콜백 함수, 정수형 id값을 인자로 전달받는다.
  static void TimerCallback (int id)
  {
    Clock* self = clock_ptr_map_[id];
    cout << self->name_ << "(id: " << id << ")" << endl;
  }

private:
  string name_;
  static map<int/*id*/, Clock*> clock_ptr_map_; //< Clock 객체의 포인터를 저장할 컨테이너
};

map<int, Clock*> Clock::clock_ptr_map_;

int main (int argc, char* argv[])
{
  Clock c1("Foo"), c2("Bar");

  c1.Start(500);
  c2.Start(1000);

  IoProcessMessage();
  
  return 0;
}

Summary

  일반적으로 운영체제(Windows, Linux, Android, iOS 등)들은 대부분 C기반이다. 즉, 1차 API는 모두 C 기반이며, 함수 베이스로 동작한다. 이게 불편하기 때문에 2차 API인 C++에서는 이들을 클래스로 랩핑해서 사용하게 되는데, 이 때 콜백 함수는 인자의 개수 문제때문에 정적 멤버 함수여야 한다.

  문제는 정적 멤버 함수는 this 포인터를 직접적으로 사용할 수가 없기 때문에 this 포인터를 콜백 함수로 전달해줄 방법이 필요하다. 이번 글에서는 1) 콜백 함수로 직접 this 포인터를 전달하는 방법, 그리고 이를 적용할 수 없는 경우에는 2) this 포인터를 보관하는 컨테이너를 만들고 콜백 함수에서는 이 컨테이너를 통해서 적절한 this 포인터에 접근하는 방법을 예제들을 통해서 살펴보았다.

'NC University > Design Pattern with C++' 카테고리의 다른 글

1일차 - 추상 클래스(Abstract class)  (0) 2021.06.11
1일차 - 상속(inheritance)  (0) 2016.05.24