함수 객체의 핵심
펑터란?
- 함수처럼 호출 가능한 객체입니다.
- 클래스/구조체에서
operator()를 정의하면 객체를 obj(args...) 형태로 호출할 수 있습니다.
- 즉, "행동(코드) + 상태(데이터)"를 한 덩어리로 다루는 방법입니다.
왜 필요한가?
- 함수 포인터는 주소만 들고 있어서 상태를 함께 담기 어렵습니다.
- 멤버 함수 포인터는 객체(
this)를 따로 관리해야 해서 사용이 번거롭습니다.
- 펑터는 호출에 필요한 데이터를 객체 내부에 저장할 수 있어 실전 코드가 단순해집니다.
함수 포인터 vs 펑터
| 항목 | 함수 포인터 | 펑터 |
|---|
| 상태 보유 | ✗ | ✓ |
| 시그니처 유연성 | 제한적 | ✓ (오버로드/템플릿 가능) |
| 템플릿과 궁합 | 제한적 | 매우 좋음 |
| 데이터 바인딩 | ✗ | ✓ |
기본 문법과 동작
상태 없는 펑터
struct Add {
int operator()(int a, int b) const {
return a + b;
}
};
Add add;
int r = add(10, 20);
상태 있는 펑터
class Accumulator {
public:
void operator()(int n) {
sum += n;
cout << "현재 합: " << sum << '\n';
}
private:
int sum = 0;
};
Accumulator acc;
acc(10);
acc(20);
- 같은 객체를 반복 호출하면 내부 상태가 누적됩니다.
- 이 "상태 유지"가 함수 포인터 대비 가장 큰 차이입니다.
행동 + 데이터 묶기
struct AddWithBias {
int bias = 0;
int operator()(int a, int b) const {
return a + b + bias;
}
};
AddWithBias op{10};
int r = op(20, 30);
템플릿과의 조합 (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 시스템에서 "행동 + 데이터"를 분리하면 어떤 문제가 생기는가?