Post List

2017/12/25

C++__함수 포인터와 함수 객체

이번에는 함수 포인터와 함수 객체에 대해 알아보겠습니다.

함수포인터란?
- 함수의 이름은 메모리상에 존재하는 함수의 위치를 가리키는 포인터입니다. 즉 함수의 이름은 메모리의 주소값을 나타내는데요, 그래서 함수의 이름과 동일한 타입의 포인터 변수를 선언해서 함수를 저장할 수 있습니다.

먼저, 함수포인터를 어떻게 사용하는지 한번 살펴보겠습니다.

1
2
3
4
5
6
int function(int a, int b)
{
    int add = a + b;
    return add;
}
cs
위 코드와 같은 함수가 있다면 함수 포인터를 선언할 때는 다음과 같이 선언합니다.

1
    int(*functionPointer)(intint= function;
cs
  [반환타입] [*함수포인터 이름] [인자타입] = [함수이름];

함수포인터의 사용은 위 구문처럼 사용하실 수 있습니다.

1
    cout << functionPointer(23<< endl;
cs
함수포인터 이름으로 인자값을 넘겨주고 반환되는 값을 콘솔창에 출력해보면 
함수의 구현대로 2 + 3 연산으로 5를 반환하게 되어 5의 값을 출력합니다.

제가 함수 포인터를 사용했던 몇가지 예를 들자면,
하나는 쓰레드 생성 함수인 _beginthreadex입니다.

1
2
3
4
5
6
7
8
_ACRTIMP uintptr_t __cdecl _beginthreadex(
    _In_opt_  void*                    _Security,
    _In_      unsigned                 _StackSize,
    _In_      _beginthreadex_proc_type _StartAddress,
    _In_opt_  void*                    _ArgList,
    _In_      unsigned                 _InitFlag,
    _Out_opt_ unsigned*                _ThrdAddr
    );
cs
함수로 전달할 수 있는 인자는 총 6개 입니다. 그 중 3번째 인자가 함수포인터로, 쓰레드가 처리할 함수의 포인터값을 전달하는데요. 쓰레드 처리 함수 같은 경우 사용자가 어떤 함수를 스레드 함수로 사용할 지 알 수 없기 때문에 미리 함수의 원형을 설정해두고 함수명만을 등록하도록 하는 형태를 취하게 됩니다. 

_beginthreadex_proc_type을 타고 들어가면
1
typedef unsigned (__stdcall* _beginthreadex_proc_type)(void*);
cs
요렇게 함수 포인터가 typedef에 의해 원형이 구현되어 있습니다.

1
2
3
4
5
6
7
unsigned __stdcall ThreadFunc(void* arg)
{
    for(int i = 0 ; i < 1000++i)
    {
        g_TotalCount++;
    }
}
cs
인자로 전달할 수 있는 함수포인터의 규격을 맞춰 아주 간단하게 1000번의 루프를 돌면서 1을 증가하는 함수를 만들었습니다. _beginthreadex()함수의 세번째 인자로 전달을 해주게 되면쓰레드는 ThreadFunc의 함수를 수행하게 됩니다
(함수포인터의 사용 방법이 목적이기때문에 동기화 방법 등은 생략하였습니다. )

두번째로는 STL의 sort함수에서도 사용자정의로 정렬할 함수를 포인터로 인자값으로 받는데요, int를 담는 list 변수를 하나 추가하여 정렬함수를 만들어 적용해보겠습니다.

1
2
3
4
5
6
7
bool AscendingSort(int a, int b)
{
    if (a < b)
        return true;
    return false;
}
cs
오름차순으로 정렬이 되는 아주 간단한 정렬 함수입니다.

1
2
3
4
5
6
7
8
9
int main()
{
    list<int> listInt;
    listInt.push_back(1);
    listInt.push_back(5);
    listInt.push_back(3);
    listInt.push_back(9);
    listInt.push_back(2);
cs
int타입을 받는 list 컨테이너 listInt 변수를 추가해줍니다. 그리고 정수형 데이터를 마구잡이로 push_back 해줍니다.

1
    listInt.sort(AscendingSort);
c

데이터 삽입이 끝났으면 이제 정렬을 해보겠습니다. sort함수를 호출해 인자로 아까 위에서 추가했던 소팅함수의 이름을 넘겨줍니다. 함수의 이름자체가 함수가 위치한 메모리 주소라고 하였지요.



그러면 데이터가 0번째 노드 부터 오름차순으로 정렬된것을 확인할 수 있습니다. 
이렇듯 간단한 정렬에 사용할 수 도 있고, 객체의 특정 데이터값을 기준으로 정렬을 할때에도 사용자 정의 함수를 만들어서 정렬을 할 수 있겠지요.

이처럼 함수포인터를 사용하게 되면 원형이 동일한 경우 포인터를 이용해서 여러개의 함수에 접근할 수 있어,
간결한 코드 구현이 가능하며, 상황에 따라 다양한 함수를 받을 수 있어 확장성에서나 다형성면에서 
큰 힘을 발휘할 수 있겠죠.


이번에는 함수객체에 대해 알아보겠습니다.
함수 객체(Function Object)란, 쉽게 설명하자면 객체가 함수처럼 동작하는 것을 말합니다. 객체가 함수처럼 동작하기 위해서 ()에 대한 연산자 오버로딩이 정의되어있어야 합니다. 그리고 멤버들을 추가로 가질 수 있다는 점도 있습니다.
연산중 필요한 변수가 있다면 멤버로 만들어 둘 수 있고, 추가로 필요한 동작이 있다면 멤버함수도 가질 수 있습니다.

또한 속도도 일반 함수보다 함수 객체가 빠르며, 함수 포인터는 인라인 될 수 없지만, 함수 객체는 인라인 될 수 있고 컴파일러가 쉽게 최적화 할 수 있답니다.

예전에 함수 객체를 사용하였던 방법을 소개하면서 어떻게 사용되는지에 대해 간단히 소개하겠습니다.
저는 이펙트 관련 정보를 담고있는 STL컨테이너인 List에서 찾고자 하는 이펙트 정보를 얻어오기 위해 함수객체를 사용했었습니다.

1
2
3
4
5
6
struct EffectInfo
{
    int type;
    int delay;
    int value;
};
cs
이펙트 관련 데이터를 가지는 EffectInfo 구조체와, 

1
list<EffectInfo> listEffectInfo;
cs
현재 플레이어에게 적용된 이펙트정보들을 관리하는 컨테이너가 있습니다.

listEffectInfo에서 type으로 이펙트 정보를 얻어오기 위해선 EffectInfo의 type을 비교해야합니다.
어떤 데이터를 찾기 위해서 많이 보셨을 만한 함수로 find()와 find_if()가 있으실겁니다. 여기서 둘의 차이점은 find는 값으로 찾는다는 점과 find_if는 함수로 찾는데에 차이가 있습니다.

저는 값이 아닌 type을 비교하여 원하는 이펙트 정보를 찾을 수 있도록 함수객체를 만들어 찾을것이기 때문에 find_if로
이펙트 정보를 얻어올것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct EffectInfoFinder
{
public:
    EffectInfoFinder(int type) : m_type(type) {}
    bool operator()(const EffectInfo& rinfo)
    {
        if(rinfo.type == m_type)
            return true;
        return false;
    }
private:
    int m_type;
};
cs
EffectInfoFinder는 함수객체의 타입 클래스가 되겠습니다. 생성자는 type을 받아야 함수객체가 생성될 수 있도록 생성자에 인자를 추가해 줍니다. 찾고자 하는 type데이터를 필수로 얻기 위함이죠.
그리고 함수객체로 사용하기 위한 함수는 bool operator()(EffectInfo& rInfo)가 되겠습니다.
인자로 받는 const EffectInfo& rinfo는 탐색되고있는 이펙트 정보들입니다. rinfo의 type과 함수객체가 가진 type이 동일하다면 제가 찾고자 하는 이펙트 정보를 찾은 셈이지요.
(list에 동일한 타입의 이펙트는 존재하지 않기 때문에 구현방식에 따라 예외처리 및 함수객체 구현이 다를 수 있습니다.)

이제 함수객체를 생성하여 타입을 찾아보겠습니다.

1
EffectInfoFinder effectFinder(type);
cs
찾고자 하는 타입을 생성자의 인자로 넘겨 함수객체를 생성합니다.

1
2
3
4
5
6
list<EffectInfo>::iterator fIter = find_if(sIter, eIter, effectfinder);
if(fIter != eIter)
{
  // 찾은 타입에 대한 기능 처리
}

cs
find_if는 iterator를 반환하게 됩니다. 찾지 못한경우도 iterator를 반환하나 그 값은 iterator의 end값이 되겠지요.
sIter는 listEffectInfo의 begin()이 되겠고, eIter는 listEffectInfo의 end()가 되겠습니다.
list의 begin()부터 end()까지 탐색을 하겠다는 의미이지요.

탐색이 끝나면 fIter는 찾은 노드의 값을 가지거나, 못찾을 경우 end()값을 가지게 됩니다.
값에 따라 기능 구현을 해 나아가면 되겠지요.

위 사용 예는 함수객체의 사용에 있어서 일부분이니, 기능에 맞게 구현하시면 됩니다.
함수객체의 핵심은 operator() 연산자 오버로딩입니다.

ps. 부가적으로 더 설명드리고 싶은 부분 함수포인터와 함수객체의 인라인화입니다.
아까 위에서 말씀드렸다시피 함수포인터는 인라인함수가 될 수 없고 함수객체는 가능하다고 하였습니다.
그 이유에 대해서 설명하겠습니다.

먼저 인라인함수는 컴파일 타임에 결정이 됩니다. 함수포인터는 반환형과 매개변수의 종류는 알지만
정확히 어떤 함수가 호출될지는 모릅니다. 그래서 함수포인터를 사용하면 인라인함수로 치환되지 않고 런타임에
함수 호출을 하기 때문에 무의미해집니다. inline을 선언을 한다해도 무늬만 inline함수일 뿐이지요.

1
2
3
4
5
6
7
8
9
10
bool sort_increase(int a, int b);
bool sort_decrease(int a, int b);
 
void SortInt(int a, int b, bool(*sort)(int,int))
{
    if( sort(a, b) )
    {
        //생략
    }
}
cs
SortInt함수의 인자로 받는 bool(*sort)(int, int) 같은 시크니쳐의 함수를 매핑해야합니다.
하지만 sort는 sort_increase함수가 매핑될지 sort_decrease함수가 매핑될지 컴파일 타임에서는 예측이 불가능하지요. 그래서 아무리 두 함수가 인라인함수 조건에 충족되더라도 일반함수로 호출된답니다.

하지만, 함수객체는 같은 시그니쳐의 함수라도 구별이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class sort_increase
{
    bool operator()(int a, int b){ return a > b; }
}
 
class sort_decrease
{
    bool operator()(int a, int b){ return a < b; }
}
 
void SortInt(int a, int b, sort_increase sort)
{
    if( sort(a, b) )
    {
        //생략
    }
}
cs
SortInt함수에서 인자로 받는 sort객체는 sort_increase 타입으로 컴파일 타임에서 식별이 가능하기때문에
인라인 함수로 사용이 가능합니다.

저번시간에 인라인에 대해 소개할때도 말씀드렸다시피 인라인의 사용이 많아지면 코드의 길이가 길어져
실행파일의 크기가 증가한다고 했습니다. 이 또한 고려해서 사용해야합니다.
하지만 C++11에서 인라인함수와 기능이 비슷한 '람다(Lamda)'라는 새로운 문법이 있으니, C++11을 사용해 구현하신다면 람다에 대해 알아보시고 사용하시는것도 추천해드립니다.