I know this problem has already been discussed on other threads, but I still find this aspect a bit confusing.
As I have understood, there is no easy way (like using UPROPERTY() for TArrays, TMap etc.) to prevent the GC from deleting UObjects whose pointers are only available from a TQueue.
However, as I really need to use a queue in my project (also, queues are a really useful and common structure to use, so there is no reason not to use them), the only two solutions I could find to the problem are:
Create a custom Queue class: this approach would lead to unnecessary work, especially because TQueues are also thread safe, which is a really nice feature :D.
Create a UCLASS() which contains the TQueue + an additional TArray preceded by UPROPERTY() to make the GC to not delete those elements, therefore dealing with the updating of both structures when enqueuing/dequeuing objects.
The latter option would be much simpler than the former, but it also leads to memory waste as all elements pointers are doubled.
The question is if there exists a cleaner and more straightforward way to deal with the problem, maybe using UE4 built-in functionalities I’m not aware of.
You could make 2 wrapper methods for Enqueue / Dequeue that call AddToRoot / RemoveFromRoot respectively to prevent GC while the elements are in the queue. You’ll have to make sure that you clean up correctly at the end by dequeuing everything though.
I ended up implementing my version of a queue with TArray, making it thread-safe through the use of ue4 FScopeLock. This custom class inherits from FGCObject and overrides AddReferencedObjects(), marking all elements of the TArray to prevent GC.
Your solution however looks much simpler and cleaner, I will give it a try.
In both cases it is necessary to clean up and properly destroy objects once they are dequeued, or when the whole queue is destroyed.
I’m in the same situation. What’s more confusing is that this states that “A pointer to the object must not be stored in a container” in order to be GC’d.
To second GrumbleBunny’s solution, AddToRoot() / RemoveFromRoot() work well for background threads, so long as you ensure everything is removed from rootset at some point, as MarkPendingKill() (called on all objects on end play) has a check(!IsRooted()).
I do the latter via a custom ASyncTask on GameThread based off of TGraphTask's template (I used FNiagaraAudioPlayerAsyncTask for reference), so that I can ensure RemoveFromRoot() is called from the ASyncTask’s destructor