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

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

이 글의 핵심

C++ std::function 함수·람다·함수 객체를 변수에 저장, 콜백 패턴 구현, 전략 패턴(Strategy Pattern), operator() 오버로딩, std::bind 사용법, 성능 오버헤드, 실전 이벤트 시스템 구현까지 상세히 설명합니다.

들어가며: 함수를 변수에 저장하고 싶다

”버튼 클릭 시 실행할 함수를 어떻게 저장하죠?”

UI 버튼에 클릭 이벤트 핸들러를 등록하고 싶었습니다. 하지만 함수 포인터는 제한적이었습니다. std::function은 “호출 가능한 것(callable—함수, 람다, operator()를 가진 객체 등)“을 하나의 타입으로 감싸서 저장·전달할 수 있게 해 줍니다. 타입이 달라도 시그니처(함수의 반환 타입과 매개변수 타입 목록)만 맞으면 같은 변수에 넣을 수 있어서, 콜백·전략 패턴·이벤트 핸들러를 구현할 때 실무에서 널리 쓰입니다. 다만 인라인되지 않을 수 있어서, 성능이 중요한 경로에서는 템플릿으로 callable을 그대로 받는 방식도 고려할 만합니다. 문제의 코드: 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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으로 해결: 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 타입 정의
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으로 콜백을 저장할 수 있습니다.
  • 함수 객체를 만들고 활용할 수 있습니다.
  • 전략 패턴을 구현할 수 있습니다.
  • 실전에서 유연한 코드를 작성할 수 있습니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

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

추가 문제 시나리오: 언제 std::function이 필요한가?

시나리오 1: 알고리즘 비교 함수를 런타임에 바꾸고 싶다

상황: 사용자가 “오름차순/내림차순”을 UI에서 선택하면, 그에 맞게 정렬해야 합니다. std::sort는 비교 함수를 템플릿 인자로 받아 컴파일 타임에 고정되는데, 런타임에 선택하려면 std::function에 담아야 합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 컴파일 타임에 고정됨
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 요청이 비동기로 완료되면 결과를 처리하는 함수를 호출해야 합니다. 요청을 보낸 시점과 완료 시점이 다르므로, 콜백을 저장해 두었다가 나중에 호출해야 합니다. 함수 포인터는 캡처가 있는 람다를 받을 수 없습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 함수 포인터: 캡처 불가
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으로 시그니처를 통일하면 같은 벡터에 담을 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 타입 관계도

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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이든 같은 변수에 넣을 수 있어서, 콜백·전략 패턴을 구현할 때 유용합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 복사해 붙여넣은 뒤: 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 타입에 넣을 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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)만으로 호출할 수 있게 합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 변수에 대입하고 나중에 교체할 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

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 확인

아래 코드는 cpp를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

기본 함수 객체

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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
}

상태를 가진 함수 객체

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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과 함께 사용

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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()에서 검증합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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()에서 로깅 후 위임합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 등에 유용합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

기본 사용법

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

인자 순서 변경

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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)

멤버 함수 바인딩

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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. 콜백 패턴

이벤트 핸들러

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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));
}

에러 핸들링

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 전략 패턴

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 명령 패턴

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 체인 패턴

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 메모이제이션

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 타이머

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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()를 호출함 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 위험
std::function<void()> func;
func();  // std::bad_function_call
// ✅ 호출 전 검사
if (func) {
    func();
}

오류 2: 시그니처 불일치

증상: 컴파일 에러 “no matching function for call” 원인: std::function에 넣으려는 callable의 시그니처가 맞지 않음 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 반환 타입 불일치
std::function<void(int)> f =  { return x * 2; };  // int 반환
// ✅ 시그니처 일치
std::function<int(int)> f =  { return x * 2; };

오류 3: 람다 캡처로 인한 dangling reference

증상: 크래시 또는 undefined behavior 원인: 람다가 참조로 캡처한 지역 변수가 스코프를 벗어난 뒤 콜백이 호출됨 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 위험
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로 묶은 객체 포인터가 가리키는 객체가 먼저 소멸됨 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 위험
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에 담을 때, 아직 초기화되지 않은 자기 자신을 호출하려 함 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 재귀 람다
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억 회 호출 가정)

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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)

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 작은 람다: 힙 할당 없음 (대부분의 구현에서)
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 체크를 반복하지 않도록 래퍼를 둡니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 스레드 안전 이벤트 큐

작업 스레드에서 이벤트를 큐에 넣고, 메인 스레드에서 콜백을 실행하는 패턴입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 데코레이터 체인

여러 콜백을 순서대로 실행하는 체인입니다. 미들웨어, 필터 패턴에 활용됩니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 타임아웃 래퍼

지정 시간 내에 완료되지 않으면 에러 콜백을 호출하는 패턴입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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++ 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. 상태 필요하면 함수 객체

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3