C++ 함수 객체(Functor) 완벽 가이드 | operator·상태 보유
이 글의 핵심
C++ 함수 객체(Functor) 기초부터 실전까지. operator() 오버로딩, 상태 보유 functor, 비교 functor, std::function vs functor 선택 가이드, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴.
들어가며: “함수처럼 호출되는 객체가 필요해요”
구체적인 문제 시나리오
프로젝트를 진행하다 보면 이런 상황을 자주 겪습니다:
- std::sort에 커스텀 비교 기준을 넘기려 했는데, 비교 로직에 상태(예: 정렬 기준 필드 인덱스)가 필요하다
- std::find_if, std::count_if에 넘기는 predicate가 여러 번 재사용되어야 하는데, 람다는 타입이 매번 달라 템플릿 인자로 넘기기 어렵다
- map/set의 키 비교에 “나이로 비교”, “이름으로 비교”처럼 런타임에 선택되는 기준이 필요하다
- 콜백을 저장할 때 힙 할당 없이 인라인 최적화가 되길 원한다
- unique_ptr의 커스텀 deleter처럼 타입의 일부가 되어야 하는 호출 가능 객체가 필요하다
추가 문제 시나리오
시나리오 1: 정렬 기준이 런타임에 결정되는 테이블
사용자가 “이름순”, “나이순”, “급여순” 중 하나를 선택하면 그에 맞게 std::vector<Employee>를 정렬해야 합니다. 비교 함수에 어느 필드로 비교할지를 넘겨야 하는데, 일반 함수는 상태를 가질 수 없습니다. 함수 객체는 생성자로 sortBy 인덱스를 받아 operator()에서 해당 필드로 비교할 수 있습니다.
시나리오 2: 재시도 횟수를 가진 네트워크 호출
HTTP 요청이 실패하면 최대 3번까지 재시도하는 래퍼가 필요합니다. “몇 번 재시도할지”는 생성 시점에 정해지고, 호출 시마다 남은 횟수를 감소시켜야 합니다. 상태를 가진 함수 객체가 적합합니다.
시나리오 3: map의 커스텀 비교자
std::map<Person, int>에서 Person을 “나이로만” 비교하고 싶습니다. operator<를 수정하면 다른 곳에서 Person 비교가 바뀌므로, 비교 함수 객체를 map의 세 번째 템플릿 인자로 넘기는 것이 안전합니다.
시나리오 4: unique_ptr 커스텀 deleter
FILE*을 fclose로 닫는 unique_ptr을 만들 때, deleter 타입이 unique_ptr의 일부가 되어 타입 소거 없이 인라인됩니다. std::function<void(FILE*)> 대신 함수 객체를 쓰면 힙 할당이 없고 성능이 좋습니다.
시나리오 5: 알고리즘에 통계 수집 predicate
std::count_if로 조건을 만족하는 개수를 세면서, 동시에 조건을 만족한 값들의 합도 누적하고 싶습니다. 람다는 캡처로 할 수 있지만, 함수 객체는 getSum() 같은 접근자를 제공해 결과를 깔끔하게 가져올 수 있습니다.
C++에서 함수 객체(Functor)는 operator()를 오버로드한 클래스/구조체로, “객체이면서 함수처럼 호출 가능”한 타입입니다. 상태를 가질 수 있고, 타입으로 구분되며, 인라인되기 쉬워 STL 알고리즘, 콜백, deleter 등에서 널리 사용됩니다.
함수 객체 vs 다른 Callable 시각화
flowchart TB
subgraph callable["Callable (호출 가능한 것)"]
A[일반 함수]
B[함수 포인터]
C[람다 표현식]
D["함수 객체br/operator()"]
E["std function"]
end
subgraph traits["특징"]
T1[상태 보유]
T2[타입 일부]
T3[인라인 용이]
T4[타입 소거]
end
D --> T1
D --> T2
D --> T3
C --> T1
E --> T4
style D fill:#e1f5e1
이 글을 읽으면
operator()오버로딩으로 함수 객체를 만들 수 있습니다.- 상태를 가진 functor, 비교 functor를 완전히 이해할 수 있습니다.
std::functionvs functor 선택 기준을 알 수 있습니다.- 자주 발생하는 에러와 해결법을 익힐 수 있습니다.
- 프로덕션에서 바로 쓸 수 있는 패턴을 적용할 수 있습니다.
개념을 잡는 비유
C++에서 자주 쓰는 비유로, 템플릿은 붕어빵 틀, 스마트 포인터는 자동 청소 로봇, RAII는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.
목차
- operator() 기초
- 상태를 가진 함수 객체
- 비교 함수 객체
- std::function vs Functor
- 완전한 예제 모음
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
1. operator() 기초
1.1 기본 개념
함수 객체(Functor)는 operator()를 오버로드한 클래스 또는 구조체입니다. 인스턴스를 만들고 obj(args)처럼 호출하면, 컴파일러가 obj.operator()(args)로 변환합니다. 따라서 “함수처럼 호출되는 객체”가 됩니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o functor_basic functor_basic.cpp && ./functor_basic
#include <iostream>
struct Adder {
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Adder adder;
std::cout << adder(3, 5) << "\n"; // 8
std::cout << adder.operator()(3, 5) << "\n"; // 동일: 8
return 0;
}
실행 결과: 8이 두 줄 출력됩니다.
위 코드 설명:
Adder는operator()(int, int)를 갖고 있어adder(3, 5)처럼 호출 가능합니다.const를 붙이면operator()가 멤버를 수정하지 않음을 나타내며, const 객체에서도 호출 가능합니다.adder.operator()(3, 5)는adder(3, 5)와 동일한 호출입니다.
1.2 여러 인자, 여러 오버로드
operator()는 일반 멤버 함수처럼 오버로드할 수 있습니다. 인자 개수나 타입이 다르면 다른 operator()가 호출됩니다.
#include <iostream>
#include <string>
struct Printer {
void operator()(int x) const {
std::cout << "int: " << x << "\n";
}
void operator()(const std::string& s) const {
std::cout << "string: " << s << "\n";
}
void operator()(int a, int b) const {
std::cout << "two ints: " << a << ", " << b << "\n";
}
};
int main() {
Printer print;
print(42); // int: 42
print("hello"); // string: hello
print(10, 20); // two ints: 10, 20
return 0;
}
1.3 템플릿 operator()
제네릭 함수 객체는 template <typename T>를 operator()에 적용해 어떤 타입이든 받을 수 있습니다.
#include <iostream>
struct Identity {
template <typename T>
T operator()(T x) const {
return x;
}
};
int main() {
Identity id;
std::cout << id(42) << "\n"; // 42
std::cout << id(3.14) << "\n"; // 3.14
std::cout << id(std::string("hi")) << "\n"; // hi
return 0;
}
1.4 STL 알고리즘과 함께 사용
std::count_if, std::find_if, std::sort 등은 predicate 또는 비교자를 받습니다. 함수 객체를 넘기면 됩니다.
#include <algorithm>
#include <iostream>
#include <vector>
struct IsEven {
bool operator()(int x) const {
return x % 2 == 0;
}
};
struct Greater {
bool operator()(int a, int b) const {
return a > b;
}
};
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
int count = std::count_if(v.begin(), v.end(), IsEven());
std::cout << "Even count: " << count << "\n"; // 3
std::sort(v.begin(), v.end(), Greater());
for (int x : v) std::cout << x << " "; // 6 5 4 3 2 1
std::cout << "\n";
return 0;
}
주의점: IsEven()와 Greater()는 임시 객체를 생성합니다. std::count_if는 이 객체를 복사해 사용하므로, operator()가 const이면 const 임시에서도 호출 가능합니다.
2. 상태를 가진 함수 객체
2.1 생성자로 상태 전달
함수 객체는 멤버 변수를 가질 수 있어, 생성 시점에 값을 받아 저장하고 operator()에서 사용합니다. 일반 함수는 이런 “상태”를 가질 수 없습니다.
#include <iostream>
struct Multiplier {
int factor;
Multiplier(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
int main() {
Multiplier times2(2);
Multiplier times10(10);
std::cout << times2(5) << "\n"; // 10
std::cout << times10(5) << "\n"; // 50
return 0;
}
2.2 호출 시마다 상태 변경 (mutable)
호출할 때마다 내부 상태를 바꾸는 함수 객체가 필요할 수 있습니다. 예: 카운터, 재시도 횟수 감소. 이때 operator()를 const로 두면서 특정 멤버만 mutable로 선언하거나, operator()를 비const로 둡니다.
#include <iostream>
struct Counter {
int count = 0;
int operator()() {
return ++count;
}
};
int main() {
Counter cnt;
std::cout << cnt() << "\n"; // 1
std::cout << cnt() << "\n"; // 2
std::cout << cnt() << "\n"; // 3
return 0;
}
mutable 사용 예 (const 객체에서도 상태 변경):
struct MutableCounter {
mutable int count = 0;
int operator()() const {
return ++count;
}
};
2.3 범위 검증 Functor (실전 예제)
입력값이 특정 범위 내인지 검사하는 predicate입니다. 최소/최대값을 생성자로 받습니다.
#include <algorithm>
#include <iostream>
#include <vector>
struct InRange {
int minVal, maxVal;
InRange(int min, int max) : minVal(min), maxVal(max) {}
bool operator()(int x) const {
return x >= minVal && x <= maxVal;
}
};
int main() {
std::vector<int> data = {5, 15, 25, 35, 45};
int count = std::count_if(data.begin(), data.end(), InRange(10, 40));
std::cout << "In range [10,40]: " << count << "\n"; // 3 (15, 25, 35)
return 0;
}
2.4 필드 인덱스로 정렬 (실전 예제)
여러 필드 중 “어느 필드로 정렬할지”를 런타임에 결정하는 비교자입니다.
#include <algorithm>
#include <iostream>
#include <vector>
struct Employee {
std::string name;
int age;
double salary;
};
struct CompareByField {
int fieldIndex; // 0=name, 1=age, 2=salary
CompareByField(int idx) : fieldIndex(idx) {}
bool operator()(const Employee& a, const Employee& b) const {
switch (fieldIndex) {
case 0: return a.name < b.name;
case 1: return a.age < b.age;
case 2: return a.salary < b.salary;
default: return false;
}
}
};
int main() {
std::vector<Employee> emps = {
{"Alice", 30, 50000},
{"Bob", 25, 45000},
{"Charlie", 35, 60000}
};
std::sort(emps.begin(), emps.end(), CompareByField(1)); // 나이순
for (const auto& e : emps)
std::cout << e.name << " " << e.age << "\n";
return 0;
}
3. 비교 함수 객체
3.1 Strict Weak Ordering
std::sort, std::map, std::set 등에 넘기는 비교자는 Strict Weak Ordering을 만족해야 합니다:
comp(a, a)는 항상falsecomp(a, b)가 true이면comp(b, a)는 falsecomp(a, b)와comp(b, c)가 true이면comp(a, c)도 true (추이성)- 동등:
!comp(a,b) && !comp(b,a)이면 a와 b는 동등
3.2 기본 비교자: std::less, std::greater
#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end(), std::less<int>{}); // 오름차순
std::sort(v.begin(), v.end(), std::greater<int>{}); // 내림차순
for (int x : v) std::cout << x << " ";
std::cout << "\n";
return 0;
}
3.3 map/set에 커스텀 비교자
std::map의 세 번째 템플릿 인자가 비교자 타입입니다. Person을 나이로만 비교하는 map 예제입니다.
#include <iostream>
#include <map>
#include <string>
struct Person {
std::string name;
int age;
bool operator==(const Person& o) const {
return name == o.name && age == o.age;
}
};
struct CompareByAge {
bool operator()(const Person& a, const Person& b) const {
return a.age < b.age;
}
};
int main() {
std::map<Person, int, CompareByAge> ageToId;
ageToId[{"Alice", 30}] = 1;
ageToId[{"Bob", 25}] = 2;
ageToId[{"Charlie", 30}] = 3; // 나이가 같으면 다른 사람으로 저장됨
for (const auto& [p, id] : ageToId)
std::cout << p.name << " " << p.age << " -> " << id << "\n";
return 0;
}
주의: CompareByAge는 나이만 비교하므로, 나이가 같은 두 Person은 “다른 키”로 취급됩니다 (map은 동등이 아닌 “비교 불가”만 구분). 실무에서는 나이만으로는 유일성이 보장되지 않을 수 있으므로, 필요 시 (age, name) 등 복합 키를 사용합니다.
3.4 해시 함수 객체 (unordered_map)
std::unordered_map은 해시 함수와 동등 비교가 필요합니다. 해시 함수 객체를 세 번째 템플릿 인자로 넘깁니다.
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
struct PersonHash {
size_t operator()(const std::pair<std::string, int>& p) const {
return std::hash<std::string>{}(p.first) ^ (std::hash<int>{}(p.second) << 1);
}
};
int main() {
std::unordered_map<std::pair<std::string, int>, int, PersonHash> m;
m[{"Alice", 30}] = 1;
m[{"Bob", 25}] = 2;
std::cout << m[{"Alice", 30}] << "\n"; // 1
return 0;
}
4. std::function vs Functor
4.1 비교표
| 항목 | 함수 객체 (Functor) | std::function |
|---|---|---|
| 타입 | 타입이 functor마다 다름 | 시그니처로 통일 (타입 소거) |
| 인라인 | 가능 (컴파일러가 인라인) | 어려움 (간접 호출) |
| 힙 할당 | 없음 | 경우에 따라 있음 (SBO 초과 시) |
| 상태 | 멤버로 보유 | 내부에 저장 |
| 컨테이너 | 타입이 달라 한 컨테이너에 섞기 어려움 | vector<function<...>> 가능 |
| 런타임 교체 | 불가 (타입 고정) | 가능 |
4.2 언제 Functor를 쓸까?
- unique_ptr deleter: 타입의 일부가 되어야 하고, 인라인이 중요할 때
- STL 알고리즘에 템플릿으로 넘길 때:
template <typename Compare>로 받으면 functor가 인라인됨 - 핫 루프: 초당 수백만 번 호출되는 경로에서는
std::function오버헤드가 부담될 수 있음 - map/set 비교자: 템플릿 인자로 타입이 고정되어야 함
4.3 언제 std::function을 쓸까?
- 런타임에 콜백 교체: 버튼 클릭 핸들러를 나중에 바꾸는 경우
- 여러 타입의 callable을 한 컨테이너에:
vector<function<void()>>listeners - API 경계: DLL/공유 라이브러리 경계에서 시그니처만 노출하고 싶을 때
- 간편함: 타입을 신경 쓰지 않고 “호출 가능한 것”만 저장하고 싶을 때
4.4 성능 비교 개념
// Functor: 인라인 가능, ~수 ns/호출
template <typename F>
void process(F&& f) {
for (int i = 0; i < 1'000'000; ++i)
f(i);
}
process( { return x * 2; });
// std::function: 간접 호출, ~수십 ns/호출
std::function<int(int)> f = { return x * 2; };
for (int i = 0; i < 1'000'000; ++i)
f(i);
선택 가이드: 콜백이 초당 수천 번 이하로 호출되면 std::function도 충분합니다. 수백만 번 이상이면 템플릿으로 functor를 직접 받는 방식을 고려하세요.
5. 완전한 예제 모음
5.1 재시도 Functor
실패 시 N번까지 재시도하는 함수 객체입니다.
#include <exception>
#include <iostream>
template <typename Func>
class Retry {
Func func;
int maxAttempts;
public:
Retry(Func f, int max = 3) : func(std::move(f)), maxAttempts(max) {}
template <typename... Args>
auto operator()(Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
for (int i = 0; i < maxAttempts; ++i) {
try {
return func(std::forward<Args>(args)...);
} catch (const std::exception& e) {
std::cerr << "Attempt " << (i + 1) << "/" << maxAttempts
<< " failed: " << e.what() << "\n";
if (i == maxAttempts - 1) throw;
}
}
throw std::runtime_error("Retry exhausted");
}
};
int riskyOp() {
static int count = 0;
if (++count < 2) throw std::runtime_error("fail");
return 42;
}
int main() {
Retry<decltype(&riskyOp)> retry(riskyOp, 3);
std::cout << retry() << "\n"; // 42 (2번째 시도에서 성공)
return 0;
}
5.2 로깅 래퍼 Functor
기존 함수를 감싸 호출 전후로 로그를 남깁니다.
#include <functional>
#include <iostream>
#include <string>
template <typename R, typename... Args>
class LoggingWrapper {
std::function<R(Args...)> wrapped;
std::string name;
public:
LoggingWrapper(std::function<R(Args...)> f, const std::string& n)
: wrapped(std::move(f)), name(n) {}
R operator()(Args... args) {
std::cout << "[LOG] Calling " << name << "\n";
R result = wrapped(args...);
std::cout << "[LOG] " << name << " returned\n";
return result;
}
};
int add(int a, int b) { return a + b; }
int main() {
LoggingWrapper<int, int, int> loggedAdd(add, "add");
std::cout << loggedAdd(3, 5) << "\n";
return 0;
}
5.3 unique_ptr 커스텀 Deleter
FILE*을 fclose로 닫는 deleter입니다. 함수 객체를 쓰면 힙 할당 없이 인라인됩니다.
#include <cstdio>
#include <memory>
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
std::fclose(fp);
}
}
};
int main() {
std::unique_ptr<FILE, FileDeleter> fp(std::fopen("test.txt", "r"));
if (fp) {
// 파일 사용
}
// fp 소멸 시 FileDeleter::operator()(fp.get()) 호출 -> fclose
return 0;
}
5.4 통계 수집 Predicate
조건을 만족하는 개수와 합을 동시에 수집합니다. std::ref로 참조를 넘겨 원본이 수정되도록 합니다.
#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
struct CountAndSum {
int count = 0;
int sum = 0;
bool operator()(int x) {
if (x % 2 == 0) {
++count;
sum += x;
return true;
}
return false;
}
};
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
CountAndSum stat;
(void)std::count_if(v.begin(), v.end(), std::ref(stat));
std::cout << "Count: " << stat.count << ", Sum: " << stat.sum << "\n";
return 0;
}
주의: std::count_if는 predicate를 값으로 받아 복사합니다. std::ref(stat)로 참조를 넘기면 reference_wrapper<CountAndSum>이 전달되고, 그 operator()가 stat을 호출하므로 원본 stat이 수정됩니다.
6. 자주 발생하는 에러와 해결법
에러 1: operator()를 const로 선언하지 않아 const 객체에서 호출 불가
증상: std::count_if 등에서 “passing ‘const X’ as ‘this’ argument discards qualifiers” 에러
원인: STL은 predicate를 const 참조로 전달할 수 있습니다. operator()가 비const이면 const 객체에서 호출할 수 없습니다.
// ❌ const 객체에서 호출 불가
struct Bad {
bool operator()(int x) { return x > 0; }
};
std::count_if(v.begin(), v.end(), Bad()); // 일부 구현에서 에러
// ✅ 상태를 바꾸지 않으면 const
struct Good {
bool operator()(int x) const { return x > 0; }
};
에러 2: 비교자에서 Strict Weak Ordering 위반
증상: std::sort 후 순서가 이상하거나, std::map에서 크래시/무한 루프
원인: comp(a, a)가 true를 반환하거나, 추이성이 깨지면 undefined behavior입니다.
// ❌ comp(a,a)가 true일 수 있음
struct Bad {
bool operator()(int a, int b) const {
return a <= b; // a<=a -> true, 위반!
}
};
// ✅ Strict Weak Ordering
struct Good {
bool operator()(int a, int b) const {
return a < b;
}
};
에러 3: map/set 비교자에서 동등 시 true 반환
증상: map에 “같은” 키로 보이는 원소가 둘 다 들어가거나, 예상과 다른 동작
원인: comp(a,b)와 comp(b,a)가 둘 다 false일 때 a와 b는 “동등”입니다. comp(a,a)는 반드시 false여야 합니다.
// ❌ a==b일 때 true 반환
struct Bad {
bool operator()(int a, int b) const {
return a <= b; // a==b일 때 a<=b true -> 위반
}
};
에러 4: 함수 객체를 복사할 때 상태 손실
증상: std::count_if에 넘긴 후 통계가 0으로 나옴
원인: 알고리즘이 predicate를 복사해서 사용합니다. 복사본의 상태가 변경되며, 원본은 그대로입니다.
// 원본 stat은 변경되지 않음
CountAndSum stat;
std::count_if(v.begin(), v.end(), stat); // stat의 복사본이 사용됨
// ✅ std::ref로 참조 전달
std::count_if(v.begin(), v.end(), std::ref(stat));
에러 5: 람다와 함수 객체 혼동
증상: “no matching function for call” - 템플릿 인자 추론 실패
원인: 람다는 타입이 컴파일마다 다릅니다. std::function에 담아야 시그니처를 통일할 수 있습니다.
// ❌ 람다 타입이 달라 vector에 넣기 어려움
std::vector</* ??? */> vec;
vec.push_back( { return 1; });
vec.push_back( { return 2; }); // 다른 타입!
// ✅ std::function으로 시그니처 통일
std::vector<std::function<int()>> vec;
vec.push_back( { return 1; });
vec.push_back( { return 2; });
에러 6: operator() 오버로드 시 모호함
증상: “call to ‘operator()’ is ambiguous”
원인: 여러 operator()가 같은 인자로 호출 가능할 때, 컴파일러가 선택하지 못합니다.
// ❌ int와 long long이 모호할 수 있음
struct Ambiguous {
void operator()(int x) const {}
void operator()(long long x) const {}
};
Ambiguous a;
a(42); // int? long long? 모호
// ✅ 명시적 캐스트 또는 하나만 두기
에러 7: 이동 후 사용 (use-after-move)
증상: 크래시 또는 잘못된 결과
원인: std::move로 이동한 함수 객체를 다시 사용함
Retry<Func> r(f, 3);
auto r2 = std::move(r);
r(1); // ❌ r은 이동됨, undefined behavior
에러 8: 해시 함수에서 같은 키가 다른 해시값 반환
증상: unordered_map에서 같은 키가 여러 번 저장되거나 찾기 실패
원인: 동등한 키에 대해 해시 함수가 같은 값을 반환해야 합니다.
// ❌ 매번 다른 값 (나쁜 예)
struct BadHash {
size_t operator()(int x) const {
return std::rand(); // 같은 x라도 다른 값!
}
};
// ✅ 동등하면 같은 해시
struct GoodHash {
size_t operator()(int x) const {
return std::hash<int>{}(x);
}
};
7. 베스트 프랙티스
1. 상태를 바꾸지 않으면 operator()를 const로
STL과의 호환성과 const 객체 사용을 위해, 상태를 변경하지 않는 operator()는 const로 선언합니다.
struct Good {
bool operator()(int x) const { return x > 0; }
};
2. struct vs class
함수 객체는 보통 struct로 두고 멤버를 public으로 합니다. 데이터만 담는 경량 타입이라는 의도가 드러납니다.
struct CompareByAge {
bool operator()(const Person& a, const Person& b) const {
return a.age < b.age;
}
};
3. 비교자는 Strict Weak Ordering 준수
<만 사용하고 <=를 쓰지 않습니다. comp(a,a)는 항상 false여야 합니다.
bool operator()(const T& a, const T& b) const {
return a.field < b.field; // ✅
// return a.field <= b.field; // ❌
}
4. 작은 함수 객체는 인라인 유지
멤버가 많거나 크기가 크면 복사 비용이 늘어납니다. 필요한 상태만 담고, 나머지는 포인터/참조로 보관합니다.
5. std::invoke 활용 (C++17)
std::invoke는 함수 포인터, 멤버 함수 포인터, 함수 객체를 통일된 방식으로 호출합니다. 제네릭 코드에서 유용합니다.
template <typename F, typename... Args>
auto call(F&& f, Args&&... args) {
return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}
6. 람다로 간단한 것은 처리
한 곳에서만 쓰는 1~2줄 로직은 람다가 더 읽기 쉽습니다. 재사용되거나 상태가 복잡하면 함수 객체를 선택합니다.
// 간단: 람다
std::sort(v.begin(), v.end(), { return a > b; });
// 복잡: 함수 객체
std::sort(emps.begin(), emps.end(), CompareByField(userSelectedIndex));
8. 프로덕션 패턴
패턴 1: optional 콜백
콜백이 없을 수 있는 API에서, 호출 전 null 체크를 반복하지 않도록 래퍼를 둡니다.
#include <functional>
template <typename Signature>
class OptionalCallback {
std::function<Signature> callback;
public:
void set(std::function<Signature> cb) { callback = std::move(cb); }
template <typename... Args>
void invoke(Args&&... args) {
if (callback) {
callback(std::forward<Args>(args)...);
}
}
};
패턴 2: 메모이제이션 Functor
같은 인자에 대해 결과를 캐시해 재계산을 피합니다.
#include <functional>
#include <map>
#include <tuple>
template <typename R, typename... Args>
class Memoized {
std::function<R(Args...)> func;
mutable std::map<std::tuple<Args...>, R> cache;
public:
explicit Memoized(std::function<R(Args...)> f) : func(std::move(f)) {}
R operator()(Args... args) const {
auto key = std::make_tuple(args...);
auto it = cache.find(key);
if (it != cache.end()) return it->second;
R result = func(args...);
cache[key] = result;
return result;
}
};
패턴 3: 파이프라인 Functor
여러 단계를 순서대로 적용하는 체인입니다.
#include <functional>
#include <vector>
template <typename T>
class Pipeline {
std::vector<std::function<T(T)>> stages;
public:
Pipeline& add(std::function<T(T)> f) {
stages.push_back(std::move(f));
return *this;
}
T operator()(T input) const {
for (const auto& f : stages) {
input = f(input);
}
return input;
}
};
// 사용
// Pipeline<int> p;
// p.add( { return x * 2; }).add( { return x + 10; });
// int result = p(5); // 20
패턴 4: 타임아웃 래퍼
지정 시간 내에 완료되지 않으면 에러 콜백을 호출합니다.
#include <chrono>
#include <functional>
#include <future>
void withTimeout(
std::function<void()> task,
std::chrono::milliseconds timeout,
std::function<void()> onTimeout)
{
auto future = std::async(std::launch::async, std::move(task));
if (future.wait_for(timeout) == std::future_status::timeout) {
onTimeout();
} else {
future.get();
}
}
패턴 5: 커스텀 Deleter (RAII)
리소스 해제를 함수 객체로 캡슐화합니다.
#include <memory>
struct FdDeleter {
void operator()(int* fd) const {
if (fd && *fd >= 0) {
::close(*fd);
*fd = -1;
}
}
};
using unique_fd = std::unique_ptr<int, FdDeleter>;
패턴 6: 이벤트 큐 (스레드 안전)
작업 스레드에서 이벤트를 큐에 넣고, 메인 스레드에서 콜백을 실행합니다.
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
class EventQueue {
std::queue<std::function<void()>> queue;
std::mutex mtx;
std::condition_variable cv;
public:
void post(std::function<void()> task) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(std::move(task));
cv.notify_one();
}
void processOne() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return !queue.empty(); });
auto task = std::move(queue.front());
queue.pop();
lock.unlock();
task();
}
};
함수 객체 적용 체크리스트
실무에서 함수 객체를 도입할 때 확인할 항목입니다.
- 상태가 필요한가? → 함수 객체 (생성자로 전달)
- map/set 비교자 → Strict Weak Ordering 준수
- unique_ptr deleter → 함수 객체 (타입 일부, 인라인)
- 핫 루프 → 템플릿으로 functor 직접 받기
- 런타임 교체/컨테이너 → std::function
- operator() const → 상태 변경 없으면 const
- 해시 함수 → 동등한 키에 같은 해시값 반환
정리
| 항목 | 내용 |
|---|---|
| 함수 객체 | operator()를 오버로드한 클래스/구조체 |
| 상태 보유 | 생성자로 받아 멤버에 저장 |
| 비교자 | Strict Weak Ordering 준수, comp(a,a)==false |
| std::function vs Functor | 타입 소거·유연함 vs 인라인·성능 |
| unique_ptr deleter | 함수 객체 권장 (힙 할당 없음) |
| STL 알고리즘 | predicate/비교자를 값으로 복사, std::ref로 참조 전달 가능 |
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ std::function | 콜백·전략 패턴과 함수 객체
- C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
이 글에서 다루는 키워드 (관련 검색어)
C++ 함수 객체, functor, operator(), std::function vs functor, 비교 함수 객체, Strict Weak Ordering, predicate, callable 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 함수 객체와 람다의 차이는?
A. 람다는 “이름 없는 함수 객체”입니다. 한 곳에서만 쓰는 간단한 로직은 람다, 상태를 많이 가진 재사용 가능한 로직은 함수 객체가 적합합니다.
Q. std::function과 함수 객체 중 언제 뭘 쓰나요?
A. 런타임에 콜백을 바꾸거나 여러 타입의 callable을 컨테이너에 담을 때는 std::function. 핫 루프·인라인 최적화가 중요하면 템플릿으로 functor 직접 받기.
Q. operator()를 const로 선언해야 하나요?
A. 상태를 변경하지 않는 operator()는 const로 선언하면 const 객체에서도 호출 가능하고, STL 알고리즘과의 호환성이 좋아집니다.
Q. map의 비교자로 람다를 쓸 수 있나요?
A. 람다는 타입이 없어서(또는 타입이 길어서) 템플릿 인자로 직접 넘기기 어렵습니다. decltype으로 람다 타입을 추출할 수는 있지만, 보통은 함수 객체나 std::function을 쓰는 편이 낫습니다.
한 줄 요약: 함수 객체는 operator()로 “호출 가능한 객체”를 만들며, 상태 보유·비교자·deleter 등에 활용됩니다. std::function은 유연하지만 오버헤드가 있으므로, 성능이 중요하면 functor를 템플릿으로 직접 받으세요.
이전 글: C++ std::function과 함수 객체 (#13-2)
다음 글: C++ 람다 표현식 (#10-1)
관련 글
- C++ 백엔드·게임 서버 시스템 디자인 | 대규모 동시 접속과 메모리 풀 [#46-1]
- C++ 자주 틀리는 C++ 기술 면접 질문 50선 | 출제 의도와 모범 답변 [#46-2]
- C++ 도메인별 요구 역량 | 네카라쿠배·금융·게임 [#46-3]
- C++ std::function | 콜백·전략 패턴과 함수 객체
- C++ 오픈소스 기여: 유명 라이브러리 분석부터 첫 Pull Request까지 [#45-1]