[C++][UE5] 델리게이트 람다 캡처 This에 약한참조를 쓰기 (TWeakObjectPtr, AddLambda)

ChangJin·2025년 5월 27일
0

Unreal Engine5

목록 보기
121/122
post-thumbnail

글에 사용된 모든 그림과 내용은 직접 실험하고 작성한 것입니다.


🎯 글의 목적

언리얼 엔진에서 델리게이트에 람다를 사용할 때 객체 생명 주기 문제를 방지하기 위해 TWeakObjectPtr을 캡처에 사용하는 방식을 정리하기 위함. 또한 C++17 이후의 람다 문법과 함께, TWeakObjectPtr을 사용하는 이유를 정리하기 위함.


🔎 알게 된 점

  1. AddLambda를 사용할 때 UObject를 직접 캡처하면, 해당 객체가 소멸됐음에도 호출되어 크래시 위험이 있다.
  2. TWeakObjectPtrIsValid() 체크를 통해 안전하게 접근할 수 있다. (스마트 포인터의 weak_ptr과 비슷하다)
  3. 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를 지원합니다.



언리얼에서의 TWeakObjectPtr

다음처럼 해시값으로 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()객체가 유효한지 확인
AddLambdathis를 직접 쓰지 말고 반드시 TWeakObjectPtr로 감싸서 사용
대상UI 위젯, GameMode, GameInstance, 서버 응답 처리 전반
사용 시점HTTP 요청, Timer, Async Load, FindSession, SaveGame 등 비동기 흐름 전반

참고 문서

profile
게임 프로그래머

0개의 댓글