Delegate
Delegate
‘대리자’라는 개념을 가졌으며
기본적으로 C++에서 별도의 키워드로 분류되지는 않지만
‘함수 포인터’를 다른 곳으로 넘겨주어
필요에 따라 ‘호출’을 ‘위임’하는 구조
‘Callback’의 객체지향적 표현이다
함수 포인터
일반적인 함수 포인터
void Hello(int x) {
std::cout << "Hello " << x << std::endl;
}
void Run(void (*func)(int)) {
func(42);
}
Run(Hello); // Hello 42
---
멤버 함수 포인터
class MyClass {
public:
void Say(int x) { std::cout << "MyClass: " << x << std::endl; }
};
void Call(MyClass* obj, void (MyClass::*method)(int)) {
(obj->*method)(100);
}
MyClass mc;
Call(&mc, &MyClass::Say); // MyClass: 100
C,C++ 에서 Delegate 등을 구현하기 위해 사용
가볍고 빠른 특징이 존재
‘함수’ 역시 메모리 안에 존재하기에
그들을 ‘호출’하기 위하여 ‘주소값’이 필요하다
람다 함수
auto f = [](int x) { std::cout << "Lambda: " << x << std::endl; };
f(20);
int y = 5;
auto g = [y](int x) { std::cout << x + y << std::endl; }; // y 복사 캡처
함수를 클래스 등에 선언하지 않고
간편하게 만들어 사용하는 방식
함수 인자나 콜백 용도 등으로 사용
- [&] : 캡쳐 방식을 통해 외부 변수를 참조로 받을 수 있지만
엄연히 다른 메모리로 관리되기에
람다의 수명이 길어지거나, 멀티스레드 환경에선
댕글리 포인터의 위험이 있음을 염두할 것
(이럴꺼면 WeakPtr을 외부에 선언하고 직접 변수이름으로 건네주자)
람다는 내부적으로 익명 구조체 자동으로 만들고
그 구조체의 Operater()를 오버로드된다
cppreference
int x = 10;
auto f = [x](int y) { return x + y; };
=> (컴파일러가 익명의 클래스를 만든다)
struct __Lambda_123 {
int x;
int operator()(int y) const {
return x + y;
}
};
__Lambda_123 f = {10};
따라서 람다 전달은 다음과 같다
- 람다 함수 선언시, 컴파일러가 컴파일 타임에 Code 영역에 구조체 정의
- auto a 등으로 람다 함수의 인스턴스를 받음
- 이후 다른 함수에 전달 시, 해당 인스턴스를 복사,참조 등으로 넘겨줌
- 다만 std::function로 저장하는 방식은 heap 영역에 저장
람다 함수는 클래스에서 직접 함수 선언하기엔 너무 가벼울 때,
부담 없이 사용이 가능한 일종의 표현식(expression)
Unreal 에선?
여러모로 자주 쓰인다
Unreal의 다양한 이벤트 시스템의 기반이 된다
예시로 충돌 검사에 많이 사용하는
BeginOverlap을 들자면
UPROPERTY (BlueprintAssignable, Category="Collision")
FComponentBeginOverlapSignature OnComponentBeginOverlap
DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams(
FComponentBeginOverlapSignature,
UPrimitiveComponent*, OverlappedComponent,
AActor*, OtherActor,
UPrimitiveComponent*, OtherComp,
int32, OtherBodyIndex,
bool, bFromSweep,
const FHitResult &, SweepResult
);
처럼 다이나믹 멀티캐스트 Delegate로 선언되어 있다
(f는 보통 Delegate의 선언에 접두사로 붙인다)
기본적으로 ‘매크로’를 통해 선언하며
Dynamic 이나 MultiCast를 붙여 타입을 구분하고
이후 OneParam 등으로 매개변수의 개수를 선언한다
Delegate의 종류
정적 Delegate
컴파일 시간에 타입이 결정, BP와 연동하지 않는 C++용 Delegate
타입 | 멀티캐스트 | 블루프린트 연동 | 사용 예 |
---|---|---|---|
DECLARE_DELEGATE |
❌ 단일 바인딩 | ❌ | 간단한 콜백 |
DECLARE_DELEGATE_OneParam ~ _FiveParams |
❌ | ❌ | 인자 있는 단일 콜백 |
DECLARE_MULTICAST_DELEGATE |
✅ | ❌ | 여러 함수에 이벤트 브로드캐스트 |
DECLARE_DELEGATE(FMySimpleDelegate);
FMySimpleDelegate OnSomething;
OnSomething.BindUObject(this, &UMyClass::HandleIt);
OnSomething.ExecuteIfBound();
동적 Delegate
리플렉션 시스템과 연동하여 BP에서 사용 가능, 등록할 함수들은 UFUNCTION()이 필요
(없으면 컴파일 에러 발생)
(BP에서 호출하기 위하여 타입 정보가
필요하기에 리플렉션 기능을 이용해야 함)
타입 | 멀티캐스트 | 블루프린트 연동 | 특징 |
---|---|---|---|
DECLARE_DYNAMIC_DELEGATE |
❌ | ✅ | 단일 바인딩, UFUNCTION 필수 |
DECLARE_DYNAMIC_MULTICAST_DELEGATE |
✅ | ✅ | BP에서 Add Event 로 바인딩 가능 |
DECLARE_DYNAMIC_DELEGATE_OneParam ~ _TenParams |
❌ | ✅ | 다양한 인자 버전 제공 |
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDamageTaken, float, Damage);
UCLASS()
class UMyComponent : public UActorComponent {
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FOnDamageTaken OnDamaged;
};
// C++ 호출
OnDamaged.Broadcast(42.f);
// BP에서도 바인딩 가능
이벤트 Delegate
외부에서 Broadcast() 할수 없고, Add만 가능
타입 | 설명 |
---|---|
DECLARE_EVENT(OwnerType, EventName) |
Multicast Delegate 의 래퍼 |
DECLARE_EVENT_OneParam 등 |
인자 있는 버전 |
class AMyActor : public AActor {
DECLARE_EVENT(AMyActor, FOnFireEvent)
FOnFireEvent& OnFire() { return FireEvent; }
private:
FOnFireEvent FireEvent;
};
Delegate 함수들
함수 | 설명 |
---|---|
BindUObject() |
UObject 기반 클래스 멤버 함수에 바인딩 (GC 안전) |
BindStatic() |
정적 함수, 전역 함수 바인딩 |
AddUObject() |
멀티캐스트 Delegate에 바인딩 |
AddLambda() |
람다 바인딩 (주의: GC 추적 불가)(정적 Delegate만 사용가능) |
Unbind() , Remove() |
Delegate 해제 |
Broadcast() |
멀티캐스트 호출 |
Execute() , ExecuteIfBound() |
단일 Delegate 호출 |
BindUObject()
단일 Delegate에 UObject 기반 클래스 멤버 함수를 바인딩
GC와의 연동을 위해 UObject 추적이 가능하도록 설계됨
DECLARE_DELEGATE(FOnPressed);
FOnPressed OnPressed;
OnPressed.BindUObject(this, &UMyClass::HandlePressed); // this는 UMyClass* 타입으로 캐스팅 가능해야 함
BindStatic()
정적 함수(static Function)이나 ‘전역 함수’ 바인딩 용
void StaticFunc() { UE_LOG(LogTemp, Log, TEXT("Static Called")); }
OnPressed.BindStatic(&StaticFunc);
일반적인 멤버 함수는 바인딩할 수 없고
static 키워드가 붙은 멤버 함수나
전역 함수만이 등록이 가능하다
일반적으로는 유틸리티용 콜백으로 주로 사용
(정적 Delegate만 사용 가능)
AddUObject()
멀티캐스트용 Delegate에 UObject 기반 클래스 멤버 함수를 바인딩
여러 함수를 바인딩 가능
DECLARE_MULTICAST_DELEGATE(FOnEvent);
FOnEvent OnSomethingHappened;
OnSomethingHappened.AddUObject(this, &UMyClass::NotifySomething);
- 바인딩한 객체 소멸 시, Delegate가 해당 함수 바인딩을 제거하여 안전하게 동작
AddLambda()
람다 함수를 Delegate에 바인딩
편리하지만 UObject 추적이 되지 않기에
GC와 관련없는 람다만 사용하거나 WeakPtr로 캡쳐하는 것이 안전
(정적인 Delegate에서만 사용 가능)
OnSomethingHappened.AddLambda([]() {
UE_LOG(LogTemp, Log, TEXT("Lambda Triggered"));
});
-
람다 내부에 [this] 같은 UObject 기반이 캡쳐되면
위 쪽의 [&]와 비슷하게 댕글리 포인터와 같은 일이 벌어질 수 있음
따라서 WeakPtr 등을 사용하여 안전하게 사용을 권장함 -
또한 사용 후, Clear()나 RemoveAll() 등을 호출하여 수동 제거를 권장
Unbind(), Clear(), Remove(), RemoveAll()
Delegate 또는 특정한 Bind를 해제
// 단일 Delegate
OnPressed.Unbind();
// 멀티캐스트 Delegate 전체 제거
OnSomethingHappened.Clear();
// 핸들을 멤버 변수 등으로 저장하는 것이 일반적
FDelegateHandle MyHandle = OnSomethingHappened.AddUObject(this, &UMyClass::HandleEvent);
// 멀티캐스트 특정 핸들의 바인드를 제거함
OnSomethingHappened.Remove(MyHandle);
// 멀티캐스트 특정 객체의 바인드만 전부 제거
OnSomethingHappened.RemoveAll(this);
BroadCast()
MultiCast Delegate의 바인딩된 모든 함수를 호출
OnSomethingHappened.Broadcast(); // 모든 AddUObject / AddLambda 함수 호출됨
- 한번만 호출
Execute() / ExecuteIfBound()
단일 Delegate 호출
OnPressed.Execute(); // 바인딩된 함수 호출
OnPressed.ExecuteIfBound(); // 안전 확인 후 호출 (권장)
-
단일 Delegate는 ‘바인딩’된 함수가 없는 상황에서 Execute() 호출시 크래시 발생
따라서 IsBound()로 체크하거나 ExecuteIfBound()를 사용을 권장
(+ Multicast는 BroadCast()를 사용하며 이는 바인딩과 관련없이 크래시를 발생시키지 않음) -
바인딩 객체 소멸시, Delegate가 무효화되기에 IsBound()를 사용하는 이유임
함수 정리
함수명 | 설명 | 정적 Delegate (DECLARE_* ) |
동적 Delegate (DECLARE_DYNAMIC_* ) |
---|---|---|---|
BindUObject() |
UObject 멤버 함수 바인딩 (GC 추적) |
✅ 가능 | ✅ 가능 (AddDynamic() 내부적으로 사용) |
BindStatic() |
정적 함수 / 전역 함수 바인딩 | ✅ 가능 | ❌ 불가 (UFunction 아님) |
AddUObject() |
멀티캐스트 Delegate에 UObject 바인딩 |
✅ 가능 | ❌ (AddDynamic() 을 써야 함) |
AddLambda() |
람다 함수 바인딩 (비-GC 추적) | ✅ 가능 | ❌ 불가 (리플렉션 시스템 호출 불가) |
Unbind() / Remove() |
Delegate 해제 (핸들 기반 제거 포함) | ✅ 가능 | ✅ 가능 (RemoveDynamic() 도 있음) |
Broadcast() |
모든 바운드 대상 호출 (Multicast에서만) | ✅ 가능 | ✅ 가능 |
Execute() / ExecuteIfBound() |
단일 Delegate 호출 | ✅ 가능 | ✅ 가능 (ExecuteIfBound() 이 안정적) |
또한 람다의 바인딩 대상이 되는 경우에 대하여 정리하자면 이렇다
바인딩 대상 | 정적 Delegate | 동적 Delegate | 설명 |
---|---|---|---|
전역 함수 / static 함수 | ✅ 가능 | ❌ 불가 | UObject 기반이 아닌 별도의 함수 위치를 가짐 |
UObject 멤버 함수 | ✅ 가능 | ✅ 가능 | 단, 동적은 UFUNCTION() 필수 |
람다 함수 | ✅ 가능 | ❌ 불가 | 정적 Delegate에서만 가능 |
- 동적 Delegate는 UObject 인스턴스가 기반이 되어 호출함을 기억해두자
UObject 인스턴스 ----> 클래스 메타데이터 ----> UFunction(FName 매칭) ----> 호출 (ProcessEvent)
▲
│
동적 Delegate
댓글남기기