I’m trying to wrap my head around UObject lifetime management - particularly I’m trying to understand if it’s okay to put UObjects inside TVariants?
As far as I understand, UObjects must be created on the heap and only through NewObject, not directly with new - but what would happen if I create UObject on the stack or as a part of another UObject, without indirection?
TVariant uses placement new under the hood, so on the first glance that also should be illegal..I think. However what if TVariant object is a UPROPERTY inside another UObject?
(I’m trying to understand that because I’m seeing how raw pointer marked as UPROPERTY but put inside TVariant starts to point to unallocated memory and that shouldn’t be possible)
Your instinct is right about not putting UObject directly inside a variant, i.e. “by value”. UObjects must be allocated on the heap using the appropriate Unreal-approved object creation functions.
So that means you need to put a UObject* (i.e. a pointer) in there instead… but what about lifetime stuff? Well, TVariant can’t be a UPROPERTY, so you are going to need to step in and do something yourself manually. You have a few options:
Use TStrongObjectPtr<UYourObject> or TWeakObjectPtr<UYourObject> inside the variant, instead of a raw pointer. These pointers participate in the garbage collector’s view of the world and so they’ll behave safely across GC runs, unlike a raw pointer. Just be aware of TStrongObjectPtr reference cycles.
Manually implement AddReferencedObjects in the containing class(es), possibly inheriting from FGCObject if necessary. In your implementation(s), check the variant for whether it’s holding the UYourObject*, and add the pointer as a referenced object if so.
This is a bit sketchy if your variant is changing types often; after calling this function, Unreal holds on to a pointer-to-your-pointer, so if you destroy your pointer by switching types in the variant, you may get into hot water.
Note that if you go this route, Unreal may pressure you into using TObjectPtr inside the variant instead of a raw pointer.
Keep a “side-UPROPERTY” that mirrors whatever object is held inside the variant, if any. Whenever you place a UYourObject* into the variant, also place it into this side property, and any time you want to access the pointer inside the variant, check this side property for null first. I don’t really recommend this approach, but just mentioning it for completeness.
Personally I’d go with option 1, since it encodes the safety in the variant itself rather than forcing you to duplicate it (and remember to duplicate it) everywhere you’re holding one of these variants. It also composes nicely if you start sticking the variants inside other data structures like arrays, maps, etc… having the safety built into the type means it always “just works”.
If you need cyclic references or are only holding this variant in one spot and want to save a few CPU cycles (the strong and weak ptrs have some VERY miniscule overhead), I recommend option 2.