Allocating a TArray on the stack

Array Elements in Memory

The TArray type is used in Unreal C++ to store a variable sized array of items.

Logically a TArray has two parts:

  • the TArray variable which contains stuff like the capacity of the array and how many elements are stored in it, and
  • the elements themselves. By default the elements of an array are stored on the heap. This imposes a performance cost because the memory required for this storage must be allocated when the array is created and deallocated when the array is destroyed.

A TArray object can be configured to store its elements on the stack. The allocation of memory on the stack is many times faster than on the heap. To make a TArray store its elements on the stack, use a TInlineAllocator. For example we can declare an array of UGameplayEffect pointers like this:

const TArray< TSubclassOf<UGameplayEffect>, TInlineAllocator<3> > Effects;

This example is from function in the Lyra source code:

void UAuraAbilitySystemFunctionLibrary::InitializeDefaultAttributes(
	const UObject* WorldContextObject, 
	ECharacterClass CharacterClass, 
	float Level, 
	UAbilitySystemComponent* AbilitySystemComponent )
{
	if( !AbilitySystemComponent ) return;
	
	if( const AAuraGameModeBase* Base = 
		Cast<AAuraGameModeBase>( UGameplayStatics::GetGameMode(WorldContextObject) ) )
	{
		check(Base->CharacterClassInfo)

		const FCharacterClassDefaultInfo& Info = 
			Base->CharacterClassInfo->GetClassDefaultInfo( CharacterClass );

		const TArray< TSubclassOf<UGameplayEffect>, TInlineAllocator<3> > Effects {
			Info.PrimaryAttributesGameplayEffect,
			Base->CharacterClassInfo->SecondaryAttributesGameplayEffect,
			Base->CharacterClassInfo->VitalAttributesGameplayEffect };

		if( const AActor* Source = AbilitySystemComponent->GetAvatarActor() )
		{
			for( const TSubclassOf<UGameplayEffect>& Effect : Effects  )
			{
				if( Effect )
				{
					...
				}
			}
		}
	}
}

The full array declaration from the above function is:

const TArray< TSubclassOf<UGameplayEffect>, TInlineAllocator<3> > Effects {
		Info.PrimaryAttributesGameplayEffect,
		Base->CharacterClassInfo->SecondaryAttributesGameplayEffect,
		Base->CharacterClassInfo->VitalAttributesGameplayEffect };

The number passed to the TInlineAllocator type is the initial capacity of the array, that is the number of elements for which memory has been allocated - this is not the number of elements in the array, it's just the number of elements for which space has been allocated. A declaration like this:

 TArray< TSubclassOf<UGameplayEffect>, TInlineAllocator<3> > Effects {
		Info.PrimaryAttributesGameplayEffect,
		Base->CharacterClassInfo->VitalAttributesGameplayEffect };

allocates an array with an initial capacity for 3 elements, which contains only 2 elements. So 1 more element can be added without allocating more memory.

Reallocation

Array elements are not guaranteed to be contiguous but are usually assumed to be so. Once more elements are added to the array that then initial capacity passed to the TInlineAllocator, the array is reallocated. This means a new area of heap memory is allocated and all the elements are copied to that area. This new memory is not on the stack, it is on the heap, and this reallocation incurs the cost of allocation and deallocation we were trying to avoid by using the TInlineAllocator. So it is important to avoid reallocation by determining the maximum size of the array when it is created and passing this to the TInlineAllocator.

Passing Different Array Types

A function can be declared to take an array parameter like so:

void SumFunc(TArray<int> View)
{
    UE_LOG(LogTemp, Warning, TEXT("viewsize %d"), View.Num());
}

This will accept a parameter declared as a TArray<int> but will not accept one declared with a non-default allocator such as TArray< int32, TInlineAllocator<3> >, because this is a different type from a simple TArray.

To write the function to a TArray which is declared either on the stack or on the heap, rewrite it to take a TArrayView parameter like so:

void SumFunc(TArrayView<int> View)
{
	UE_LOG(LogTemp, Warning, TEXT("viewsize %d"), View.Num());
}

This function will accept both TArray<int> and TArray< int32, TInlineAllocator<3> > parameters.

There are a couple of complications:

  • the TArrayView cannot be resized. Individual elements can be changed but elements cannot be added or deleted.
  • When passing a TArray< int32, TInlineAllocator<3> > parameter the compiler creates a TArrayView<int> object which wraps the original array. As the TArrayView<int> is a temporary object passed to the function it cannot be passed by reference, a call to a function declared like this will not compile:
void SumFuncByReference(TArrayView<int>& View)
{
	UE_LOG(LogTemp, Warning, TEXT("viewsize %d"), View.Num());
}

This can be remedied by using an rvalue reference like this:

void SumFuncByRValueReference(TArrayView<int>&& View)
{
	UE_LOG(LogTemp, Warning, TEXT("viewsize %d"), View.Num());
}