TWeakObjectPtr<> 은 GC Referncing 은 하지 않으면서 Object 가 유효한지 확인하고, 유효하면 값을 가져 올 수 있는 기능을 제공한다. 이름 그대로 약한 참조이며 C# 의 WeakReference 와 동일 하다. 즉 GC를 막지는 않으면서 값은 안전하게 확인후 가져올 수 있는 기능을 제공한다.
이번 포스팅에서는
등에 대해서 살펴 보겠다. 들어가기 전에 에 항상 언급하듯이 UE5.4.4 기준이며 다른 버전에서는 궁금할때 따로 확인하도록 하자. 하지만 크게 다르지는 않을 것이다.
당연히 TWeakPtr<...> 와는 아무 관련이 없다.
FWeakObjectPtr<...> 을 위해서 GUObjectArray: FUObjectArray 가 온 힘을 다해서 도와주는 형상이다.

FUObjectArray 는 global 변수 GUObjectArray 의 타입으로 엔진에 존재하는 모든 UObject 를 Cluster Chunk 로 나뉘어져 있는 Array 형태로 관리한다. UObjectBase 에 있는 int32 InternalIndex 가 그 Array 에서의 Index 를 나타 낸다.
IndexToObject 를 통해서 InternalIndex 로 부터 UObjectBase 로 가져 올 수 있는데 중간에 FUObjectItem 이 Wrapper 로 한번 감싸서 가지고 있다. 이중 눈여겨 볼 부분은 FUObjectItem::SerialNumber 이다. 이 SerialNumber 가 FWeakObjectPtr 가 UObject 로참조를 걸때 FUObjectArray::AllocateSerialNumber(...) 로 받아 오는 값이며, FUObjectArray 는 SerialNumber 를 FUObjectItem 에 같이 저장하게 된다.
UMyObject* NewObject = NewObject<UMyObject>(this);
TWeakObjectPtr<UMyObject> WeakReferencedObject = NewObject;
결국 위와 같이 하고 나면 WeakReferencedObject 에 저장 되는 값 딱 2개는
ObjectIndex - NewObject 의 InternalIndex ObjectSerialNumber - NewObject 를 감싸고 있는 FUObjectItem 에 할당된 SerialNumber이다.
FORCEINLINE_DEBUGGABLE bool FWeakObjectPtr::SerialNumbersMatch(FUObjectItem* ObjectItem) const
{
const int32 ActualSerialNumber = ObjectItem->GetSerialNumber();
ObjectSerialNumber); // serial numbers should never shrink
return ActualSerialNumber == ObjectSerialNumber;
}
WeakObjectPtr 은 Get(...) 이나 IsValid(...) 에서 유효성 검사를 할떄는, 자신이 가지고 있는 ObjectIndex 에 해당하는 FUObjectItem 이 저장하고 있는 SerialNumber 와 자신이 참조를 걸때 땡겨둔 ObjectSerialNumber 가 일치 하는지만 확인 하면 된다. 끝.
여기서 중요한점은 SerialNumber 가 언제 Counting 되고 범위는 어디냐는 것이다. 간략하게나마 살펴보자.
우선 Weak 로 참조가 걸릴때 작동되는 Code 이다.
void FWeakObjectPtr::operator=(const class UObject *Object)
{
if (Object // && UObjectInitialized() we might need this at some point, but it is a speed hit we would prefer to avoid
)
{ ObjectIndex = GUObjectArray.ObjectToIndex((UObjectBase*)Object);
ObjectSerialNumber = GUObjectArray.AllocateSerialNumber(ObjectIndex);
checkSlow(SerialNumbersMatch());
}
else
{
Reset();
}}
Code 를 보면 WeakObj 로 참조가 걸릴떄 만 GUObjectArray.AllocateSerialNumber 가 호출되는 것을 알 수 있다.
AllocateSerialNumber 는 아래와 같이 되어있다.
int32 FUObjectArray::AllocateSerialNumber(int32 Index)
{
FUObjectItem* ObjectItem = IndexToObject(Index);
checkSlow(ObjectItem);
volatile int32 *SerialNumberPtr = &ObjectItem->SerialNumber;
int32 SerialNumber = *SerialNumberPtr;
if (!SerialNumber)
{ // Open around PrimarySerialNumber as if we fail/abort a transaction we dont need to undo this, simply allow it to grow for the next use
UE_AUTORTFM_OPEN({
SerialNumber = PrimarySerialNumber.Increment();
UE_CLOG(SerialNumber <= START_SERIAL_NUMBER, LogUObjectArray, Fatal, TEXT("UObject serial numbers overflowed (trying to allocate serial number %d)."), SerialNumber);
int32 ValueWas = FPlatformAtomics::InterlockedCompareExchange((int32*)SerialNumberPtr, SerialNumber, 0);
if (ValueWas != 0)
{ // someone else go it first, use their value
SerialNumber = ValueWas;
}
});
}
checkSlow(SerialNumber > START_SERIAL_NUMBER);
return SerialNumber;
}
간략히 살펴 보면,
1. Index 에 해당하는 ObjectItem 을 가져오고
2. 해당 ObjectItem 에 SerialNumber 가 할당 된 적이 없으면 Thread-Safe 하게 PrimarySerialNumber 를 +1 시켜서 할당
3. SerialNumber 가 할당 된 적이 있으면 그대로 사용
ObjectItem 의 SerialNumber 가 0으로 청소되는 곳은, bool UObject::ConditionalFinishDestroy() 에서 호출하는 GUObjectArray.ResetSerialNumber(this); 이다. 따라서 GC(flag 도 확인하지만) 되고 나면 WeakObj 는 Invalid 함으로 처리 될 수 있다. TWeakObjectPtr 에 있는 ObjectIndex 가 가르키는 FUObjectItem 의 SerialNumber 가 일치하지 않기 때문이다.
위에서 짚고 넘어갈 중요한 점 세가지를 살펴보자.
즉, 아래 코드가 약 21억회 넘게 작동하면 UObject가 중간에 GC되어서 몇개가 존재하든지 관계 없이 오류 처리된다.
UMyObject* NewObject = NewObject<UMyObject>(this);
TWeakObjectPtr<UMyObject> WeakReferencedObject = NewObject;
충분 한 값이라면 값인데... 그래도 발생 가능성이 있다는 것은 알고 넘어가자.
Garbage Collection 포스팅 에서 AActor 의 경우 World(정확히는 Level)에서 참조하고 있으므로 AActor 는 따로 UPROPERTY(...) 가 아니어도 GC 되지 않음을 보았다. 그러면 UObject 계열이고 Blueprint 에서 접근할 필요가 없다고 가정 했을때, Destroy 가 호출되고 GC 가 되었음을 확인만 하면 되는데
UPROPERTY(...) TObjectPtr<AMyActor> MyActor;TWeakObjectPtr<AMyActor> WeakMyActor; static void AddReferencedObjects(....) 에서 처리위 셋중 어느게 가장 가볍고 빠를까? 하나씩 따져 보자.
따라서, Blueprint 에서 접근할 필요가 없으면 '3' 번이 가장 가볍다고 생각된다. 3번의 경우 값을 참조할 때 nullptr 인지만 확인하면 족하다. static void AddReferencedObjects 으로의 Pointing 은 UCLASS 당 하나만 저장되는 것을 기억하자.
그럼 TWeakObjectPtr<...> 은 언제 사용하는게 좋을까? 당연히 UObject* 참조 변수를 UObject 계열이든 아니든 GC를 막지는 않고 Valid 한지 확인해 가면서 사용할때는 다른 대안이 없다. 또한 AGameState, AGameMode 처럼 Application 생명 주기에서 새로 만들어지는 횟수가 적은것을 대상으로 할때는 더욱 좋을 것이다.