C++ std::function | 콜백·전략 패턴과 함수 객체

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으로 콜백을 저장할 수 있습니다.
  • 함수 객체를 만들고 활용할 수 있습니다.
  • 전략 패턴을 구현할 수 있습니다.
  • 실전에서 유연한 코드를 작성할 수 있습니다.

목차

  1. std::function 기초
  2. 함수 객체 (Functor)
  3. std::bind
  4. 콜백 패턴
  5. 실전 활용
  6. 자주 발생하는 오류와 해결법
  7. 성능 비교와 선택 가이드
  8. 프로덕션 패턴

추가 문제 시나리오: 언제 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 레퍼런스를 다룹니다.

핵심 원칙:

  1. 콜백 저장은 std::function
  2. 성능 중요하면 템플릿
  3. 간단한 로직은 람다
  4. bind보다 람다 선호
  5. 상태 필요하면 함수 객체

관련 글

  • C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
  • C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴
  • C++ map·set 완벽 가이드 | ordered vs unordered· 커스텀 키
  • C++ 컨테이너 선택 가이드 | vector/list/deque/map/set 상황별 선택과 성능 최적화
  • C++ 함수 객체(Functor) 완벽 가이드 | operator·상태 보유