How to iterate through UPROPERTY in nested USTRUCT or UCLASS.

How to iterate through UPROPERTY in nested USTRUCT or UCLASS.

Created
Apr 6, 2024 03:03 AM
Tags
C++

Introduction

In Unreal Engine development, it's a common practice to use USTRUCT as properties within classes. However, there are instances where we may need to access the UPROPERTY of these structs iteratively without prior knowledge of their types. From example when we are dealing with structure asset in Unreal Editor. This approach not only makes our code more flexible but also streamlines development by reducing the need for explicit type declarations. A fundamental understanding of UCLASS and USTRUCT is assumed.

USTRUCT Iteration

Simple Property

Let's start by considering two USTRUCT types: FTest and FTestContainer, where FTest is nested within FTestContainer.
USTRUCT() struct FTest { GENERATED_BODY() UPROPERTY() FString StringProperty = ""; UPROPERTY() FVector VectorProperty = FVector(0); }; USTRUCT() struct FTestContainer { GENERATED_BODY() UPROPERTY() FTest Test; };
To access the properties of these structs, we define a FTestContainer property in a component or actor class.
UPROPERTY() FTestContainer Container;
Now, let's iterate through all UPROPERTY instances in FTestContainer.
for (TFieldIterator<FProperty> It(Container.StaticStruct()); It; ++It) ...
This line will create a TFieldIterator based on the static struct type of our container, which is FTestContainer. Each iteration provides us with a UPROPERTY in this struct. To access the actual data, we need to obtain the address of the Test property from the Container object. The function ContainerPtrToValuePtr takes a pointer to the container of property value, with the second parameter often omitted.
FProperty* Property = *It; if(Property != nullptr) { if(const FStructProperty* StructProperty = CastField<FStructProperty>(*It)) { const void* TargetPtr = Property->ContainerPtrToValuePtr<void>(&Container); UScriptStruct* NestedStruct = StructProperty->Struct; ...
Now we have the address of variable Test, we then do the same operation to find StringProperty and VectorProperty. The UScriptStruct contains reflection data for a standalone structure declared in a header or as a user defined struct according to the official comment.
for(TFieldIterator<FProperty> NestedIt(NestedStruct); NestedIt; ++NestedIt) { FProperty* NestedProperty = *NestedIt; ...
With the address of Test, we repeat previous process to find StringProperty and VectorProperty within FTest. Notice how we convert the property to specified types.
if(NestedProperty != nullptr) { const void* DataPtr = NestedProperty->ContainerPtrToValuePtr<void>(TargetPtr); if(const FStrProperty* StrProperty = CastField<FStrProperty>(NestedProperty)) { FString* Value = (FString*) DataPtr; FString StrValue = *Value; UE_LOG(LogTemp, Warning, TEXT("%s"), *StrValue); } ...

Array Property

Assume we are dealing with an array property like TestArray as following.
UPROPERTY() TArray<FTest> TestArray;
When we get the value pointer points to the property, we need to create a FScriptArrayHelper in order to work with array properties in a sensible way.
const void* TargetPtr = RowProperty->ContainerPtrToValuePtr<void>(Iter.Value()); if(const FArrayProperty* RowArrayPorperty = CastField<FArrayProperty>(&Container)) { FScriptArrayHelper ArrayHelper(RowArrayPorperty, TargetPtr); ...
To access the template struct type of this array, in our case is (FTest), we extract the struct property stored in Inner.
FProperty* Inner = RowArrayPorperty->Inner; for(int32 Idx = 0; Idx < ArrayHelper.Num(); ++Idx) { if( FStructProperty* StructProperty = CastField<FStructProperty>(Inner)) { UScriptStruct* UStructTemp = StructProperty->Struct; ...
Now we know how FTest looks, we need to get the real pointers based on the actual items in the array. We use ArrayHelper.GetRawPtr(Idx) to achieve this.
if(NestedProperty != nullptr) { const void* DataPtr = NestedProperty->ContainerPtrToValuePtr<void>(ArrayHelper.GetRawPtr(Idx)); FString Name = NestedProperty->GetName(); if(const FStrProperty* StrProperty = CastField<FStrProperty>(NestedProperty)) ...
Then, we process these properties as we did in the previous section.
This approach is incredibly useful, especially when combined with UDataTable, because the row of a data table is an UScriptStruct. So external files like CSVs for dynamic data management within Unreal Engine is no longer needed.

Additional Insight: How an USTRUCT/UCLASS is stored.

Although for a real UCLASS/USTRUCT object it stores in a more complex structure, for iterating properties, we can simplify it as a linked list.
// // An UnrealScript variable. // class COREUOBJECT_API FProperty : public FField { DECLARE_FIELD(FProperty, FField, CASTCLASS_FProperty) // Persistent variables. int32 ArrayDim; int32 ElementSize; EPropertyFlags PropertyFlags; uint16 RepIndex; private: TEnumAsByte<ELifetimeCondition> BlueprintReplicationCondition; // In memory variables (generated during Link()). int32 Offset_Internal; public: FName RepNotifyFunc; /** In memory only: Linked list of properties from most-derived to base **/ FProperty* PropertyLinkNext; /** In memory only: Linked list of object reference properties from most-derived to base **/ FProperty* NextRef; /** In memory only: Linked list of properties requiring destruction. Note this does not include things that will be destroyed by the native destructor **/ FProperty* DestructorLinkNext; /** In memory only: Linked list of properties requiring post constructor initialization.**/ FProperty* PostConstructLinkNext; ...
When we get the StaticStruct() of the USTRUCT, it is essentially an UScriptStruct. It stores the reflection data of a user defined struct.
/** * Reflection data for a standalone structure declared in a header or as a user defined struct */ class UScriptStruct : public UStruct { public: /** Interface to template to manage dynamic access to C++ struct construction and destruction **/ struct COREUOBJECT_API ICppStructOps { /** Filled by implementation classes to report their capabilities */ struct FCapabilities { EPropertyFlags ComputedPropertyFlags; bool HasNoopConstructor : 1; bool HasZeroConstructor : 1; bool HasDestructor : 1; bool HasSerializer : 1; bool HasStructuredSerializer : 1; bool HasPostSerialize : 1; bool HasNetSerializer : 1; bool HasNetSharedSerialization : 1; bool HasNetDeltaSerializer : 1; bool HasPostScriptConstruct : 1; bool IsPlainOldData : 1; bool IsUECoreType : 1; bool IsUECoreVariant : 1; bool HasCopy : 1; bool HasIdentical : 1; bool HasExportTextItem : 1; bool HasImportTextItem : 1; bool HasAddStructReferencedObjects : 1; bool HasSerializeFromMismatchedTag : 1; bool HasStructuredSerializeFromMismatchedTag : 1; bool HasGetTypeHash : 1; bool IsAbstract : 1; #if WITH_EDITOR bool HasCanEditChange : 1; #endif }; ...
As we dig deeper we will find UCLASS is derived from USTRUCT.
/** * An object class. */ class COREUOBJECT_API UClass : public UStruct ...
So let’s summarize the whole process. When we define a USTRUCT, the compiler generates the reflection metadata during compilation. It becomes a template object, an UScriptStruct, which stores the reflection data of the user-defined struct. This UScriptStruct has a linked list-like structure containing detailed information about the struct's properties. When we search for or simply iterate through properties, we need a pointer to the real object in memory, which acts like a “head pointer”. Then we look at the property linked list to offset this pointer based on the property type. Therefore the function we used for indexing is called ContainerPtrToValuePtr(). The following diagram is for you to have a better understanding.
notion image