'함수 객체(Functor)'를 유용성을 알아보기 위해서는 먼저 '함수 포인터를 이용한 콜백 메커니즘'을 알아봐야합니다.

 

여기서 '콜백 메커니즘'이 뭘까요?

'콜백 메커니즘'의 개념을 설명하기 위해선 '서버 코드'와 '클라이언트 코드'의 개념이 필요합니다.

서버 코드: 기능이나 서비스를 제공하는 코드

클라이언트 코드: '서버 코드'가 제공해주는 기능이나 서비스를 사용하는 코드

간단한 예제를 살펴보겠습니다.d

// 서버 코드
void Print() {
    cout << "Server Code!" << endl;
}

// 클라이언트 코드
void main() {
    Print();
}

위의 예제에서 Print() 함수는 출력 기능을 제공하는 '서버 코드'입니다. main() 함수는 Print() 함수를 호출해 출력 기능을 사용하는 '클라이언트 코드'가 됩니다.

 

위의 예처럼 일반적으로 '클라이언트 코드'에서 '서버 코드'를 호출하고 기능을 사용하지만, 반대로 '서버 코드'에서 '클라이언트 코드'를 호출하는 형태도 가능합니다. '클라이언트 코드'에서 '서버 코드'를 호출하는 것을 '콜(call)'이라 하며, 반대로 '서버 코드'에서 '클라이언트 코드'를 호출하는 것을 '콜백(callback)'이라고 합니다.

// 서버 코드
void Print() {
    cout << "Server Code!" << endl;
    Client(); // '서버 코드'에서 '클라이언트 코드'를 호출합니다.
}

// 클라이언트 코드
void Client() {
    cout << "Client Code!" << endl;
}

void main() {
    Print(); // '서버 코드'를 호출합니다.
}

위의 예제 코드는 단순히 설명을 돕기 위한 것으로 실제로 '서버 코드'에서 '클라이언트 코드'를 직접적으로 호출하는 것은 불가능합니다. 왜냐하면, '서버 코드'는 기능을 제공해주는 코드이고 '클라이언트 코드'는 서버에서 제공해주는 기능을 사용하는 코드인데, '서버 코드'에서는 클라이언트가 어떤 식으로 사용할지 미리 예측할 수 없기 때문입니다.

 

그렇다면 '서버 코드' 입장에서 자신이 제공해주는 기능을 '클라이언트 코드'가 어떤 식으로 사용할지 신경쓰지 않게 하려면 어떻게 해야 할까요? 여기에 대한 답이 바로 우리가 알아보고자하는 '함수 포인터를 이용한 콜백 메커니즘'입니다.

(콜백 메커니즘을 구현하는데 사용하는 방법은 '함수 포인터' 외에도 '함수 객체', '대리자', '전략 패턴' 등이 있습니다.

// 서버 코드
// 배열의 모든 원소에 반복적인 작업을 수행하도록 추상화합니다.
void For_each(int* begin, int* end, void(*pf)(int)) {
    while (begin != end)
    {
        pf(*begin++); // '서버 코드'에서 '클라이언트 코드'를 호출합니다. (콜백)
    }
}

// 클라이언트 코드1
void Print1(int n) {
    cout << n << endl;
}

// 클라이언트 코드2
void Print2(int n) {
    cout << n + n << endl;
}

// 클라이언트 코드3
void Print3(int n) {
    cout << n * n << endl;
}

void main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    For_each(arr, arr+5, Print1);  // Print1() 함수의 주소를 전달합니다.
    For_each(arr, arr+5, Print2);  // Print2() 함수의 주소를 전달합니다.
    For_each(arr, arr+5, Print3);  // Print3() 함수의 주소를 전달합니다.
}

지금까지 '함수 포인터를 이용한 콜백 메커니즘'을 알아보았고, 이제 '함수 객체(Functor)'를 살펴보도록 하겠습니다.

 

'함수 객체'는 함수처럼 호출 가능한 클래스 객체입니다. 클래스 객체이기 때문에 클래스가 가지는 장점을 모두 이용하면서 함수처럼 사용할 수 있습니다. 함수처럼 사용할 수 있다는 말은 지금까지 살펴봤던 '함수 포인터를 이용한 콜백 메커니즘'에서 클래스 객체를 함수 포인터로 넘길 수 있게 된다는 말입니다. 이게 정말 중요한 포인트입니다.

그리고 '함수 객체'는 'Functor'라고 많이들 부릅니다. (Function Object의 약자)

 

간단한 예제를 살펴보겠습니다.

struct Functor {
    void operator()() {
        cout << "Functor!" << endl;
    }
}

void main() {
    Functor functor;
    functor(); // 해석하면 operator()()이란 '멤버 함수'를 호출합니다.
}

 

당연히 매개변수를 가질 수도 있습니다.

struct Functor {
    void operator()(int a, int b) {
        cout << "Functor!" << endl;
        cout << a << ", " << b << endl;
    }
}

void main() {
    Functor functor;
    functor(5, 10); // 매개변수를 가지고 호출합니다.
}

 

더 나아가 '함수 객체(Functor)'의 장점을 조금 더 끌어올려보겠습니다.

class Adder {
private:
    int total;
    
public:
    explicit Adder(int n = 0) : total{n} { /* do somthing */ }
    
    ~Adder() { /* do somthing */ }
    
    int operator()(int n)
    {
        return total += n;
    }
}

void main() {
    Adder add{0};
    
    cout << add(10) << endl;
    cout << add(20) << endl;
    cout << add(30) << endl;
}

클래스 객체이기 때문에 생성자, 소멸자, 멤버 변수, 멤버 함수를 가지면서 정작 사용은 함수처럼 합니다.

이전에 살펴봤던 '함수 포인터를 이용한 콜백 메커니즘'과 더해서 생각해보면, '클라이언트 코드'측에선 훨씬 많은 정보를 '서버 코드'로 넘겨줄 수 있게되고, '서버 코드'측에선 훨씬 디테일하게 기능을 제공해줄 수 있게됩니다.

 

하지만 가만히 생각해보면 '클래스'는 '사용자 정의 타입'입니다. 클래스마다 타입이 다르다는 얘기인데, 어떻게 함수처럼 일반화시켜 값을 넘겨줄 수 있는걸까요?

답은 '추상화'입니다. C++에서는 '템플릿(Template)'을 이용해 '추상화'시켜 코드를 일반화합니다.