개요
삭제된 객체의 함수가 바인딩된 델리게이트를 실행했을 때, 크래쉬가 나는 경우가 있다. 이 문제를 해결하기 위해서는 어떻게 해야될까? 그리고 두 델리게이트 바인딩 방법의 차이점을 알아보자...
결론
동적 바인딩 | Blueprint와 연동 | 크래쉬 위험 (IsBound 함수를 사용하지 않을 때) | 리플렉션 | |
---|---|---|---|---|
BindUFunction | 런타임에 함수 이름을 결정해야 할 경우 | 가능 | 있음 | UFunction 이기 때문에 가능 |
CreateUObject | 컴파일 타임에 함수가 결정되어야 함 | 불가능 | 없음 | 네이티브 C++ 함수이기 때문에 불가능 |
- Blueprint/동적 바인딩: BindUFunction
- 단순 네이티브 바인딩: CreateUObject
- IsBound 는 꼭! 사용하자
BindUFunction 바인딩
void ADelegateTestActor::StartDangerousTest()
{
UE_LOG(LogTemp, Warning, TEXT("=== 위험한 테스트 시작 (직접 바인딩) ==="));
TargetObj = GetWorld()->SpawnActor<ATargetObject>();
UE_LOG(LogTemp, Warning, TEXT("BindUFunction 으로 직접 바인딩 완료"));
DirectBindDelegate.BindUFunction(TargetObj, FName("OnDelegateCallback"));
UE_LOG(LogTemp, Warning, TEXT("타겟 객체 삭제 ..."));
TargetObj->Destroy();
UE_LOG(LogTemp, Warning, TEXT("DirectBindDelegate 델리게이트 호출 시도..."));
if (!DirectBindDelegate.IsBound()) // IsBound 함수는 델리게이트 내부의 UserObjectPtr (Object 의 WeakPtr) 가 IsValid 한지 체크 한다.
{
UE_LOG(LogTemp, Warning, TEXT("DirectBindDelegate 의 UserObjectPtr is Not Valid"));
}
DirectBindDelegate.Execute(123);
UE_LOG(LogTemp, Warning, TEXT("Execute 하면 Crash 발생"));
}
- 객체 삭제 후, 델리게이트의 UserObjectPtr은 여전히 0x10ABC123 값을 가지고 있음 (포인터 값 자체는 변경되지 않음)
- 0x10ABC123 주소의 상태: 이미 해제된 메모리
- WeakPtr의 내부 상태: Invalid (참조 카운트로 객체 삭제 감지)
RetValType Execute(ParamTypes... Params) const final
{
using FParmsWithPayload = TPayload<RetValType(typename TDecay<ParamTypes>::Type..., typename TDecay<VarTypes> ::Type...)>;
checkSlow(IsSafeToExecute());
TPlacementNewer<FParmsWithPayload> PayloadAndParams;
this->Payload.ApplyAfter(PayloadAndParams, Forward<ParamTypes>(Params)...);
UserObjectPtr->ProcessEvent(CachedFunction, &PayloadAndParams);
return PayloadAndParams->GetResult();
}
- DirectBindDelegate 델리게이트는 위의 Execute 를 실행
- 하지만 UserObjectPtr 은 이미 해제된 메모리에 접근하여 크래쉬 발생
❓ 왜 언리얼에서는 Crash가 날 수 있는 코드를 작성했을까?
- 언리얼은
UFUNCTION
시스템을 통해 Blueprint나 C++ 모두에서 함수 호출을 가능하게 함. - 이를 실제로 호출하는 방식이 바로
UObject::ProcessEvent()
- 이때
CreateUObject
의Execute
와 다르게ProcessEvent
함수를 호출함.TDelegate::CreateUObject
는 일반적인 C++ 객체 함수 포인터를 안전하게 바인딩- 하지만 UFUNCTION 기반 Delegate는
ProcessEvent
를 사용하여 호출
- ProcessEvent 는 기본적으로 유효한 UObject 에서 호출 가능하기 때문에 당연히 유효성 검증은 프로그래머가 실시하도록 만들어진 함수
- 왜 유효한 UObject 인지 직접 검사하지 않는 이유는 성능 상 문제일 가능성이 높음
- UFUNCTION 은 (애니메이션, 타이머, 블프 이벤트 등등) 많이 호출되기 때문에
ProcessEvent
가 있는 모든 곳에 유효성을 검사하는 로직을 추가하면 비용이 클 것으로 보임 - 실제로
AnimNotify
도 캐릭터가 사라지면 Crash 가 가능함.
CreateUObject 바인딩
void ADelegateTestActor::StartSafeTest()
{
UE_LOG(LogTemp, Warning, TEXT("=== 안전한 테스트 시작 (CreateUObject) ==="));
TargetObj = GetWorld()->SpawnActor<ATargetObject>();
UE_LOG(LogTemp, Warning, TEXT("CreateUObject 바인딩 완료"));
CreateUObjectDelegate = FTestDelegateCreateUObject::CreateUObject(
TargetObj, &ATargetObject::OnDelegateCallback);
UE_LOG(LogTemp, Warning, TEXT("타겟 객체 삭제 ..."));
TargetObj->Destroy();
UE_LOG(LogTemp, Warning, TEXT("CreateUObject 델리게이트 호출 시도..."));
if (!CreateUObjectDelegate.IsBound()) // IsBound 함수는 델리게이트 내부의 UserObjectPtr (Object 의 WeakPtr) 가 IsValid 한지 체크 한다.
{
UE_LOG(LogTemp, Warning, TEXT("CreateUObjectDelegate 의 UserObjectPtr is Not Valid"));
}
CreateUObjectDelegate.Execute(456);
UE_LOG(LogTemp, Warning, TEXT("하지만 Execute 해도 Crash 나지 않는다."));
}
- 객체 삭제 후, 델리게이트의 UserObjectPtr은 여전히 0x10ABC123 값을 가지고 있음 (포인터 값 자체는 변경되지 않음)
- 0x10ABC123 주소의 상태: 이미 해제된 메모리
- WeakPtr의 내부 상태: Invalid (참조 카운트로 객체 삭제 감지)
RetValType Execute(ParamTypes... Params) const final
{
using MutableUserClass = std::remove_const_t<UserClass>;
// Verify that the user object is still valid. We only have a weak reference to it.
checkSlow(UserObject.IsValid());
// Safely remove const to work around a compiler issue with instantiating template permutations for
// overloaded functions that take a function pointer typedef as a member of a templated class. In
// all cases where this code is actually invoked, the UserClass will already be a const pointer.
MutableUserClass* MutableUserObject = const_cast<MutableUserClass*>(UserObject.Get());
checkSlow(MethodPtr != nullptr);
return this->Payload.ApplyAfter(MethodPtr, MutableUserObject, Forward<ParamTypes>(Params)...);
}
- CreateUObjectDelegate 은 위의 Execute 를 실행
- 여기서 UserObject가 유효한지 체크
- 만약 객체가 삭제되면 UserObject.Get() 에서 Null 반환
- 내부적으로 WeakPtr 의 Get 을 호출
- 계속 들어가보면... 유효한 오브젝트 아니면 nullptr 반환
[[nodiscard]] FORCEINLINE T* Get(bool bEvenIfPendingKill) const
{
return (T*)WeakPtr.Get(bEvenIfPendingKill);
}
// 계속 들어가보면... 유효한 오브젝트 아니면 nullptr 반환
FORCEINLINE_DEBUGGABLE UObject* Internal_Get(bool bEvenIfGarbage) const
{
FUObjectItem* const ObjectItem = Internal_GetObjectItem();
return ((ObjectItem != nullptr) && GUObjectArray.IsValid(ObjectItem, bEvenIfGarbage)) ? (UObject*)ObjectItem->GetObject() : nullptr;
}
- 따라서 직접적인 댕글링 포인터를 사용할 일이 없음.