728x90

특징

- 함수처럼 동작하는 객체

- () 연산자 오버로딩을 통해 함수와 비슷한 형태로 호출합니다.

- 함수자(Functor) 라고도 합니다.

 

함수 포인터와 함수 객체의 차이점

- 함수 포인터는 시그니처가 일치 하지 않으면 전달할 수 없지만 함수 객체는 객체를 전달하기 때문에 상관 없다.

- 함수 포인터는 정해진 시그니처에 맞게만 호출 가능하지만 함수 객체는 ()연산자 오버로딩을 통해 다양한 시그니처로 호출될 수 있다.

- 함수 객체는 객체의 특성을 모두 갖기 때문에 상태를 저장(람다에서의 캡쳐와 동일)할 수 있다.

- 함수 객체는 인라인 함수로 호출될 수 있습니다.

 

함수 객체 간단한 구현 예제

- 함수 객체는 () 오버라이딩과 객체기 떄문에 멤버변수를 갖을 수 다는 특징이 있습니다.

#include <iostream>
using namespace std;

class Functor
{
public:
    void operator() (int data)
    {
        _data = data;
        cout << "void (void) : " << _data << endl;
    }

    int operator() (int a, int b)
    {
        cout << "int (int, int)" << endl;
        return a + b;
    }

public:
    int _data;
};

int main()
{   
    Functor functor1;
    functor1(10);

    Functor functor2;
    cout << functor2(1, 2) << endl;

    return 0;
 }

 

함수 객체만 인라인 함수 호출이 가능하다는 건 무슨 얘기일까?

함수 객체는 ()연산자 함수를 inline으로 호출되게 할수 있습니다. 이 말인 즉슨 일반 함수 포인터를 사용하게 되면 해당 함수는 inline방식으로 호출할 수 없습니다.

 

여기서 inline 함수란 뭘까요? 인라인함수를 사용하면 컴파일 타임에 코드가 매크로와 같이 치환됩니다. 따라서 함수를 호출하는 오버헤드가 없지만, 소스코드양이 증가합니다. 따라서 코드가 짧고 자주 호출되는 함수를 inline으로 지정하는 것이 효율적입니다. 하지만 요즘 컴파일러는 최적하여 inline 적용 여부를 정합니다. 이떄 __forceiline 키워드를 이용하여 컴파일러에게 inline을 강제할 수도 있긴 하지만 이 경우에도 컴파일러가 판단하기에 비효율적이면 무시될 수도 있습니다.

 

그렇다면 왜 함수 객체를 이용하면 inline 사용을 컴파일러에게 유도할 수 있는 것일까요?

이는 inlline 적용 여부가 컴파일 단계에서 결정 되기 때문에 inline 시킬 대상(코드로 치환할)이 명확하게 컴파일러가 알수 있어야 합니다.

하지만 함수 포인터를 이용한 호출의 경우 시그니처가 동일한 함수가 여러개 정의 되어 있으면 컴파일러는 어떤 코드를 치환해야 할지 알수 없습니다.

 

아래 코드 실행시 생성되는 어셈블러 코드를 확인 하면 함수포인터로 호출된 함수는 무조건 일반함수 호출을 하는 것을 알 수 있습니다.

#include <iostream>
using namespace std;

void PFunc1()
{
    cout << "PFunc1" << endl;
}

inline void PFunc2()
{
    cout << "PFunc2" << endl;
}

class Functor
{
public:
    void operator() ()
    {
        cout << "Functor()" << endl;
    }

    inline void operator() (int num)
    {
        cout << "Functor(int)" << endl;
    }

};

int main()
{
    void (*pfunc)(void);
    pfunc = &PFunc1;
    pfunc(); // 일반함수 -> 일반함수 방식으로 호출(항상)

    pfunc = &PFunc2;
    pfunc(); // 인라인함수 -> 일반함수 방식으로 호출(항상)

    Functor functor;
    functor(); // 일반함수 -> 인라인 함수로 호출(컴파일러가 판단)
    functor(1); // 인라인 함수 -> 인라인 함수로 호출(컴파일러가 판단)

}

 

함수 객체의 의미

- 일감을 객체 형태로 정의(데이터, 로직)하고 실제 일감은 다른 로직을 통해 적절한 시기에 일감이 처리 되도록 할수 있다. 이는 일감을 정의하는 시점과 일감을 처리하는 시점을 구분할 수 있다는 것을 의미(커맨더 패턴) 하는 중요하는 개념이다.

- 템플릿과 같이 사용되어 콜백 함수 구현 등에 활용 된다.

 

콜백함수 구현 예제

#include <iostream>
using namespace std;

class Item
{
public:
    int _itemId = 0;
    int _rarity = 0;
    int _ownerId = 0;
};

//함수 객체
class FindByOwnerId
{
public:
    bool operator() (const Item* item)
    {
        return (item->_ownerId == _ownerId);
    }

public:
    int _ownerId;
};

//함수 객체
class FindByRarity
{
public:
    bool operator() (const Item* item)
    {
        return (item->_rarity == _rarity);
    }

public:
    int _rarity;
};

template<typename T>
Item* FindItem(Item items[], int itemCount, T selector)
{
    for (int i = 0; i < itemCount; i++)
    {
        Item* item = &items[i];

        if (selector(item))
        {
            return item;
        }
    }

    return nullptr;
}

int main()
{       
    Item items[10];
    items[3]._ownerId = 100;
    items[8]._rarity = 2;

    FindByOwnerId functor1;
    functor1._ownerId = 100;

    FindByRarity functor2;
    functor2._rarity = 2;

    Item* item1 = FindItem(items, 10, functor1);
    Item* item2 = FindItem(items, 10, functor2);

    return 0;
 }

결론

- 함수 객체(함수자)가 일반함수포인터를 사용하는 경우보다 장점이 훨씬 많습니다.

- 인라인 함수 호출 유도, 상태 저장, 템플릿과 궁합이 잘 맞는 등..

- STL에서 함수객체를 받는 이유도 inline 함수 호출을 유도하여 라이브러리단의 기능을 최적화 하려는 것으로 볼 수 있다.


공부한 내용을 개인적으로 복습하기 쉬운 형태로 정리했습니다.

잘못된 내용이 있는 경우 언제든 댓글 혹은 이메일로 지적해주시면 감사드립니다.

 

728x90

'Programming Language > C++' 카테고리의 다른 글

using - type alias declaration  (0) 2022.02.06
auto  (0) 2022.02.06
함수 포인터  (0) 2022.02.06
2-2_입력( <<,manipulator,조정자)  (0) 2021.12.07
C++/StackPadding  (0) 2021.11.23

+ Recent posts