Efficiently returning structs

I am trying to work out the most efficient way of returning a struct from a datatable in c++. I’m not quite sure about returning by reference (avoiding duplications) or value. In my mind, this should be the most efficient way I can get to compile without errors (and I get the output I intended), but is this correct? (Am I putting the references & and pointers * in the most optimal place to avoid value copies)

my struct for the Datatable is

// ©2022 Marcus Rivers

#pragma once

// INCLUDE FOR - FTableRowBase
#include "Engine/DataTable.h"

// STANDARD INCLUDES 
//#include "CoreMinimal.h"
//#include "UObject/NoExportTypes.h"
#include "DT_Test.generated.h"

/**
 * 
 */

USTRUCT(BlueprintType)
struct F_TEST_DATATABLE : public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FName T_ID;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FName T_DATA;
};

In my headerfile I have

//-----------------------------FUNCTIONS FOR LEVEL PARAMETERS - START
	UPROPERTY()
		F_TEST_DATATABLE RowData;

	UFUNCTION()
		F_TEST_DATATABLE& GetDT_TEST_RowData(UDataTable* TheDataTable, FName &TheRowName);

	UFUNCTION()
		void LogDT_TEST_RowData(F_TEST_DATATABLE &TheRowData);
	//-----------------------------FUNCTIONS FOR LEVEL PARAMETERS - END

in cpp I have

//-----------------------------FUNCTIONS FOR TEST PARAMETERS - START
F_TEST_DATATABLE& UCON_Datatable::GetDT_TEST_RowData(UDataTable* TheDataTable, FName &TheRowName)
{
	RowData = *TheDataTable->FindRow<F_TEST_DATATABLE>(TheRowName, "");

	return RowData;
}

void UCON_Datatable::LogDT_TEST_RowData(F_TEST_DATATABLE &TheRowData)
{
	UL_Log::Log(LogIndex::Log_CON_Datatable, " ");
	UL_Log::Log(LogIndex::Log_CON_Datatable, "--------------TEST DATATABLE VALUE LOG--------------START");

	UL_Log::Log(LogIndex::Log_CON_Datatable, "VALUE = " + TheRowData.T_ID.ToString());
	UL_Log::Log(LogIndex::Log_CON_Datatable, "VALUE = " + TheRowData.T_DATA.ToString());

	UL_Log::Log(LogIndex::Log_CON_Datatable, "--------------TEST DATATABLE VALUE LOG--------------END");
	UL_Log::Log(LogIndex::Log_CON_Datatable, " ");
}
//-----------------------------FUNCTIONS FOR TEST PARAMETERS - END

If you want to expose this to Blueprint, then you have no choice but to copy. Blueprint does not support references/pointers to structs - the VM always copies them internally anyway. There is a dedicated Blueprint Node for getting row data from any table already.

In C++, the sensible approach is to return a const pointer. Returning a referenece here is dangerous because there’s no garauntee the table contains the row, and may return nothing - in this case that will result in a nullptr dereference.

As an addendum - I recommend not passing FName by reference. Epic doesn’t do this anywhere in the engine, and FNames are merely two int32’s - there’s no benefit to passing them by ref unless you want to modify them.

2 Likes

Thanks.

Im just using this to setup and extract info from my datatables purely in c++. I dont use blueprint except for widget design, but I do create the datatables in the Editor via a spreadsheet import.

The problem is, later on some of these datatable structs will be very large and need to be passed around in realtime, say 120fps, so I need to get this right. (for complex mechanical simulations)

In the past I resorted to copying these massive structs into a class once upon program initialization, and then using the class, which i can refer to by pointer (which works very well, but is a pain to set up). The speed increase was phenominal by not passing around huge structs by value.

I get an error of unexposed pointer type for structs I try to pass around by pointers, so I would like to learn how to define a struct once, and use it by reference only.

Is it technically bad (or just pointless) to pass FName by reference? Just wondering what the problem is…Where is the cutoff point? Should I not worry about passing FVectors around by reference for example?

The reason you’re getting the “unexposed pointer type for structs” is because you’ve made it part of a UFUNCTION() or a UPROPERTY() - and you cannot “expose” struct pointers to the reflection system.

If it’s a UPROPERTY() then it must be stored as a copy, there is no other way. I don’t know why you are using UPROPERTY() here though, unless that struct is keeping UObjects alive (unlikely since it’s from a Data Table).

You also have to be extremely careful that the data table itself does not become unloaded, since if it does, then your row pointer will be left dangling. If you really must cache a pointer to the row, then do something like this:

// UPROP to keep data table loaded
UPROPERTY(Transient)
const UDataTable* LoadedDataTable;

// cached row ptr
const FMyRow* CachedRowLookup;

If you want to read CachedRowLookup in Blueprint, then you can create a UFUNCTION to copy it to an output param like so:


UFUNCTION(BlueprintCallable)
bool GetCachedRowData(FMyRow& OutData) const
{
	if (LoadedDataTable && CachedRowLookup)
	{
		OutData = *CachedRowLookup;
		return true;
	}

	return false;
}

However, this is a copy - and as mentioned you can’t avoid that in Blueprint.

1 Like

Ok, good info for my understanding, but I am purely C++, I don’t use blueprints for anything.

I preload all my datatables into a Map when the program starts, and I can access them until I quit, so they dont (seem to) die, nor do they keep other things alive as far as I can deduce.

I guess im using UPROPERTY() out of habit. Is it ok for them to live in code without this (doesn’t cause memory leaks). I’m not advanced understanding of coding or how the engine functions, somewhat intermediate.

The only reason to use UPROPERTY is if you need it exposed to reflection (or Blueprint) or serialized for some reason, which is not the case here. Or in the case of UObject pointers, to prevent them being automatically GC’d or unloaded.

The “Transient” flag in this particular case is just good house-keeping - the property will keep the data table loaded, but will not serialize the value on load/save.

Just be aware that the lifetime of that pointer is tied to that data table. If the data table is unloaded, then the pointer will be left dangling. If you aren’t using the pointer that often, you may be better off just looking it up each time you need it.

F_TEST_DATATABLE& UCON_Datatable::GetDT_TEST_RowData(UDataTable* TheDataTable, FName &TheRowName)
{
	RowData = *TheDataTable->FindRow<F_TEST_DATATABLE>(TheRowName, "");

	return RowData;
}

Ok, while I digest this, am I doing this correctly in this function? (returning the address of the struct rather than the struct by copy)

This will be useful to know as I have many cases where I want to improve my coding understanding.

is there any difference between these 3 statements. They all compile and give correct output.

RowData = *TheDataTable->FindRow<F_TEST_DATATABLE>(TheRowName, "");
RowData = *(TheDataTable)->FindRow<F_TEST_DATATABLE>(TheRowName, "");
RowData = *(TheDataTable->FindRow<F_TEST_DATATABLE>(TheRowName, ""));

Great nugget of information right there, thanks!

IMO, you should return a const pointer. It’s actually a failing of the engines’ API to not return a const pointer from FindRow IMO.

GetDT_TEST_RowData() could fail to find the row, in which case your program will crash when you try to dereference it. This will be true whether you return a copy or a reference.

Also, returning a non-const reference implies that the return value can be modified - which it shouldn’t be.

This is actually going a bit beyond my original question, but this is GOOD because I am forced to think about other things I dont concern myself with as a solo developer, so to return a reference address to a struct I should be using…My example is a bit simplified as there are checks on RowNames before calling this function to ensure it exists…

.h

UFUNCTION()
		const F_TEST_DATATABLE& GetDT_TEST_RowData(UDataTable* TheDataTable, FName TheRowName);