Using C++20 with Unreal

Updated for Unreal Engine 5.1.1

Using C++20

The code here is developed and tested using Visual Studio 2022 setup as described here

Project Configuration

As at April 2023 Unreal Engine is configured to use c++17 by default.

To configure a project to use c++20 add this line to the Project.build.cs

CppStandard = CppStandardVersion.Cpp20;

Subobjects

The USubobjectDataSubsystem class has methods for finding the components which make up a blueprint. We will use this as a example. The stages of getting from a blueprint to a specific component (whether by type or name) are:

  • the USubobjectDataSubsystem gives us a list of FSubobjectDataHandle data handles
  • from the FSubobjectDataHandle data handles we can get a list of FSubobjectData data items
  • from the FSubobjectData data items we can get a list of UObject pointers (each of which is a component)
  • then we can filter the UObject pointers to get the type we want

The process of getting from the list of FSubobjectDataHandle data handles to a smaller list of components looks like this in python, using nested list comprehensions:

components = 
    [ comp for comp in 
		[ library.get_object(subobject) for subobject in 
			[ subsystem.k2_find_subobject_data_from_handle(handle) for handle in 
					subsystem.k2_gather_subobject_data_for_blueprint(context=blueprint) ] 
			]
		if comp.get_class().get_name() == "MyActorComponent"
	]

Ranges

To do the same thing in c++ as the python code above does we will use the std::ranges classes, new in c++20.

Having included the <ranges> header file we can write this like so:

UBlueprint* CarBlueprint = Cast<UBlueprint>( UEditorAssetLibrary::LoadAsset
            ("/Game/VehicleTemplate/Blueprints/SportsCar/SportsCar_Pawn"));

TArray< FSubobjectDataHandle > SubobjectDataHandles;

// get the subobject data handles into an unreal TArray
SubobjectDataSubsystem->K2_GatherSubobjectDataForBlueprint(CarBlueprint, SubobjectDataHandles);

// copy from the TArray to a std::vector
std::vector< FSubobjectDataHandle > Handles(SubobjectDataHandles.Num());
for (int j = 0; j < SubobjectDataHandles.Num(); ++j)
{
    Handles[j] = SubobjectDataHandles[j];
}

// create a range of USkeletalMeshComponent* from the vector of handles

auto range = Handles
    | std::views::transform([&SubobjectDataSubsystem](const FSubobjectDataHandle& Handle) {
        // handles to FSubobjectData
        FSubobjectData Data;
        SubobjectDataSubsystem->K2_FindSubobjectDataFromHandle(Handle, Data);
        return Data;
    })
    | std::views::transform([](const FSubobjectData& Data) {
        // FSubobjectData to UObject*
        return USubobjectDataBlueprintFunctionLibrary::GetObject(Data);
    })
    | std::views::filter([](const UObject* Object) {
        // just return the skeletal mesh components
        return ( Cast< const USkeletalMeshComponent >(Object) != nullptr );
    })
    | std::views::transform([](const UObject* Object) {
        // UObject* to USkeletalMeshComponent*&
        return Cast< const USkeletalMeshComponent >(Object);
    });

Each stage of the process is either a call to std::views::transform which transforms an item in the list from one type to another, or a call to std::views::filter which removes items from the list if they do not match the selection criteria.

The result of the code above is the objects range on which we can use in a loop to iterate over all the USkeletalMeshComponent like so:

for (const USkeletalMeshComponent* SkelMesh : range)
{
    CompareSkeletalMeshComponents( SkelMesh, OtherSkelMesh );
}

Structured Bindings

Given a C++ type such as FVector which has members X, Y, Z, structured bindings allows us to deconstruct an FVector variable into three separate values like this:

FVector Vector(1.0f,2.0f,3.0f);
auto [x, v, z] = Vector;

We need to write come code to make the structured binding work. Looking at https://en.cppreference.com/w/cpp/language/structured_binding we are using "Case 2: binding a tuple-like type", so we need to:

  • implement std::tuple_size<> to tell the compiler how many variables an FVector will be deconstructed to
  • implement std::tuple_element<> to tell the compiler the type of each element of the FVector
  • implement get<>() to tell the compiler how to get each element from the FVector

The FVector will be deconstructed into 3 separate varibales, so we implement std::tuple_size<> to tell the compiler how many variables a FVector will be deconstructed to like this:

namespace std
{
	template<>
	struct tuple_size<FVector> 
	{
		static constexpr int value = 3;
	};
}

We implement std::tuple_element<> to tell the compiler the type of element of the FVector like this:

namespace std 
{
	template<size_t Index>
	struct tuple_element<Index, FVector>
	{
		using type = FVector::FReal;
	};
}

We implement get<>() which is called for index values from 0 to std::tuple_size (i.e. 3). Note that the get<>() function does not need to be in the std namespace. The compiler using argument-dependent lookup; it looks for the get<>() function in the namespace which the FVector is in. FVector is declared like this:

using FVector = UE::Math::TVector<double>;

so the compiler looks for the get<>() function in the UE::Math namespace, so we put it there:

namespace UE::Math
{
    template<size_t Index>
    constexpr auto get(const FVector& Vector)
    {
    if constexpr (Index == 0) return Vector.X;
    if constexpr (Index == 1) return Vector.Y;
    if constexpr (Index == 2) return Vector.Z;
    }
}

Once we have the above code we can use structured binding to deconstruct an FVector variable like this:

void Test()
{
	FVector Vector(1.0f, 2.0f, 3.0f);
	auto [x, y, z] = Vector;
}

Idioms

These are common patterns seen in the Unreal Engine source code, not necessarily specific to C++ 20.

Anonymous Enums

For example:

template<>
struct TMassExternalSubsystemTraits<UMassTestGameInstanceSubsystem>
{
	enum
	{
		GameThreadOnly = false,
		ThreadSafeRead = true,
		ThreadSafeWrite = false,
	};
};

This declares the three values specified in the enum without allocating any variables. When the compiler sees the variable TMassExternalSubsystemTraits<UMassTestGameInstanceSubsystem>::GameThreadOnly it substitues the value false. Because no variable is allocated it does not take any memory and the value cannot be changed.

References

Microsoft Ranges
Making a Container from a C++20 Range
Filtering Containers
Structured Bindings