Array of arrays of interface pointers gets cleaned when it shouldn't

I’ll start by explaining the class layout I have. I’ll change names and skip irrelevant parts of the code to make it easier to understand, so if you see any mistakes that are too obvious or could lead to compilation errors, it’s likely that I simply made a mistake modifying that part.

The class that contains the array of arrays looks like this:

UCLASS()
class MYPROJECT_API UMyObject : public UObject
{
	GENERATED_BODY()
public:
	UMyObject();

	UFUNCTION()
	void Add(TScriptInterface<IMyInterface> element);

protected:
	UPROPERTY()
	TArray<FInternalArray> ElementCollection;
};

Where FInternalArray is a struct that I use as a workaround to the fact that UE doesn’t support TArrays of TArrays:

USTRUCT()
struct FInternalArray
{
	GENERATED_BODY()

public:
	UPROPERTY()
	TArray<TScriptInterface<IMyInterface>> row;
};

This is how I initialize this 2D array:

UMyObject::UMyObject()
{
    TArray<TScriptInterface<IMyInterface>> tmpArray;

    for (uint16 i = 0; i < 7; i++)
    {
        ElementCollection.Add({tmpArray});
    }
}

Now, here comes the issue. When I tried to add an element to the inner array, I noticed that, apparently, the outer array had been cleaned up. This is all that the Add() function does (index is always between 0 and 6):

void UMyObject::Add(TScriptInterface<IMyInterface> element, uint16 index)
{
        UE_LOG(LogTemp, Warning, TEXT("ElementCollection.Num(): %d"), ElementCollection.Num())
        ElementCollection[index].row.Add(element);
}

The values logged can be whatever:

ElementCollection.Num(): 7
ElementCollection.Num(): -1
ElementCollection.Num(): 7051781
...

And, either the line where I add an element to the inner array fails, or it succeeds and, then, I get an undefined reference error a few seconds after.

The class that has an instance of this UMyObject class looks like this:

UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
protected:
	// UPROPERTY()
	UMyObject* Container;
...

Uncommenting the UPROPERTY() line above the declaration of Container makes the line where I print the number of elements in ElementContainer trigger an access violation (so it apparently makes GC kick in even sooner). This is the line:

UE_LOG(LogTemp, Warning, TEXT("ElementCollection.Num(): %d"), ElementCollection.Num())

Container is initialized like this:

AMyCharacter::AMyCharacter():
    Super()
{
    Container = NewObject<UMyObject>();
}

And I add elements to it this way:

Container->Add(NewElement, index);

I tried to initialize ElementCollection in other ways: with Emplace(), with Init(), etc. All of them yielded the same result.

As far as I’m aware, specifying UPROPERTY() should make the reflection system resgister the variable as a member of the class, hence the garbage collector shouldn’t be cleaning the outer array, or any of the arrays it contains.

I know that interfaces have several limitations in UE, but changing the types of the elements of the inner arrays to UObject* didn’t solve the issue either.

However, the fact that calling Num() on the outer array yields pretty much undefined values, makes me think that the GC may be somehow cleaning the content of the array. Or maybe I just can’t spot my mistake (which is likely).

I would appreciate if you could point out anything that I’m doing wrong.

Thanks in advance!

Hey @yetmax. I think the issue is in the way you are initializing your array of arrays. You are currrently declaring an empty array and using that same reference for each entry in your array. AND that empty array (the one you call tmpArray) is only initialized in the local scope of the UMyObject() method.

I think you could use TSharedPtr<> here. A shared pointer is not garbage collected until there is no class referencing the pointer.

// Create a Shared Pointer
TSharedPtr<FMyObjectType> NewPointer = MakeShared<FMyObjectType>(MyArgs);

With this, when you initialize your array, you should create a shared pointer for each of the elements in your array, something like the following:

UMyObject::UMyObject()
{
    for (uint16 i = 0; i < 7; i++)
    {
        TSharedPtr<FMyObjectType> NewsafePointer = MakeShared<FInternalArray>();
        ElementCollection.Add({NewsafePointer});
    }
}

I didn’t test this code, so it might have some issues, but my point here is to show you how you can do this yourself. So I recommend you read a little about the shared pointer library and proceed to try it out in your code. Maybe you will need to use a UObject instead of a struct, I’m not sure here.

Anyway, hope this helps!

1 Like

Adding to ItemCollection, but printing size of ElementCollection.
This is intentional or mistake?

1 Like

Hi @kribbeck, thanks a lot for your reply.

So, as I understand this, the Add() function from TArray should copy the element you pass to it as a parameter, otherwise it would be very inconvenient. That is, it should not be storing a reference to the struct I’m initializing (among other things, because I’m not passing a reference to the instance, but the instance intself), but creating a new struct initialized with the one I specified and storing that copy. I’ll do a quick mockup of this in pseudocode for better understanding:

TArray::Add(TArray& array)
{
    // Deep copy of the input array
    TArray CopiedArray(array)
    MyInternalArray.Push(CopiedArray)
}

From the Unreal Engine documentation: “In the case of our TArray<FString> , Add will create a temporary FString from the string literal and then move the contents of that temporary FString into a new FString inside the container”

Also, your suggested workaround would require changing the declaration of the outer array to store smart pointers to TArrays instead of TArrays themselves, which would make the data structure even more convoluted that it already is. I would have something like this:

TArray<TSharedPtr<TArray<TScriptInterface<IMyInterface>>

Maybe my understanding of the Add() function for TArrays is incorrect and it does store references rather than copies. If that’s the case, shouldn’t there by another function that actually stores a copy? Maybe you can point me out to that one?

Thanks!

Hi @Emaer, thanks for your reply!

Yes, that was a mistake. Both should be ElementCollection.

I went ahead and edited the OP.

Thanks for ponting that out.

1 Like

Is the container not destroyed in the meantime due to the lack of UPROPERTY?

1 Like

Hi @Emaer,

Thanks for pointing that out, I was indeed missing that UPROPERTY(). What’s weird, though, is that adding it apparently makes GC to kick in with the whole ElementCollection (that is, the outer array), so this line:

UE_LOG(LogTemp, Warning, TEXT("ElementCollection.Num(): %d"), ElementCollection.Num())

Now triggers an access violation exception instead of printing the random amount of elements it was printing before.

I’ll update OP to mention this.

This would suggest that without UPROPERTY you had a UB, so anything could happen. The Container was destroyed but the pointer was not set (because GC didn’t know about it) to nullptr. After adding UPROPERTY, the GC sets it to nullptr and therefore throws a crash. Just guessing.
Crash is always better than UB :wink:

One more question: Are ElementArray and ElementCollection the same?

Sorry for that. Yes, ElementArray should have been ElementCollection. OP updated (once again).

Regarding your first paragraph, I don’t totally follow with what you suggest. If the pointer to Container is nulled by GC, then access violation should have been raised when calling Container->Add() instead of inside it. Unless the call to the method happens at the middle of Container’s wipe out, so it is not NULL when calling the method, but it is nulled right after getting inside it. And honestly, to me this still sounds like UB, even if I’m making a mistake somewhere (which I certainly am).

Thanks for your help so far, we certainly are closer to know what I’m messing up here.

Mistery solved. It turns out that initializing Container inside the character’s constructor keeps the GC collection system away from detecting that the Container pointer has a reference. Moving the initialization from constructor to BeginPlay() solves the issue and avoids GC from kicking in when it shouldn’t.

So, the issue was in where I was initializing the property. I guess this is Unreal Engine 101, so appologies for the mistake.

Thanks to both of you for all the help!

1 Like