
UScriptStruct는 POD(Plain Old Data)로 사용된다는 점을 다시 한 번 상기하세요. 앞서 설명한 섹션에서는 ReturnStructParams를 통해 구조체에 집계되는 프로퍼티 타입의 생성이 어떻게 수집되는지 다루었습니다. 그런데 여기서 또 하나 흥미로운 템플릿 시스템이 도입됩니다.
#define UE_IMPLEMENT_STRUCT(PackageNameText, BaseName) \
static UScriptStruct::TAutoCppStructOps<F##BaseName> BaseName##_Ops(FTopLevelAssetPath(TEXT(PackageNameText), TEXT(#BaseName)));
이 매크로는 다음과 같이 분해됩니다.
/** Template for noexport classes to autoregister before main starts **/
template<class CPPSTRUCT>
struct TAutoCppStructOps
{
TAutoCppStructOps(FTopLevelAssetPath InName)
{
DeferCppStructOps(InName,new TCppStructOps<CPPSTRUCT>);
}};
여기서 핵심적으로 주목해야 할 부분은 DeferCppStructOps 호출입니다.
/** Stash a CppStructOps for future use
* @param Target Name of the struct
* @param InCppStructOps Cpp ops for this struct
**/
void UScriptStruct::DeferCppStructOps(FTopLevelAssetPath Target, ICppStructOps* InCppStructOps)
{
TMap<FTopLevelAssetPath, UScriptStruct::ICppStructOps*>& DeferredStructOps = GetDeferredCppStructOps();
if (UScriptStruct::ICppStructOps* ExistingOps = DeferredStructOps.FindRef(Target))
{
IReload* Reload = GetActiveReloadInterface();
if (Reload == nullptr)
{
check(ExistingOps != InCppStructOps); // 만약 같다면, 이미 만료된 포인터를 다시 맵에 추가하는 셈이 됨
delete ExistingOps;
}
else if (!Reload->GetEnableReinstancing(false))
{
delete InCppStructOps;
return;
}
// 리로드 중에는, 이 포인터들이 사용 중일 수 있으므로 그냥 누수시킴
}
DeferredStructOps.Add(Target, InCppStructOps);
}
DeferCppStructOps는 타입 정보를 수집하는 진입점 역할을 합니다. 즉, 이 함수에서 네이티브 타입이 래핑됩니다. 엔진 소스의 Property.cpp에서는 TStructOpsTypeTraits가 선언되어 있습니다.
template<typename T>
struct TIntPointStructOpsTypeTraits : public TStructOpsTypeTraitsBase2<T>
{
enum
{
WithIdenticalViaEquality = true,
WithNoInitConstructor = true,
WithZeroConstructor = true,
WithSerializer = true,
WithSerializeFromMismatchedTag = true,
};
static constexpr EPropertyObjectReferenceType WithSerializerObjectReferences = EPropertyObjectReferenceType::None;
};
template<> struct TStructOpsTypeTraits<FInt32Point> : public TIntPointStructOpsTypeTraits<FInt32Point> {};
template<> struct TStructOpsTypeTraits<FInt64Point> : public TIntPointStructOpsTypeTraits<FInt64Point> {};
template<> struct TStructOpsTypeTraits<FUint32Point> : public TIntPointStructOpsTypeTraits<FUint32Point> {};
template<> struct TStructOpsTypeTraits<FUint64Point> : public TIntPointStructOpsTypeTraits<FUint64Point> {};
UE_IMPLEMENT_STRUCT("/Script/CoreUObject", Int32Point);
UE_IMPLEMENT_STRUCT("/Script/CoreUObject", Int64Point);
UE_IMPLEMENT_STRUCT("/Script/CoreUObject", Uint32Point);
UE_IMPLEMENT_STRUCT("/Script/CoreUObject", Uint64Point);
UE_IMPLEMENT_STRUCT("/Script/CoreUObject", IntPoint); // Aliased
UE_IMPLEMENT_STRUCT("/Script/CoreUObject", UintPoint); // Aliased
...
여기서 사용된 템플릿 마법은 구조체의 생성과 소멸에 사용되는 일부 public 함수들을 "컴파일 타임"에 수집하기 위한 시스템을 구현합니다. 이러한 public 함수들은 프로그램 시작 시점에 생성됩니다. 이는 ICPPStructOps 인터페이스와 이를 구현한 TCppStructOps를 통해 이루어집니다. 아래 코드는 이러한 public 함수들을 생성하기 위해 UHT가 삽입한 예시입니다.
void* Z_Construct_UScriptStruct_FMyStruct_Statics::NewStructOps()
{
return (UScriptStruct::ICppStructOps*) new UScriptStruct::TCppStructOps<FMyStruct>();
}
ICppStructOps와 TCppStructOps의 사용은 컴파일 타임에 데이터 구조와 그 위에서 동작하는 함수들을 분리하는 visitor 디자인 패턴을 따릅니다.
UStruct의 경우, ICppStructOps에는 public 함수와 virtual 함수가 정의되어 있어, 구조체의 정확한 타입을 컴파일 타임에 알지 못하더라도 이러한 함수들을 통해 구조체를 조작할 수 있습니다.
new UScriptStruct::TCppStructOps<FMyStruct>
템플릿은 DeferredStructOps에 추가할 public 함수들을 설정합니다. UScriptStruct의 경우, 이는 프리-등록(pre-registration) 단계와 inner singleton 생성 과정에서 모듈 UPackage를 통해 이루어집니다. 예를 들어, 기본 생성자, 속성 크기 설정, 메모리 및 정렬(alignment), 플래그 등 다양한 함수가 이에 포함됩니다.
이 필드들은 구조체가 직렬화 함수, 대입 연산자, zero constructor(0으로 초기화 가능한 생성자) 등을 갖추고 있는지와 같은 다양한 C++ 특성을 저장합니다.
Dazhao의 설명에 따르면,
"일부 C++ 구조체 정보는 템플릿만으로는 감지할 수 없기 때문에, 수동으로 표시해주어야 합니다."
실제로 이러한 작업은 바로 이 부분에서 이루어집니다.
/** 스크립트 구조체의 커스텀 특성을 정의하는 타입 트레이트 **/
template <class CPPSTRUCT>
struct TStructOpsTypeTraitsBase2
{
enum
{
WithZeroConstructor = false, // 메모리를 0으로 채우면 유효한 객체가 되는 구조체인지 여부
WithNoInitConstructor = false, // EForceInit 파라미터를 받는 생성자가 존재하여 강제 초기화가 가능한지 여부
WithNoDestructor = false, // 구조체가 파괴될 때 소멸자가 호출되지 않는지 여부
WithCopy = !TIsPODType<CPPSTRUCT>::Value, // 복사 대입 연산자를 통한 복사가 가능한지 여부
WithIdenticalViaEquality = false, // operator==로 비교가 가능한지 여부 (WithIdentical과 상호 배타적)
WithIdentical = false, // Identical(const T* Other, uint32 PortFlags) 함수로 비교가 가능한지 여부 (WithIdenticalViaEquality와 상호 배타적)
WithExportTextItem = false, // ExportTextItem 함수로 문자열 직렬화가 가능한지 여부
WithImportTextItem = false, // ImportTextItem 함수로 문자열 역직렬화가 가능한지 여부
WithAddStructReferencedObjects = false, // AddStructReferencedObjects 함수로 GC 참조 추가가 가능한지 여부
WithSerializer = false, // Serialize 함수로 FArchive 직렬화가 가능한지 여부
WithStructuredSerializer = false, // FStructuredArchive 기반 Serialize 함수가 존재하는지 여부
WithPostSerialize = false, // PostSerialize 함수가 존재하는지 여부
WithNetSerializer = false, // NetSerialize 함수로 네트워크 직렬화가 가능한지 여부
WithNetDeltaSerializer = false, // NetDeltaSerialize 함수로 상태 변화만 직렬화가 가능한지 여부
WithSerializeFromMismatchedTag = false, // SerializeFromMismatchedTag 함수로 다른 프로퍼티 태그에서 변환 가능한지 여부
WithStructuredSerializeFromMismatchedTag = false, // FStructuredArchive 기반 SerializeFromMismatchedTag 함수가 존재하는지 여부
WithPostScriptConstruct = false, // 블루프린트에서 생성 후 PostScriptConstruct 함수가 호출되는지 여부
WithNetSharedSerialization = false, // 패키지 맵 없이 NetSerialize가 가능한지 여부
WithGetPreloadDependencies = false, // GetPreloadDependencies 함수로 로드 시점에 Preload될 객체 목록을 반환하는지 여부
WithPureVirtual = false, // PURE_VIRTUAL 함수가 존재하여 CHECK_PUREVIRTUALS가 true일 때 생성 불가 여부
WithFindInnerPropertyInstance = false, // FindInnerPropertyInstance 함수로 FProperty와 데이터 포인터를 제공하는지 여부
WithCanEditChange = false, // 에디터에서 CanEditChange 함수로 자식 프로퍼티의 읽기 전용 여부를 제어하는지 여부 (UObject::CanEditChange와 유사)
};
static constexpr EPropertyObjectReferenceType WithSerializerObjectReferences = EPropertyObjectReferenceType::Conservative; // Serialize 함수가 직렬화할 수 있는 오브젝트 참조 타입(기본 Conservative는 알 수 없음)
};
이러한 시스템의 활용을 이해하면 Net Core, 네트워크 직렬화, Mass 등 다양한 기능에서 이 시스템이 어떻게 사용되는지 파악할 수 있습니다.
예를 들어, Online Framework에서 빠른 TArray 복제를 활성화하는 간단한 예시는 다음과 같습니다.
/** Specified to allow fast TArray replication */
template<>
struct TStructOpsTypeTraits<FLobbyPlayerStateInfoArray> : public TStructOpsTypeTraitsBase2<FLobbyPlayerStateInfoArray>
{
enum
{
WithNetDeltaSerializer = true,
};
};
"WithNetDeltaSerializer" 열거형 값을 설정하면 FLobbyPlayerStateInfoArray 구조체가 생성되는 방식이 달라집니다.
/** Implement support for fast TArray replication */
bool NetDeltaSerialize(FNetDeltaSerializeInfo & DeltaParms)
{
return FFastArraySerializer::FastArrayDeltaSerialize<FLobbyPlayerStateActorInfo, FLobbyPlayerStateInfoArray>(Players, DeltaParms, *this);
}

UStruct의 수집 과정은 먼저 각 원자 타입(atomic type)의 생성부터 시작합니다. 각 멤버 속성에 대한 정보가 PropPointers 배열에 수집되어 구조체 데이터와 병합됩니다. ConstructUScriptStruct 함수는 Inner Singleton을 생성하며, 구조체 파라미터는 FPropertyParamsBase 메모리 레이아웃을 기반으로 하여 구조체 간 캐스팅 다형성을 지원합니다. 초기화 리스트(={})를 사용해 파라미터를 간단하게 설정할 수 있습니다. FPropertyParams가 POD(Plain Old Data)이기 때문에, 메모리 레이아웃이 일관되고 보장된다면 타입 포인터를 통해 다른 구조체로 안전하게 변환할 수 있습니다. 또한, TCppStructOps를 활용해 네이티브 구조체의 기능을 컴파일 타임에 설정합니다. 필요한 데이터가 모두 수집된 후에는 Deferred Registration 단계로 전달됩니다.
UEnum의 정적 수집 및 생성 과정은 UStruct와 동일하며, UEnum은 UClass나 UFunction과 같은 객체를 위한 UStruct의 기반 기능이 필요하지 않습니다.

UPackage의 컬렉션 단계는 가장 단순합니다. OuterSingleton이 아직 생성되지 않은 경우, UHT가 삽입한 PackageParams가 "/Script/ReflectionTest"에 위치한 모듈의 참조를 설정합니다. 모듈 규칙은 Registration Phase에서 확장되지만, UPackage는 해당 모듈에 정의된 모든 타입의 Outer Owner Reference이기 때문에 반드시 가장 먼저 생성되어야 합니다. 함수 포인터 Z_Construct_UPackage__Script_ReflectionTest()는 Inner와 Outer Singleton을 보관하는 FPackageRegistrationInfo와 함께 수집됩니다. 이 모든 정보는 CompiledInDeferFile을 통해 Deferred Registration 단계로 전달됩니다.
UPackage가 FPackageDeferredRegistry로 전달되면, UClass와 UInterface의 dependent singleton들은 호출할 함수 포인터를 가지게 되며, 네이티브 타입인 UEnum과 UStruct는 이를 통해 UPackage의 OuterSingleton 참조를 얻습니다.
정리하자면, UHT는 정적 초기화 단계에서 타입 정보를 수집하기 위해 정적 타입을 삽입합니다. UECodeGen_Private 네임스페이스는 이러한 로직을 캡슐화하며, 등록에 사용되는 데이터 구조와 생성자를 포함합니다. 모듈이 처리되는 시점에는 수집된 모든 타입 정보가 템플릿 기반의 Deferred Registry로 전달되어 사전 등록(pre-registration) 단계에서 활용됩니다.