왜 Delegate를 인자로 사용하지 못할까?
간단한 내부 구조
DECLARE_DELEGATE_OneParam과 같이 선언한 Delegate 타입은 함수 포인터 + 캡처 객체주소를 포함하는 구조이다.
MyDelegate.AddUObject(this, &AClassName::FunctionName);
다음과 같이 사용한다면 AClassName이 함수 포인터(힙에 할당)이며 this가 캡처 객체 인스턴스 주소이다.
즉, 내부적으론 어떤 함수를 가리키는 포인터, 그 함수가 멤버 함수라면 해당 객체에 대한 포인터가 있다.
또한 바인딩 상태, 종류(멤버, 람다, 정적), 등록 해제 상태 등 다양한 값들이 관리가 된다.
그래서 왜 못하는가?
Delegate는 내부적으로 함수 포인터 정보(힙 메모리) + 캡처 객체 정보로 이루어져있다.
이러한 내부 구조 때문에 복사를 하면 동일한 힙 메모리 주소를 2개의 인스턴스가 동시에 참조하게 된다.
이때 하나의 Delegate에서 Unbind()를 하면 다른 하나는 이미 잘못된 주소를 가르킨다.
그러면 그 이후 Execute()호출 시 Crash가 날 수 있다.
이를 방지하기 위해 Unreal에서 Delegate의 복사 생성자를 막아놓았다.
또한 Delegate는 Move Semantics에도 제한적이다.
Move는 내부적으로 소유권 이전이 일어나는데
Delegate 내부엔 힙 리소스, 바인딩 정보 등 다양한 값을 가지고 있는 상태에서 제대로 이전되지 않으면
복사와 마찬가지로 댕글링 포인터나 Crash등 에러가 발생할 수 있다.
특히 Multicast Delegate는 여러 콜백 list를 가지고 있는데 이걸 Move로 옮기다가 레퍼런스 카운트가 꼬이면 Broadcast 호출 시 문제가 발생할 수 있다.
일부 Delegate는 Move가 가능하지만 일반적인 Delegate는 Move까지 복사생성자를 막아놓았다.
위와 같은 이유로 Delegate는 복사나 할당 연산이 되지 않기에 TArray에 넣으려고 하면 컴파일 에러가 난다.
(TArray 는 내부적으로 복사/이동 생성자를 요구하기 때문이다.)
Delegate를 배열의 인자로 만드는 방법
위의 설명대로 Delegate자체는 복사/배열화가 불가능하다.
그렇기에 참조를 관리하거나 포인터 형태로 간접적으로 관리해야한다.
DECLARE_DELEGATE_OneParam(FMyDelegate, float /*num*/);
USTRUCT(BlueprintType)
struct FMyDelegateWrapper
{
GENERATED_BODY()
FMyDelegateWrapper() {}
FMyDelegateWrapper(const FMyDelegate& InItemDelegate) : ItemDelegate(InItemDelegate) {}
FMyDelegate ItemDelegate;
};
다음과 같이 MyDelegate를 Wrapper 구조체로 만들면 배열화가 가능하다.
구조체 안의 멤버로 있을 때는 MoveConstructible(Move 생성자를 가지고 있는 것)한 상태로 관리가 되며
TArray는 구조체 단위로 Move 또는 Placement New를 사용하기에 Delegate가 포함되어도 사용이 가능하다.
결국 Delegate를 직접 인자로 넣거나 배열화를 하는 것은 불가능하기에 Wrapper 구조체를 만들어서
해당 구조체로 배열을 만들어야한다.
배열은 그냥 배열 만들듯이 만들면 된다.
// 헤더
UPROPERTY()
TArray<FMyDelegateWrapper> DelegateArray;
배열에 Delegate를 추가하는 방법이다.
MoveTemp는 복사를 하지 말고 Move 생성자로 옮겨라는 의미이다.
만약 없다면 Delegate의 복사 생성자는 delete로 되어있어서 컴파일 에러가 난다.
// 예: 배열에 Delegate 추가
FMyDelegate MyDelegate;
MyDelegate.BindLambda([](float num)
{
UE_LOG(LogTemp, Log, TEXT("Delegate called with num: %f"), num);
});
// 배열에 Push 시에는 Move로 넣어야 안전함!
DelegateArray.Add(FMyDelegateWrapper(MoveTemp(MyDelegate)));
------------------------------------------------
//다른 예시로 새로 생성해서 넣는 방법
DelegateArray.Add(FMyDelegateWrapper(MyDelegate::CreateUObject(this, &MyClass:Funcion)));
이제 Delegate에 있는 함수가 IsBound로 유효한 함수에 바인딩 되어있는지 확인하고
모두 다 유효하다는 가정하에 DelegateArray에 있는 모든 Delegate에 42.0의 숫자를 넣어서 Delegate에 등록된 함수를 실행한다.
for (const auto& Wrapper : DelegateArray)
{
if (Wrapper.ItemDelegate.IsBound())
{
Wrapper.ItemDelegate.Execute(42.0f); // 예시 숫자
}
}
'Unreal 게임 개발 > Unreal C++ 공부' 카테고리의 다른 글
게임 시작 시 포커스가 뷰포트로 들어가게 하기 (0) | 2025.03.21 |
---|---|
경로기반 Class & Object 정보를 들고오는 방법 (0) | 2025.03.21 |
Unreal StaticClass() (0) | 2024.10.03 |
C++ 람다 표현식 (Lambda expression) (0) | 2024.09.05 |
Unreal Module 추가 방법 (0) | 2023.01.26 |