📚 델리게이트 시리즈
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과 람다는 단순 함수 호출을 넘어서,
상태와 객체를 함께 가지는 유연한 콜백 시스템을 만들 수 있다는 점입니다.

위 구조처럼 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은 ‘상태를 가진 함수’를 만들기 위한 도구입니다”
🚀 한 단계 더 나아가고 싶다면
콜백 구조와 이벤트 시스템은
게임 엔진의 핵심 구성 요소입니다.
단순히 사용하는 것을 넘어서,
엔진이 어떻게 동작하는지 이해하는 데 초점을 맞춘 내용입니다.