글에 사용된 모든 그림과 내용은 직접 실험하고 작성한 것입니다.
언리얼 엔진에서 델리게이트에 람다를 사용할 때 객체 생명 주기 문제를 방지하기 위해
TWeakObjectPtr
을 캡처에 사용하는 방식을 정리하기 위함. 또한 C++17 이후의 람다 문법과 함께,TWeakObjectPtr
을 사용하는 이유를 정리하기 위함.
AddLambda
를 사용할 때 UObject를 직접 캡처하면, 해당 객체가 소멸됐음에도 호출되어 크래시 위험이 있다.TWeakObjectPtr
은IsValid()
체크를 통해 안전하게 접근할 수 있다. (스마트 포인터의 weak_ptr과 비슷하다)TWeakObjectPtr<T> WeakThis = this;
는T*
를 람다에서 안전하게 사용하는 방법이다.
사용자가 로그인 버튼을 누르면 서버에 로그인 요청을 보내고,
일정 시간이 지난 후 서버에서 응답을 받아 UI에 메시지를 띄우는 로직 중 일부입니다.
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->OnProcessRequestComplete().BindLambda(
[this](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSuccess)
{
if (bSuccess && Resp->GetResponseCode() == 200)
{
this->ShowSuccessMessage(); // 위험: this가 이미 파괴되었을 수도 있음
}
});
Request->SetURL("https://api.myserver.com/login");
Request->SetVerb("POST");
Request->ProcessRequest();
이 방법은 매우 위험합니다. 요청 중인 해당 객체가 어떠한 이유에 의해서 파괴됐다면 this에 접근하는 것 자체가 오류를 일으키기 때문입니다. 특히나 서버의 응답을 클라이언트가 기다리고 있을 때는 이처럼 단순히 객체의 함수를 델리게이트로 등록해주면 안됩니다.
this
를 직접 캡처하면, 람다 실행 시점에 이미 위젯이 파괴된 상태일 수 있음- 특히 Async Task / Timer / Delegate에서 매우 위험
TWeakObjectPtr
사용TWeakObjectPtr<ULoginWidget> WeakThis = this;
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->OnProcessRequestComplete().BindLambda(
[WeakThis](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSuccess)
{
if (WeakThis.IsValid())
{
if (bSuccess && Resp->GetResponseCode() == 200)
{
WeakThis->OnLoginSuccess();
}
else
{
WeakThis->OnLoginFailed();
}
}
});
Request->SetURL("https://api.myserver.com/login");
Request->SetVerb("POST");
Request->ProcessRequest();
C++의 스마트 포인터인 weak_ptr과 사용법은 같습니다. 언리얼에서는 이를 TWeakObjectPtr로 새롭게 만들어 두었습니다. C++의 weak_ptr은 shared_ptr로 만들어진 객체에반 사용할 수 있고, 참조 카운터를 사용해 안전하게 접근합니다. 언리얼의 TWeakObjectPtr는 UObject 기반의 모든 객체를 대상으로 하고 GC를 지원합니다.
다음처럼 해시값으로 SerialNumber를 저장하고 Get()을 했을 때 GUObjectArray에서 직접 인덱스를 사용해 UObject* 조회한 후 가져오게 됩니다.
/**
* Returns true if two weak pointers were originally set to the same object, even if they are now stale
* @param Other weak pointer to compare to
*/
FORCEINLINE bool HasSameIndexAndSerialNumber(const TWeakObjectPtr& Other) const
{
return static_cast<const TWeakObjectPtrBase&>(*this).HasSameIndexAndSerialNumber(static_cast<const TWeakObjectPtrBase&>(Other));
}
/**
* SetKeyFuncs for TWeakObjectPtrs which allow the key to become stale without invalidating the set.
*/
template <typename ElementType, bool bInAllowDuplicateKeys = false>
struct TWeakObjectPtrSetKeyFuncs : DefaultKeyFuncs<ElementType, bInAllowDuplicateKeys>
{
typedef typename DefaultKeyFuncs<ElementType, bInAllowDuplicateKeys>::KeyInitType KeyInitType;
static FORCEINLINE bool Matches(KeyInitType A, KeyInitType B)
{
return A.HasSameIndexAndSerialNumber(B);
}
static FORCEINLINE uint32 GetKeyHash(KeyInitType Key)
{
return GetTypeHash(Key);
}
};
FORCEINLINE bool IsValid(bool bEvenIfPendingKill, bool bThreadsafeTest = false) const
{
return TWeakObjectPtrBase::IsValid(bEvenIfPendingKill, bThreadsafeTest);
}
TimerHandle
과의 조합 예제void ALobbyGameMode::UpdateAllConnectedClientInfoWithDelay(float delay)
{
FTimerHandle TimerHandle;
TWeakObjectPtr<ALobbyGameMode> WeakThis = this;
GetWorld()->GetTimerManager().SetTimer(
TimerHandle,
[WeakThis]()
{
if (WeakThis.IsValid())
{
WeakThis->UpdateAllClientsLobbyInfo();
}
},
delay,
false
);
}
TimerManager
는 Tick마다 델리게이트를 확인하지 않기 때문에** 지연 이후 호출 시점에 객체가 살아있는지 반드시 확인해야 합니다.
void ULoginWidget::OnLoginButtonClicked()
{
TWeakObjectPtr<ULoginWidget> WeakThis = this;
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->OnProcessRequestComplete().BindLambda(
[WeakThis](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSuccess)
{
if (WeakThis.IsValid())
{
if (bSuccess && Resp->GetResponseCode() == 200)
{
WeakThis->OnLoginSuccess();
}
else
{
WeakThis->OnLoginFailed();
}
}
});
Request->SetURL("https://api.myserver.com/login");
Request->SetVerb("POST");
Request->ProcessRequest();
ShowLoadingIndicator();
}
항목 | 설명 |
---|---|
TWeakObjectPtr | 객체 생존 여부 확인용. 델리게이트에서 안전하게 캡처 |
.IsValid() | 객체가 유효한지 확인 |
AddLambda | this 를 직접 쓰지 말고 반드시 TWeakObjectPtr 로 감싸서 사용 |
대상 | UI 위젯, GameMode, GameInstance, 서버 응답 처리 전반 |
사용 시점 | HTTP 요청, Timer, Async Load, FindSession, SaveGame 등 비동기 흐름 전반 |