함수 객체 (Functor)

Jaemyeong Lee·2024년 11월 4일

게임 서버1

목록 보기
80/220

함수 객체의 핵심

펑터란?

  • 함수처럼 호출 가능한 객체입니다.
  • 클래스/구조체에서 operator()를 정의하면 객체를 obj(args...) 형태로 호출할 수 있습니다.
  • 즉, "행동(코드) + 상태(데이터)"를 한 덩어리로 다루는 방법입니다.

왜 필요한가?

  • 함수 포인터는 주소만 들고 있어서 상태를 함께 담기 어렵습니다.
  • 멤버 함수 포인터는 객체(this)를 따로 관리해야 해서 사용이 번거롭습니다.
  • 펑터는 호출에 필요한 데이터를 객체 내부에 저장할 수 있어 실전 코드가 단순해집니다.

함수 포인터 vs 펑터

항목함수 포인터펑터
상태 보유
시그니처 유연성제한적✓ (오버로드/템플릿 가능)
템플릿과 궁합제한적매우 좋음
데이터 바인딩

기본 문법과 동작

상태 없는 펑터

struct Add {
    int operator()(int a, int b) const {
        return a + b;
    }
};

Add add;
int r = add(10, 20); // 30

상태 있는 펑터

class Accumulator {
public:
    void operator()(int n) {
        sum += n;
        cout << "현재 합: " << sum << '\n';
    }
private:
    int sum = 0;
};

Accumulator acc;
acc(10); // 현재 합: 10
acc(20); // 현재 합: 30
  • 같은 객체를 반복 호출하면 내부 상태가 누적됩니다.
  • 이 "상태 유지"가 함수 포인터 대비 가장 큰 차이입니다.

행동 + 데이터 묶기

struct AddWithBias {
    int bias = 0;
    int operator()(int a, int b) const {
        return a + b + bias;
    }
};

AddWithBias op{10};
int r = op(20, 30); // 60

템플릿과의 조합 (Callable 패턴)

타입을 고정하지 않는 호출

template<typename Func>
int DoSomething(int a, int b, Func func) {
    return func(a, b);
}

int AddFree(int a, int b) { return a + b; }

struct Mul {
    int operator()(int a, int b) const { return a * b; }
};

int a = DoSomething(10, 20, AddFree); // 함수 포인터로 호출 가능
int b = DoSomething(10, 20, Mul{});   // 펑터로 호출 가능
int c = DoSomething(10, 20, [](int x, int y) { return x - y; }); // 람다도 가능

핵심 의미

  • 템플릿은 "이 타입이 무엇인지"보다 "func(a,b) 호출 가능 여부"에 집중합니다.
  • 그래서 함수 포인터/펑터/람다를 동일 인터페이스로 처리할 수 있습니다.

priority_queue 비교자와 펑터

비교자 펑터 직접 구현

template<typename T>
struct Greater {
    bool operator()(const T& left, const T& right) const {
        return left > right;
    }
};

priority_queue<int, vector<int>, Greater<int>> minHeap;

왜 최소 힙이 되는가?

  • priority_queue는 기본적으로 "비교 기준에서 가장 우선인 원소"가 top에 옵니다.
  • Greater를 쓰면 작은 값이 더 우선되도록 동작해 최소 힙처럼 사용할 수 있습니다.

비교자 작성 주의

  • 비교자는 일관된 기준(Strict Weak Ordering)을 가져야 합니다.
  • 상태를 가진 비교자를 쓸 때는 복사/생명주기를 고려해야 합니다.

MMO Job 예제 (행동 + 데이터 큐잉)

추상 Job 인터페이스

class Job {
public:
    virtual ~Job() = default;
    virtual void Execute() const = 0;
};

구체 Job

class MoveJob : public Job {
public:
    MoveJob(int x, int y) : x(x), y(y) {}
    void Execute() const override {
        cout << "Move to (" << x << ", " << y << ")\n";
    }
private:
    int x, y;
};

class AttackJob : public Job {
public:
    explicit AttackJob(int targetId) : targetId(targetId) {}
    void Execute() const override {
        cout << "Attack target #" << targetId << '\n';
    }
private:
    int targetId;
};

관리 컨테이너 선택

  • 순차 실행만 하면 queue<unique_ptr<Job>>가 자연스럽습니다.
  • 중간 취소/삭제가 많으면 list<unique_ptr<Job>>도 고려할 수 있습니다.
  • 다형성 객체를 다룰 때는 가상 소멸자가 필수입니다.

핵심 교훈

  • 함수 포인터는 "행동 주소" 중심
  • 펑터/잡 객체는 "행동 + 데이터" 중심
  • 실제 서버/엔진 구조에서는 후자가 훨씬 확장성이 좋습니다.

자주 하는 실수 + 체크 질문

자주 하는 실수

실수문제
operator()const 누락const 컨텍스트에서 호출 불가
비교자 구현 비일관정렬/우선순위 결과 이상 동작
상태 있는 펑터를 값 복사로 남발의도와 다른 상태 분리
다형성 Job에 가상 소멸자 없음delete 시 누수/UB 가능

체크 질문 (스스로 답해보기)

  • 함수 포인터보다 펑터가 유리한 본질적 이유는 무엇인가?
  • 왜 STL 알고리즘/컨테이너는 펑터와 잘 맞을까?
  • priority_queue에서 비교자 펑터가 top 값을 어떻게 바꾸는가?
  • Job 시스템에서 "행동 + 데이터"를 분리하면 어떤 문제가 생기는가?

profile
李家네_공부방

0개의 댓글