TObjectPtr<UObject> and TScriptInterface<> embedded in USTRUCT property do not initialize instanced references correctly.

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]

Steps to Reproduce
1) Open the attached sample project.

2) Press Play In Editor

3) Note the ensures/asserts which are triggered.

NOTE: In the default configuration, you will also trigger one of these ensures on startup.

Tweak the Transient flags precense to exhibit slightly different failure behaviour (see description)

Hi there,

Thanks for providing a great repo project!

I was able to repo your issues immediately with that project.

Upon investigation, it appears to use the container’s subtype to determine if the object should be instanced or not, and this would be determined at compile time.

UCLASS(DefaultToInstanced, BlueprintType, abstract, meta=(ShortTooltip="An ActorComponent is a reusable component that can be added to any actor."), config=Engine, MinimalAPI) class UActorComponent : public UObject, public IInterface_AssetUserData, public IAsyncPhysicsStateProcessorUActorComponent has the DefualtToInstanced class specifier added.

To resolve the issue, you could either;

While you could add this class specifier to a subclass of UObject that you intend to store in that TObjectPtr, this could have other consequences depending on how you are using that class.

But I think the better way, depending on your use case, would be to add the Instanced property macro to the owner variable in your FComponentBContainer

UPROPERTY(Transient, Instanced) // Transient flags give slightly different (but still broken) behavior TObjectPtr<UObject> Owner;After making that change, I was no longer able to reproduce the issue in your sample project.

While this might not be intuitive at first, components are usually always duplicated on construction, but not all UObject’s will be, and I think making that change engine-wide could have a far greater fallout.

Let me know if this solves your problems or if there is any further help I can provide.

Kind Regards

Keegan Gibson

Hi there,

I did see this once on the test project.

But I moved the container initialization to post-init properties instead of in the constructor.

This seemed to resolve this issue for me, but as I didn’t see it again, and I also can no longer repo it in the test project either.

PostInitProperties()

Called after the C++ constructor and after the properties have been initialized, including those loaded from config. This is called before any serialization or other setup has happened.

Let me know if that solves your issue.

Kind Regards

Keegan Gibson

Additional info since the first post is too long:

The common use-case for this is an FFastArraySerializer which stores an internal reference to it’s owning component, so that it can invoke functions on it as replication data changes. In this case, I have a USTRUCT() that is designed to be used in any parent object which implements a specific interface, so the pointer it stores is just a TObjectPtr<UObject> - and was surprised to find the USTRUCT() calling functions on the CDO instance unexpectedly, and also mutated it’s state.

There are workaround for this, but since it’s clearly very obscure behaviour and hard to debug/track what exactly is occuring, it seems like a bug and/or oversight.

Also, since this is an Actor Component - PostReinitProperties() is called before the Actors’ Constructor completes, and it’s the Actors’ constructor which invokes the subobject behaviour. Resetting within PostReinitProperties() does not solve the issue, the earliest point that survives initialization to initialize the reference is OnRegister().

Finally, removing UPROPERTY() also does not solve the issue, the object ptrs are again stomped over with the CDO’s state, and since they are not visible to reflection, the pointers are not restored.

Thanks for the fast response!

Yeah marking the property as `Instanced` does seem to solve the issue at least for TObjectPtr<> UPROPERTYs. The struct then passed ContainsInstancedObjectProperty() and the property is initialized again.

The only thing I would probably flag, is that if the TObjectPtr is not exposed as UPROPERTY, or if you are using TScriptInterface, you can’t mark it as Instanced. Not sure whether that’s truly worth worrying about. I agree this would require a pretty drastic change to “fix”, but it’s such an eroneous path that maybe it warrants some specialist documentation. If you had just a regular “raw” pointer of any kind essentially, it would be referring to the CDO (which you would expect I suppose if you had deeper knowledge of how UE constructs/initializes UObjects)

Do you have any insight as to why this might be triggering the Aggressive Reference Replacement path? I can’t reproduce this in the provided test project, but it’s a reliable repro in my working project.

Cheers!