Does a UObject captured in lambda by value get retained?

While debugging a crash in my code I came to realization that UObjects captured by lambdas don’t get their reference counters incremented, do they?
It may happen then, that when the lambda is being executed the object will be already garbage collected.
In my code I keep a UPROPERTY() TArray of my UObjects which I add on the Game thread and dispatch processing in lambda on a background thread. However, there’s also another game-thread code path that can remove some objects from the TArray (this will mark them ready to be GC’ed I presume, right?). In these situations, when lambda that has captured such GC’ed object will crash when accessing it. Is my understanding correct? Is the fix just to use IsValid(object) call inside lambda before accessing its’ memebers? Is there a better workaround?

When I wrote this code, I made assumptions that UObjects are just std::shared_ptr on steroids, so in such scenario as I described above, the std::shared_ptr object will never be deallocated because lambda is still holding reference to the object. Apparently, Unreal is not respecting that. Am I missing anything?

The solution is to use a TWeakObjectPtr for your lambda. This is often the case when the object keeping a reference to it does not control the lifetime of the Object.

Unlike Shared Pointers, even UObjects that are “strongly-referenced” can be garbage collected in some cases (e.g, when something explicitly marks them as garbage, they will be GC’d no matter what references them). Unreal will automatically null UPROPERTY() references when this happens (but does not deal with special types like FGCObject or TStrongObjectPtr etc.)

Note: This is allegedly changing in the near future, as part of an extremely unwelcome change to the GC system which I pray remains optional.

IsValid() will not be safe in this case either, because if the UObject is in fact GC’d, the captured pointer can be anything.

1 Like

Thanks for your reply!

Do you mean to wrap UObject and pass it to lambda, like this:

  TWeakObjectPtr<UMyObject> objPtr(myObject);
  processingThread.dispatch([objPtr](){
    if (objPtr->IsValid())
    {
      // do the processing
    }
  });

The problem is even if I do this check, the object might get GC’ed/deallocated while I’m inside of processing block.
If I was to avoid if check inside lambda and ensure validity of myObject during lambda execution, will using TStrongObjectPtr help?

In the meantime yesterday, I implemented a workardound using std::shared_ptr like this (let me know what you think of this):

// .h
struct FMyObjectStrongRefWrapper;

UCLASS(BlueprintType)
class MY_API UMyObject : public UObject
{
    GENERATED_BODY()
public:
    std::shared_ptr<FMyObjectStrongRefWrapper> getStrongRefObject();
}

UCLASS()
class UMyObjectStrongRef : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY()
    UMyObject * object_;
};

// .cpp
struct FMyObjectStrongRefWrapper
{
    FMyObjectStrongRefWrapper(UMyObject * obj)
    {
        check(IsValid(obj));

        objRef_= NewObject<UMyObjectStrongRef>();
        objRef_->object_= obj;
        objRef_->AddToRoot();
    }

    ~FMyObjectStrongRefWrapper()
    {
        check(IsValid(objRef_));
        objRef_->RemoveFromRoot();
    }

    UMyObjectStrongRef * objRef_;
};

std::shared_ptr<FMyObjectStrongRefWrapper> UMyObject ::getStrongRefObject()
{
    return make_shared<FMyObjectStrongRefWrapper>(this);
}

with this, I can create a FMyObjectStrongRefWrapper variable that I capture in lambda by-value like this (I might overload the -> operator for it too to avoid passing myObject):

  auto objRef = myObject->getStrongRefObject();
  processingThread.dispatch([objRef, myObject](){
      // do the processing on myObject...
  });

And alternative (and simpler) approach would be to manually add your object directly to the root set:

myObject->AddToRoot( );
  processingThread.dispatch([myObject](){
      // do the processing
      myObject->RemoveFromRoot( );
  });

This should work fine for UObjects. It won’t work well for Actors, but no amount of strong-refs will keep an actor alive that was requested to be destroyed. There’s also the caveats that TheJamsh pointed out with object GC-ing.

2 Likes

Thanks!
Yes this would work too. Though I’m trying to keep lambda/processing thread as ignorant of the object’s memory management as possible.

There’s no point making the lambda “ignorant” of anything. It’s already part of whatever scope you’re declaring it in. That’s like making a switch statement ignorant of details of the function it’s in. It serves no purpose.

At least not until you’re refactoring it to be used from multiple places. But that’s an optimization problem and likely premature until you’ve proven you need it someplace else. And you could easily reuse the function containing the lambda instead of the lambda itself.

This has the disadvantage of introducing a crash if the uobject is not yet removed from root during EndPlay (such as switching levels, exiting, etc).

1 Like