C++ std::function 완벽 이해: 함수 포인터의 한계를 넘어서는 방법

📚 델리게이트 시리즈

1편: 함수 포인터
2편: std::function
3편: Delegate 구현
4편: 엔진 스타일 Delegate

🚀 들어가며

이전 글에서는 함수 포인터를 이용해
👉 함수를 나중에 실행하는 콜백 구조를 만들어봤습니다.

void Execute(void (*callback)())
{
    callback();
}

이 구조만으로도 충분히 강력하지만,
실제로 사용하다 보면 곧 한계에 부딪히게 됩니다.

이번 글에서는 그 한계를 해결하는 방법인
👉 std::function과 람다(lambda)를 자세히 살펴보겠습니다.


🎯 함수 포인터의 한계 다시 보기

함수 포인터는 간단하고 빠르지만, 다음과 같은 문제가 있습니다.


❌ 상태를 저장할 수 없다

void PrintValue(int value)
{
    std::cout << value << "\n";
}
void (*func)(int) = PrintValue;
func(10);

👉 호출은 가능하지만

“value를 미리 저장한 상태로 함수”를 만들 수는 없습니다.


❌ 객체와 함께 쓰기 어렵다

class Player
{
public:
    void Attack()
    {
        std::cout << "Player Attack\n";
    }
};
void (*func)() = &Player::Attack; // ❌ 컴파일 오류

👉 멤버 함수는 일반 함수 포인터로 사용할 수 없습니다


🎯 해결 방향

이 문제를 해결하려면 다음이 필요합니다.

✔ 상태를 저장할 수 있어야 한다
✔ 객체와 함께 사용할 수 있어야 한다
✔ 함수처럼 호출 가능해야 한다

👉 이 요구를 만족하는 것이 바로
👉 std::function입니다.


🧠 std::function이란?

std::function은 다음과 같은 역할을 합니다.

“함수처럼 호출 가능한 모든 것을 담을 수 있는 컨테이너”


🎮 기본 사용법

#include <functional>
#include <iostream>

void Jump()
{
    std::cout << "Jump!\n";
}
std::function<void()> func = Jump;
func();

👉 함수 포인터와 거의 비슷하게 사용할 수 있습니다.


🔥 중요한 차이

함수 포인터는 “함수 주소만” 저장합니다.

하지만 std::function은:

함수
람다
함수 객체
바인딩된 함수

👉 모두 저장할 수 있습니다.


🎯 람다(lambda)와 함께 사용하기

이제 진짜 핵심입니다.

std::function<void()> func = []()
{
    std::cout << "Lambda!\n";
};

func();

👉 결과:

Lambda!

🧠 람다란?

람다는 “익명 함수”입니다.

[]() { /* 코드 */ }

👉 이름 없이 바로 정의해서 사용하는 함수


🎯 상태를 가지는 함수 만들기

함수 포인터에서는 불가능했던 것이 가능합니다.

int value = 10;

std::function<void()> func = [value]()
{
    std::cout << value << "\n";
};

func();

👉 결과:

10

🔥 핵심 포인트

람다는 변수를 “캡처”할 수 있다

👉 즉:

“상태를 가진 함수”를 만들 수 있다


🎮 콜백 구조 확장

이제 콜백 구조를 다시 만들어보겠습니다.

void Execute(std::function<void()> callback)
{
    callback();
}

사용:

Execute([]()
{
    std::cout << "Jump!\n";
});

Execute([]()
{
    std::cout << "Fire!\n";
});

👉 이제 함수뿐만 아니라 람다도 전달할 수 있습니다.


🎯 객체와 함께 사용하기

이제 가장 중요한 부분입니다.

class Player
{
public:
    void Attack()
    {
        std::cout << "Player Attack\n";
    }
};
Player player;

std::function<void()> func = [&player]()
{
    player.Attack();
};

func();

👉 결과:

Player Attack

🔥 무엇이 달라졌나?

함수 포인터에서는:

객체 함수 사용 불가 ❌

하지만 std::function + 람다에서는:

객체 상태 + 함수 결합 가능 ✔

🎯 이벤트 시스템 형태로 보기

지금까지 설명한 내용을 구조적으로 정리해보면 다음과 같습니다.

핵심은:
std::function과 람다는 단순 함수 호출을 넘어서,
상태와 객체를 함께 가지는 유연한 콜백 시스템을 만들 수 있다
는 점입니다.

C++ std::function과 lambda를 이용한 콜백 및 이벤트 시스템 구조를 설명하는 인포그래픽
std::function과 람다(lambda)를 사용하면 상태를 가진 함수와 객체 기반 콜백 구조를 만들 수 있으며, 이는 게임 엔진 이벤트 시스템의 기반이 됩니다.

위 구조처럼 std::function은 단순 함수 주소만 저장하는 것이 아닙니다.

람다(lambda)를 통해:

  • 상태를 캡처할 수 있고
  • 객체와 결합할 수 있으며
  • 이벤트 시스템 형태로 확장할 수 있습니다

이 구조는 이후 Delegate 시스템,
그리고 실제 게임 엔진 이벤트 구조로 자연스럽게 연결됩니다.

std::function<void()> onClick;

onClick = []()
{
    std::cout << "Button Clicked\n";
};

onClick();

👉 구조:

이벤트 등록
↓
이벤트 발생
↓
콜백 실행

⚠️ std::function의 단점

완벽해 보이지만 단점도 있습니다.


❌ 성능 오버헤드

  • 함수 포인터 -> 매우 빠름
  • std::function → 함수 포인터보다 유연하지만 타입 소거, 내부 저장 방식으로 인해 추가 비용 발생 가능성 있음
    • 일반적인 이벤트 처리에는 대부분 문제가 되지 않지만, 매 프레임 대량으로 호출되는 구조라면 비용을 고려해야 함

❌ 메모리 사용

함수 포인터 → 단순 주소
std::function → 내부 객체 포함

🎯 그래서 실제 엔진에서는?

게임 엔진에서는 상황에 따라 선택합니다.

성능 중요 → 함수 포인터
유연성 중요 → std::function

그리고 더 나아가:

👉 델리게이트 시스템을 직접 구현합니다


🔥 핵심 정리

  • std::function은 함수처럼 호출 가능한 객체를 저장한다
  • 람다를 통해 상태를 가진 함수를 만들 수 있다
  • 객체와 결합된 콜백 구조를 만들 수 있다
  • 함수 포인터보다 유연하지만, 약간의 비용이 있다

🎯 다음 글 예고

다음 글에서는:

👉 C++ 델리게이트 시스템 직접 구현

  • 싱글 캐스트
  • 멀티 캐스트
  • 이벤트 바인딩 구조

👉 게임 엔진에서 사용하는 구조를 직접 만들어보겠습니다.


🎮 마무리

함수 포인터가 “함수 실행”의 시작이었다면,
👉 std::function“유연한 콜백 시스템”의 시작입니다.

이제 다음 단계는:

👉 델리게이트 시스템으로 확장입니다.


👍 한 줄 정리

👉 “std::function은 ‘상태를 가진 함수’를 만들기 위한 도구입니다”


🚀 한 단계 더 나아가고 싶다면

콜백 구조와 이벤트 시스템은
게임 엔진의 핵심 구성 요소입니다.

👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

단순히 사용하는 것을 넘어서,
엔진이 어떻게 동작하는지 이해하는 데 초점을 맞춘 내용입니다.

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다