C++ std::function | 콜백·전략 패턴과 함수 객체
이 글의 핵심
C++ std::function 완벽 가이드. 함수·람다·함수 객체를 변수에 저장, 콜백 패턴 구현, 전략 패턴(Strategy Pattern), operator() 오버로딩, std::bind 사용법, 성능 오버헤드, 실전 이벤트 시스템 구현까지 상세히 설명합니다.
들어가며: 함수를 변수에 저장하고 싶다
”버튼 클릭 시 실행할 함수를 어떻게 저장하죠?”
UI 버튼에 클릭 이벤트 핸들러를 등록하고 싶었습니다. 하지만 함수 포인터는 제한적이었습니다. std::function은 “호출 가능한 것(callable—함수, 람다, operator()를 가진 객체 등)“을 하나의 타입으로 감싸서 저장·전달할 수 있게 해 줍니다. 타입이 달라도 시그니처(함수의 반환 타입과 매개변수 타입 목록)만 맞으면 같은 변수에 넣을 수 있어서, 콜백·전략 패턴·이벤트 핸들러를 구현할 때 실무에서 널리 쓰입니다. 다만 인라인되지 않을 수 있어서, 성능이 중요한 경로에서는 템플릿으로 callable을 그대로 받는 방식도 고려할 만합니다.
문제의 코드:
class Button {
void (*onClick)(); // ❌ 함수 포인터만 가능
public:
void setOnClick(void (*callback)()) {
onClick = callback;
}
void click() {
if (onClick) onClick();
}
};
// ❌ 람다 캡처 불가
int clickCount = 0;
button.setOnClick([&clickCount]() { // 에러!
clickCount++;
});
std::function으로 해결:
class Button {
std::function<void()> onClick; // ✅ 모든 callable 가능
public:
void setOnClick(std::function<void()> callback) {
onClick = callback;
}
void click() {
if (onClick) onClick();
}
};
// ✅ 람다, 함수, 함수 객체 모두 가능
int clickCount = 0;
button.setOnClick([&clickCount]() {
clickCount++;
});
이 글을 읽으면:
- std::function으로 콜백을 저장할 수 있습니다.
- 함수 객체를 만들고 활용할 수 있습니다.
- 전략 패턴을 구현할 수 있습니다.
- 실전에서 유연한 코드를 작성할 수 있습니다.
목차
추가 문제 시나리오: 언제 std::function이 필요한가?
시나리오 1: 알고리즘 비교 함수를 런타임에 바꾸고 싶다
상황: 사용자가 “오름차순/내림차순”을 UI에서 선택하면, 그에 맞게 정렬해야 합니다. std::sort는 비교 함수를 템플릿 인자로 받아 컴파일 타임에 고정되는데, 런타임에 선택하려면 std::function에 담아야 합니다.
// ❌ 컴파일 타임에 고정됨
std::sort(vec.begin(), vec.end(), std::less<int>{});
// ✅ 런타임에 전략 교체 가능
std::function<bool(int, int)> comparator = std::less<int>{};
if (userWantsDescending) {
comparator = std::greater<int>{};
}
std::sort(vec.begin(), vec.end(), comparator);
시나리오 2: 네트워크 요청 완료 시 콜백을 호출하고 싶다
상황: HTTP 요청이 비동기로 완료되면 결과를 처리하는 함수를 호출해야 합니다. 요청을 보낸 시점과 완료 시점이 다르므로, 콜백을 저장해 두었다가 나중에 호출해야 합니다. 함수 포인터는 캡처가 있는 람다를 받을 수 없습니다.
// ❌ 함수 포인터: 캡처 불가
void (*onComplete)(int status);
// ✅ std::function: 람다 캡처 가능
std::function<void(int)> onComplete;
std::string requestId = "req-123";
onComplete = [requestId](int status) {
std::cout << "Request " << requestId << " completed: " << status << "\n";
};
시나리오 3: 여러 타입의 callable을 하나의 컨테이너에 담고 싶다
상황: 이벤트 리스너 목록에 “일반 함수”, “람다”, “멤버 함수를 bind한 것”을 섞어서 등록하고 싶습니다. 타입이 각각 다르므로 std::vector<void(*)()> 같은 단일 타입 컨테이너에는 넣을 수 없습니다. std::function으로 시그니처를 통일하면 같은 벡터에 담을 수 있습니다.
std::vector<std::function<void()>> listeners;
void freeFunction() { std::cout << "Free\n"; }
listeners.push_back(freeFunction);
listeners.push_back( { std::cout << "Lambda\n"; });
struct Handler {
void handle() { std::cout << "Member\n"; }
} handler;
listeners.push_back(std::bind(&Handler::handle, &handler));
for (auto& fn : listeners) fn(); // Free, Lambda, Member
Callable 타입 관계도
flowchart TB
subgraph callable["Callable (호출 가능한 것)"]
A[일반 함수]
B[함수 포인터]
C[람다 표현식]
D["함수 객체br/operator()"]
E["std bind 결과"]
end
subgraph storage["저장/전달 수단"]
F["std functionbr/(타입 소거, 유연함)"]
G["템플릿 Funcbr/(인라인, 빠름)"]
end
A --> F
B --> F
C --> F
D --> F
E --> F
A --> G
C --> G
D --> G
style F fill:#e1f5e1
style G fill:#e1e5f5
1. std::function 기초
기본 사용법
std::function<int(int, int)>는 “int 두 개를 받아 int를 반환하는 호출 가능한 것”을 담는 타입입니다. 일반 함수 add의 이름을 대입하면 그 함수를 저장하고, func(3, 5)처럼 호출할 수 있습니다. 함수 포인터와 달리 시그니처만 맞으면 람다, 함수 객체, std::bind 결과 등 어떤 callable이든 같은 변수에 넣을 수 있어서, 콜백·전략 패턴을 구현할 때 유용합니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o func_basic func_basic.cpp && ./func_basic
#include <functional>
#include <iostream>
int add(int a, int b) { return a + b; }
int main() {
std::function<int(int, int)> func = add;
int result = func(3, 5); // 8
std::cout << result << "\n";
return 0;
}
실행 결과: 8 이 한 줄 출력됩니다.
람다 저장
람다는 “이름 없는 함수 객체”이므로 타입이 컴파일마다 다릅니다. 그래서 auto로만 받거나, std::function 에 담아야 나중에 교체·저장이 가능합니다. std::function<int(int, int)>에 캡처 없는 람다를 대입하면, func(3, 5)로 일반 함수처럼 호출할 수 있고, 캡처가 있는 람다도 같은 시그니처면 같은 std::function 타입에 넣을 수 있습니다.
std::function<int(int, int)> func = {
return a + b;
};
int result = func(3, 5); // 8
멤버 함수 저장
멤버 함수는 “객체 + 함수”가 함께 있어야 하므로, 첫 번째 인자로 객체(참조)를 넘기거나 **std::bind**로 객체를 묶어 둡니다. std::function<int(Calculator&, int, int)>는 “Calculator 참조와 int 두 개를 받아 int를 반환”하는 타입이므로, func(calc, 3, 5)처럼 호출합니다. std::bind(&Calculator::add, &calc, _1, _2)는 calc를 고정하고 나머지 두 인자만 받는 callable을 만들어, 나중에 boundFunc(3, 5)만으로 호출할 수 있게 합니다.
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
};
Calculator calc;
// 멤버 함수 포인터
std::function<int(Calculator&, int, int)> func = &Calculator::add;
int result = func(calc, 3, 5); // 8
// 또는 bind 사용
auto boundFunc = std::bind(&Calculator::add, &calc,
std::placeholders::_1,
std::placeholders::_2);
result = boundFunc(3, 5); // 8
함수 객체 저장
함수 객체(functor)는 operator()를 오버로드한 구조체/클래스로, Adder()처럼 인스턴스를 만들어 std::function에 넣을 수 있습니다. std::function은 내부적으로 호출 가능한 대상을 타입 소거(type erasure)해서 저장하므로, Adder, 람다, 함수 포인터 등 서로 다른 타입이라도 시그니처가 같으면 같은 std::function 변수에 대입하고 나중에 교체할 수 있습니다.
struct Adder {
int operator()(int a, int b) const {
return a + b;
}
};
std::function<int(int, int)> func = Adder();
int result = func(3, 5); // 8
빈 function 확인
std::function<void()> func;
if (!func) {
std::cout << "Function is empty\n";
}
func = { std::cout << "Hello\n"; };
if (func) {
func(); // Hello
}
std::function 시그니처 패턴
| 시그니처 | 의미 |
|---|---|
std::function<void()> | 인자 없음, 반환 없음 |
std::function<int(int, int)> | int 두 개 받아 int 반환 |
std::function<void(const std::string&)> | 문자열 받아 void 반환 |
std::function<bool(int, int)> | 비교자 (정렬 등) |
2. 함수 객체 (Functor)
기본 함수 객체
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
}
상태를 가진 함수 객체
class Counter {
int count = 0;
public:
int operator()() {
return ++count;
}
int getCount() const { return count; }
};
int main() {
Counter counter;
std::cout << counter() << "\n"; // 1
std::cout << counter() << "\n"; // 2
std::cout << counter() << "\n"; // 3
std::cout << "Total: " << counter.getCount() << "\n"; // 3
}
STL과 함께 사용
struct IsEven {
bool operator()(int x) const {
return x % 2 == 0;
}
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
// 짝수 개수
int count = std::count_if(numbers.begin(), numbers.end(), IsEven());
// 짝수 찾기
auto it = std::find_if(numbers.begin(), numbers.end(), IsEven());
}
완전한 예제: 범위 검증 Functor
실무에서 “입력값이 유효 범위 내인지” 검사하는 함수 객체를 자주 만듭니다. 상태(최소/최대값)를 생성자로 받고, operator()에서 검증합니다.
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};
// 10~40 범위 내 개수
int count = std::count_if(data.begin(), data.end(), InRange(10, 40));
std::cout << count << "\n"; // 3 (15, 25, 35)
}
완전한 예제: 로깅 래퍼 Functor
기존 함수를 감싸서 호출 전후로 로그를 남기는 함수 객체입니다. std::function으로 원본을 저장하고, operator()에서 로깅 후 위임합니다.
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(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";
// [LOG] Calling add
// [LOG] add returned
// 8
}
완전한 예제: 재시도 Functor
실패 시 N번까지 재시도하는 함수 객체입니다. 네트워크 호출, 파일 I/O 등에 유용합니다.
template <typename Func>
class Retry {
Func func;
int maxAttempts;
public:
Retry(Func f, int max = 3) : func(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 (...) {
if (i == maxAttempts - 1) throw;
std::cout << "Retry " << (i + 1) << "/" << maxAttempts << "\n";
}
}
throw std::runtime_error("Retry exhausted");
}
};
3. std::bind
기본 사용법
int add(int a, int b, int c) {
return a + b + c;
}
// 첫 번째 인자를 10으로 고정
auto add10 = std::bind(add, 10, std::placeholders::_1, std::placeholders::_2);
int result = add10(5, 3); // add(10, 5, 3) = 18
인자 순서 변경
int subtract(int a, int b) {
return a - b;
}
// 인자 순서 바꾸기
auto reverseSub = std::bind(subtract, std::placeholders::_2, std::placeholders::_1);
std::cout << subtract(10, 3) << "\n"; // 7
std::cout << reverseSub(10, 3) << "\n"; // -7 (3 - 10)
멤버 함수 바인딩
class Printer {
public:
void print(const std::string& msg) {
std::cout << "Message: " << msg << "\n";
}
};
int main() {
Printer printer;
// 멤버 함수 바인딩
auto boundPrint = std::bind(&Printer::print, &printer, std::placeholders::_1);
boundPrint("Hello"); // Message: Hello
}
람다 vs bind
int x = 10;
// bind 사용
auto func1 = std::bind( { return a + b; }, x, std::placeholders::_1);
// ✅ 람다가 더 명확 (권장)
auto func2 = [x](int b) { return x + b; };
std::cout << func1(5) << "\n"; // 15
std::cout << func2(5) << "\n"; // 15
람다를 권장하는 이유: std::bind는 _1, _2 같은 자리 표시자가 나열되어 있어 의도가 한눈에 들어오지 않고, 인자 순서를 바꿀 때 실수하기 쉽습니다. 람다는 “어떤 값을 캡처해서 어떤 인자로 넘길지”가 그대로 드러나서 가독성과 유지보수에 유리합니다. 성능 면에서도 람다는 인라인되기 쉬운 반면, bind로 만든 호출 객체는 추가 간접 호출이 생길 수 있습니다.
4. 콜백 패턴
이벤트 핸들러
class EventEmitter {
std::vector<std::function<void(const std::string&)>> listeners;
public:
void on(std::function<void(const std::string&)> callback) {
listeners.push_back(callback);
}
void emit(const std::string& event) {
for (auto& listener : listeners) {
listener(event);
}
}
};
int main() {
EventEmitter emitter;
emitter.on( {
std::cout << "Listener 1: " << event << "\n";
});
emitter.on( {
std::cout << "Listener 2: " << event << "\n";
});
emitter.emit("click");
// Listener 1: click
// Listener 2: click
}
비동기 콜백
#include <thread>
#include <chrono>
void asyncOperation(std::function<void(int)> callback) {
std::thread([callback]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
callback(42); // 결과 전달
}).detach();
}
int main() {
std::cout << "Starting...\n";
asyncOperation( {
std::cout << "Result: " << result << "\n";
});
std::cout << "Waiting...\n";
std::this_thread::sleep_for(std::chrono::seconds(2));
}
에러 핸들링
using SuccessCallback = std::function<void(int)>;
using ErrorCallback = std::function<void(const std::string&)>;
void divide(int a, int b, SuccessCallback onSuccess, ErrorCallback onError) {
if (b == 0) {
onError("Division by zero");
} else {
onSuccess(a / b);
}
}
int main() {
divide(10, 2,
{
std::cout << "Success: " << result << "\n";
},
{
std::cerr << "Error: " << error << "\n";
});
}
5. 실전 활용
패턴 1: 전략 패턴
class Sorter {
std::function<bool(int, int)> comparator;
public:
void setStrategy(std::function<bool(int, int)> comp) {
comparator = comp;
}
void sort(std::vector<int>& vec) {
std::sort(vec.begin(), vec.end(), comparator);
}
};
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9};
Sorter sorter;
// 오름차순
sorter.setStrategy( { return a < b; });
sorter.sort(numbers);
// 내림차순
sorter.setStrategy( { return a > b; });
sorter.sort(numbers);
}
패턴 2: 명령 패턴
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
};
class CommandManager {
std::vector<std::function<void()>> commands;
public:
void addCommand(std::function<void()> cmd) {
commands.push_back(cmd);
}
void executeAll() {
for (auto& cmd : commands) {
cmd();
}
}
};
int main() {
CommandManager manager;
manager.addCommand( { std::cout << "Command 1\n"; });
manager.addCommand( { std::cout << "Command 2\n"; });
manager.addCommand( { std::cout << "Command 3\n"; });
manager.executeAll();
}
패턴 3: 체인 패턴
class Pipeline {
std::vector<std::function<int(int)>> stages;
public:
Pipeline& addStage(std::function<int(int)> stage) {
stages.push_back(stage);
return *this;
}
int execute(int input) {
int result = input;
for (auto& stage : stages) {
result = stage(result);
}
return result;
}
};
int main() {
Pipeline pipeline;
pipeline
.addStage( { return x * 2; })
.addStage( { return x + 10; })
.addStage( { return x * x; });
int result = pipeline.execute(5); // ((5*2)+10)^2 = 400
std::cout << result << "\n";
}
패턴 4: 메모이제이션
template <typename R, typename... Args>
class Memoized {
std::function<R(Args...)> func;
mutable std::map<std::tuple<Args...>, R> cache;
public:
Memoized(std::function<R(Args...)> f) : func(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;
}
};
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
Memoized<int, int> memoFib(fibonacci);
std::cout << memoFib(40) << "\n"; // 빠름 (캐시됨)
}
패턴 5: 타이머
class Timer {
std::function<void()> callback;
std::chrono::milliseconds interval;
std::thread thread;
std::atomic<bool> running{false};
public:
Timer(std::chrono::milliseconds ms, std::function<void()> cb)
: interval(ms), callback(cb) {}
void start() {
running = true;
thread = std::thread([this]() {
while (running) {
std::this_thread::sleep_for(interval);
if (running) callback();
}
});
}
void stop() {
running = false;
if (thread.joinable()) thread.join();
}
~Timer() {
stop();
}
};
int main() {
int count = 0;
Timer timer(std::chrono::milliseconds(100), [&count]() {
std::cout << "Tick " << ++count << "\n";
});
timer.start();
std::this_thread::sleep_for(std::chrono::seconds(1));
timer.stop();
}
6. 자주 발생하는 오류와 해결법
오류 1: 빈 std::function 호출
증상: std::bad_function_call 예외 또는 크래시
원인: std::function에 아무것도 대입하지 않은 상태에서 func()를 호출함
// ❌ 위험
std::function<void()> func;
func(); // std::bad_function_call
// ✅ 호출 전 검사
if (func) {
func();
}
오류 2: 시그니처 불일치
증상: 컴파일 에러 “no matching function for call”
원인: std::function에 넣으려는 callable의 시그니처가 맞지 않음
// ❌ 반환 타입 불일치
std::function<void(int)> f = { return x * 2; }; // int 반환
// ✅ 시그니처 일치
std::function<int(int)> f = { return x * 2; };
오류 3: 람다 캡처로 인한 dangling reference
증상: 크래시 또는 undefined behavior
원인: 람다가 참조로 캡처한 지역 변수가 스코프를 벗어난 뒤 콜백이 호출됨
// ❌ 위험
std::function<void()> callback;
{
int local = 42;
callback = [&local]() { std::cout << local << "\n"; };
}
callback(); // local은 이미 소멸됨!
// ✅ 값으로 캡처
int local = 42;
callback = [local]() { std::cout << local << "\n"; };
오류 4: std::bind와 객체 수명
증상: 크래시 (use-after-free)
원인: std::bind로 묶은 객체 포인터가 가리키는 객체가 먼저 소멸됨
// ❌ 위험
std::function<void()> bound;
{
Printer printer;
bound = std::bind(&Printer::print, &printer, "Hello");
}
bound(); // printer는 이미 소멸됨!
// ✅ 객체 수명이 callback보다 길어야 함
Printer printer;
auto bound = std::bind(&Printer::print, &printer, "Hello");
bound();
오류 5: 재귀 호출에서 std::function 대입
증상: 무한 재귀 또는 잘못된 동작
원인: 재귀 함수를 std::function에 담을 때, 아직 초기화되지 않은 자기 자신을 호출하려 함
// ❌ 잘못된 재귀 람다
std::function<int(int)> factorial;
factorial = [&factorial](int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
};
// ✅ 일반 함수 사용
int factorialImpl(int n) {
if (n <= 1) return 1;
return n * factorialImpl(n - 1);
}
std::function<int(int)> factorial = factorialImpl;
7. 성능 비교와 선택 가이드
std::function vs 템플릿 vs 함수 포인터
| 방식 | 인라인 | 힙 할당 | 타입 소거 | 유연성 |
|---|---|---|---|---|
std::function | ❌ | 경우에 따라 | ✅ | 높음 |
템플릿 template<typename F> | ✅ | ❌ | ❌ | 높음 |
| 함수 포인터 | ❌ | ❌ | ✅ | 낮음 |
벤치마크 개념 (1억 회 호출 가정)
// std::function: ~수백 ns/호출 (간접 호출, 인라인 어려움)
std::function<int(int)> f = { return x * 2; };
for (int i = 0; i < 100000000; ++i) sum += f(i);
// 템플릿: ~수 ns/호출 (인라인 가능)
template <typename F>
void loop(F&& f) {
for (int i = 0; i < 100000000; ++i) sum += f(i);
}
loop( { return x * 2; });
// 함수 포인터: std::function보다 약간 빠를 수 있음
int (*fp)(int) = { return x * 2; }; // 캡처 없는 람다만
선택 가이드
- 런타임에 콜백을 바꿔야 함 →
std::function - 컨테이너에 여러 타입의 callable 저장 →
std::function - 핫 루프, 초당 수백만 호출 → 템플릿으로 callable 직접 받기
- 캡처 없는 람다만 → 함수 포인터도 가능 (C++11)
작은 객체 최적화 (SBO)
// 작은 람다: 힙 할당 없음 (대부분의 구현에서)
std::function<int(int)> small = { return x * 2; };
// 큰 람다: 힙 할당 발생
int data[100];
std::function<int(int)> large = [data](int x) { return x + data[0]; };
SBO가 의미하는 것: std::function은 내부에 작은 버퍼를 갖고 있어서, 저장할 호출 객체가 그 크기 이하면 힙 할당 없이 그 버퍼에 넣습니다. 캡처가 많은 람다처럼 크기가 크면 힙에 할당하고 포인터만 들고 있어서, 호출 시 한 번 더 간접 참조가 들어가고 할당/해제 비용도 생깁니다. 그래서 “캡처를 최소화한 람다”를 넣을수록 std::function 오버헤드가 줄어듭니다.
8. 프로덕션 패턴
패턴 1: 옵셔널 콜백
콜백이 없을 수 있는 API에서, 호출 전 null 체크를 반복하지 않도록 래퍼를 둡니다.
class OptionalCallback {
std::function<void(int)> callback;
public:
void set(std::function<void(int)> cb) { callback = std::move(cb); }
void invoke(int value) {
if (callback) callback(value);
}
};
패턴 2: 스레드 안전 이벤트 큐
작업 스레드에서 이벤트를 큐에 넣고, 메인 스레드에서 콜백을 실행하는 패턴입니다.
#include <queue>
#include <mutex>
#include <condition_variable>
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();
}
};
패턴 3: 데코레이터 체인
여러 콜백을 순서대로 실행하는 체인입니다. 미들웨어, 필터 패턴에 활용됩니다.
using Middleware = std::function<std::function<void()>(std::function<void()>)>;
std::function<void()> applyMiddleware(
std::function<void()> handler,
std::vector<Middleware> middlewares)
{
for (auto it = middlewares.rbegin(); it != middlewares.rend(); ++it) {
handler = (*it)(std::move(handler));
}
return handler;
}
// 사용 예
auto logged = {
return [next]() {
std::cout << "Before\n";
next();
std::cout << "After\n";
};
};
auto final = applyMiddleware( { std::cout << "Handler\n"; }, {logged});
final(); // Before, Handler, After
패턴 4: 타임아웃 래퍼
지정 시간 내에 완료되지 않으면 에러 콜백을 호출하는 패턴입니다.
#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();
}
}
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression
- C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
- C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
이 글에서 다루는 키워드 (관련 검색어)
C++ std::function, 함수 객체, 콜백, 전략 패턴, std::bind, functor 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 용도 | 장점 | 단점 |
|---|---|---|---|
| std::function | 콜백 저장 | 유연함 | 오버헤드 |
| 함수 객체 | 상태 + 동작 | 빠름 | 코드 많음 |
| 람다 | 간단한 로직 | 간결함 | 재사용 어려움 |
| std::bind | 인자 고정 | 편리함 | 가독성 낮음 |
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ std::function 완벽 가이드. 함수·람다·함수 객체를 변수에 저장, 콜백 패턴 구현, 전략 패턴(Strategy Pattern), operator() 오버로딩, std::bind 사용법, 성능 오버헤… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: std::function으로 콜백·전략을 타입에 구애받지 않고 넘길 수 있습니다. 다음으로 이동 의미론(#14-1)를 읽어보면 좋습니다.
이전 글: C++ 실전 가이드 #13-1: 람다 표현식
다음 글: C++ 실전 가이드 #14-1: 이동 의미론 — std::move와 rvalue 레퍼런스를 다룹니다.
핵심 원칙:
- 콜백 저장은 std::function
- 성능 중요하면 템플릿
- 간단한 로직은 람다
- bind보다 람다 선호
- 상태 필요하면 함수 객체
관련 글
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
- C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴
- C++ map·set 완벽 가이드 | ordered vs unordered· 커스텀 키
- C++ 컨테이너 선택 가이드 | vector/list/deque/map/set 상황별 선택과 성능 최적화
- C++ 함수 객체(Functor) 완벽 가이드 | operator·상태 보유