I’ve made an unexpected behavioural discovery where Instanced UObjects (those set to DefaultToInstanced in their UCLASS macro and/or marked as Instanced in a UPROPERTY) are instantiated and run as a copy within their CDO Outers. I know CDOs can become rather bloated memory sinks without careful consideration for how the objects and references are initialised, but this adds a whole new performance and architectural consideration.
It’s a given that an Instanced UObject will be created once as its own CDO: useful, expected and easily managed behaviour. But an Instanced UObject is also instantiated for each CDO referencing it as a property! If that instantiated UObject has any initialisation, tick, dependency, or other default behaviours, they’ll execute in a potentially unforeseen and unmanaged way, operating with the expectation of being in the game proper.
This could have catastrophic architectural and performance issues. Imagine if the unintentionally instantiated Instanced Objects had Instanced Objects of their own, acted as Observers, Factories, Mediators, or any other critical piece of your technical design. This is a situation I’m currently in. I’m really struggling to find a solution here that doesn’t include getting the Outermost Object and testing its flags before executing most (if not all) default behaviours.
I’m confused about what you’re confused about. What you describe is the whole point of instanced objects. At least if you were creating them as default sub objects from a constructor.
They are instanced, so the copy in Object A is a distinct entity from the copy in Object B and both are distinct from the CDO. Otherwise you wouldn’t be able to configure the instances independently of each other. And the CDO, of say a blueprint, would need an instance so that it has the unique object that is duplicated by instances of that blueprint.
I’ve used instanced objects a lot, I think they’re a powerful tool for building certain things through composition when components are available. I’ve never run into catastrophic issues, but I also rarely have them doing anything on their own. They’re always accessed by or triggered by the owning outer. If I had one ticking, it wouldn’t “just tick”, it would have a tick function that the outer would need to call during its tick. It would have an init that would need to be called during the outer’s initialization (whenever that was appropriate). Maybe this is overly manual, but the behavior is always understood and managed that way.
Consider if what you want is really an instanced object or if you want a TSubclassOf through which you access the CDO instead. I understand Epic GAS feature is oriented around this pretty heavily and maybe it’s an alternative that makes sense for your cases as well.
I’m saying an engineer could get into hard-to-analyse or debug situations like mine, where extending an Instanced Object with FTickableGameObject will also register the CDO’s Instance as tickable. Since FTickableGameObject (among other utility interfaces) is a common pattern for extending behaviours, an engineer is inclined to use these methods without understanding that accounting for an Instanced Object’s own CDO flag is not enough and additional checks must be made for Outer or Outermost flags.
Note, this is just one example. Using an Instanced Object’s constructor or overriding many of its virtual methods, such as PostInitProperties, could also catch a developer off guard, as they expect and do account for their own CDO flags, but not those of a newly instantiated Instance Object’s Outer/Outermost CDO. God forbid a engineer use nested Instanced Objects and wonder why their memory footprint is so high. Yes, these are unlikely scenarios, but I’m more concerned with developers, like me, initially not taking this fact into account.
Granted, what you say is correct, and this is a needed feature for the engine to operate. Being aware of this behaviour and ensuring Instanced Objects only execute code through their owning outer is the most practical solution. But in a polymorphic environment, larger or inherited projects/teams, going between native and blueprints, using many of the patterns listed above, or when using plugins (3rd party or otherwise) this might be overlooked or impractical.
Fortunately, I identified what was going on early enough to place lightweight and practical guards around established Instanced Objects before this got completely out of hand. Still, I’d be concerned with products going to market, using Instanced Objects and not accounting for it. The performance costs might not be insignificant.