I’ve been tracking down an issue where USTRUCTS which contain references to their owning UObject, will exhibit different initialization behaviour depending on the specific type of FProperty they declare to store that reference. TObjectPtr<UActorComponent> behaves as expected, but TObjectPtr<UObject> or TScriptInterface<> behave differently, even if they too point to components. The provided test project demonstrates this behaviour.
In addition to the above, the non-component properties also trigger the “Aggressive Reference Replacement” path to be hit when compiling a Blueprint of that class when they are defined/initialized a certain way. Unfortunately I’m unable to reproduce this specific issue in a sample/test project, but in an actual project - changing the type of property from TObjectPtr<UObject> to TObjectPtr<UActorComponent> stops that from triggering.
Fundamentally this seems to be caused by CPF_ContainsInstancedReference being applied to an FStructProperty when it contains a TObjectPtr<UActorComponent>, but NOT if it contains a TObjectPtr<UObject> or TScriptInterface<>, even if those references are actually pointing to either actor component instances or some other subobject instance.
The resulting behaviour also seems to be partially affected by the precense of the Transient flag. If the properties are Transient, then ALL property types (including TObjectPtr<UActorComponent>) are affected - if the Transient flag is not present, then only the Object/Script proeprties are affected. You can add/remove these from the sample project and recompile to see the differences.
Here is the general flow:
1) When spawning the actor/component, within both of their constructors, the property references can be seen as pointing to the correct object (the newly created instance, which is this in the component context).
2) The Actors’ FObjectInitializer() destructor is hit, invoking FObjectInitializer::PostConstructInit() then FObjectInitializer::InstanceSubobjects() for each of the Actors’ subobjects.
3) UStruct::InstanceSubobjectTemplates() reaches the offending component, it’s properties are iterated over, and the FStructProperty that contains the reference to that component is reached. Depending on how the UPROPERTY has been declared, the initialization then diverges:
If the property is TObjectPtr<UObject> or TScriptInterface<>, the containing USTRUCT() does not have the CPF_ContainsInstancedReference flag, and therefore fails FProperty->ContainsInstancedObjectProperty(). Property->InstanceSubobjects is not hit, so the TObjectPtr is left pointing to the CDO’s instance (presumably because the CDO template is stomped over it after invoking the classes constructor).
If however, the property is TObjectPtr<UActorComponent>, OR more confusingly, the USTRUCT() contains *any* embedded TObjectPtr<UActorComponent> property, then FProperty->InstanceSubobjects() is invoked the struct, and all TObjectPtrs (even those which are not component references) will be correctly reinitialized/updated to the subobject pointer they were originally constructed with.
This behaviour seems erroneous and is a very confusing/obfuscated path to debug, since this is all deep in UObject territory. The only way I was able to verify this in the end was to put conditional breakpoints into UStruct::InstanceSubobjectTemplates() and break when a struct property matching my specific property name was hit (shown below). This highlighted the difference between one of my working structs and another non-working one.
[Image Removed]