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
Feedback
Please leave any feedback about this article here