Some confusion of UObject*, UPROPERTY decorator, and UWeakObjectPtr

There is a lot of documentation on UObject ptrs, and those decorated with UPROPERTY, and then TWeakObjectPtrs in general, but I haven’t seen any with a direct answer to this - or obvious to me at least.

This is referenced counted for GC, right?

class A : public UObject {
    UPROPERTY()
    UObject* A;
};

This is not…

class B : public UObject
{
    UObject* b;
};

…and this not reference counted…

class C : public UObject
{
    TWeakObjectPtr<UObject> c;
};

And I assume that if, from the above, I assign A::a, to B::b or to C::c, neither of the two add a ref count.

And if A::a is GC’d, IsValid(B::b) and C::c.IsValid() are false…?

So what is the difference between a non-reference counted UObject (B::b) and a weak object ptr (C::c), they seem like they do the same thing: weak references.

Thanks!

Weak Object Pointer are simply pointers that the Engine is allowed to delete whenever it wants if memory is getting low. You then have to “lock down the reference” before using it. In Unreal you Pin the weak object and check if you successfully got it pinned. If not you have to recreate the object again and then Pin it. All explained here: Weak Pointers | Unreal Engine Documentation.

It’s like rkeene said, but also a weak ptr’s IsValid only returns true if the original address is still valid.

For example, if you check “if (MyPtr)” with a raw ptr, you’re not checking if the original data is valid, you’re checking to see if the ptr points to real data. Basically, this means the ptr is not nullptr - it doesn’t mean it’s the data you’re actually looking for.

TWeakPtr and TWeakObjectPtr are only valid if the original address is unless it’s been explicitly reassigned, so you can be certain that if IsValid is true on these objects, you’re pointing to the original data wrapped in this ptr.

TWeakObjectPtr has no pin functionality, AFAIK. That is it acts such that…
a UObject’s lifetime (regardless it being in a weak ptr) is guaranteed throughout the life of a function. So as long as it is valid at the start of the function, it should be valid the rest of the function - I am assuming because the game doesn’t tick during the function (so it doesn’t hit GC).

Once a UObject* is GC’d, it is automatically set to null; this is basically what a weak ptr does, right? So it just seems like a UObject* is either a shared_ptr (UPROPERTY, etc.), or a weak ptr (just UObject*)

This is the crux of the question, what is the difference between a non-UPROPERTY UObject* and a WeakObjectPtr.

I could be wrong about all of this.

IsValid(B::b) is undefined behavior, I think. I haven’t tested and in practice it may often (nearly always?) return false, but that’s a dangling pointer.

So we know that A::a is GC’d, because it is a UPROJECT, and it will be null “automatically” after garbage collection. So we can test it for null directly (it’s basically a shared_ptr, and not a C++ style ptr). I guess I should add ‘right?’

But I guess my real question is, does B::b, since it points to a UPROPERTY, get the UPROPERTY characteristics, and contribute to the lifetime of the same UObject that A::a points to (thus a shared_ptr), and can be tested null in this case (non dangling)…

Does it NOT contribute to the lifetime of A::a’s UObject, but gets set to null automatically (acts like a weak_ptr)…

Or, is it a dangling pointer, like any good pointer should be :wink:

My guess, at this point and with this feedback is that A::a acts like a shared_ptr, but B::b acts like a C++ style ptr. But I see so little weak_ptrs in examples, and in our code, with no error conditions. This experience could just be situational to the code I have seen, but still wrong, of course. And then this would explain why WeakObjectPtr even exists.

Outside of Slate code you will not need WeakPtrs too much.

On newer versions of the engine you will be expected to use TObjectPtr instead of C pointers, even for UPROPERTY.

1 Like

A::a isn’t exactly like a shared pointer. If all you have is two UObjects pointing at one another (UPROPERTY pointers), they’ll both get garbage collected because nothing else is referring to them. With shared_ptrs they’d both stay in memory, because they’d both have a reference count of 1.

But yeah, B:b leaves a dangling pointer. It has no effect on lifetime and won’t be nulled when the object is destroyed; it’s just a normal C++ pointer to the UObject’s location in memory, the engine doesn’t know about it.

1 Like

Great information, thanks all! This was a very specific case, and I appreciate the help with it.

Once a UObject* is GC’d, it is automatically set to null

This is not true for a UObject * bare pointer, that doesn’t have the UPROPERTY decorator.

If you make it UPROPERTY then the UObject * will keep the object alive.
To have a pointer to an object, which does not by itself keep the object alive, and gets set to null when the object dies, you need a TWeakObjectPtr<>.