IGRanges is an Unreal Engine 5 plugin that leverages the Ranges library (C++20) to provide LINQ (C#) style code patterns.
Unreal's container types (e.g. TArray<T>
) do not support Ranges out of the box (yet?), so this plugin first adds the necessary customization point objects to make them compatible.
Beyond that, a handful of common mapping & filtering operations have been implemented in ways that are familiar to programmers acquainted with UE & LINQ.
A quick primer for Ranges from cppreference.com:
The ranges library is an extension and generalization of the algorithms and iterator libraries that makes them more powerful by making them composable and less error-prone.
The library creates and manipulates range views, lightweight objects that indirectly represent iterable sequences (ranges).
The following example "pipes" an array of integers through views to generate a sequence that is a modified subset of the original values.
int myInts[] = { 0, 1, 2, 3, 4, 5 };
auto even = [](int i) { return i % 2 == 0; };
auto square = [](int i) { return i * i; };
// the "pipe" syntax of composing the views:
for (int i : myInts | std::views::filter(even) | std::views::transform(square))
std::cout << i << ' ';
// Output: 0 4 16
This example is very C++-looking. It's not very UE-looking & it doesn't read quite as clearly as LINQ.
If we were to rewrite it in a UE style with IGRanges, then it might look something like this:
TArray<int32> MyInts = { 0, 1, 2, 3, 4, 5 };
auto IsEven = [](int i) { return i % 2 == 0; };
auto Square = [](int i) { return i * i; };
FString Result;
for (int32 i : MyInts | Where(IsEven) | Select(Square))
{
Result.AppendInt(i);
Result += TEXT(", ");
}
UE_LOG(LogTemp, Log, TEXT("Result{%s}"), *Result);
// Output: Result{0, 4, 16,}
Or, depending on style preferences, it might look something like this:
TArray<int32> MyInts = { 0, 1, 2, 3, 4, 5 };
auto Sqevens = MyInts
| Where([](int i) { return i % 2 == 0; })
| Select([](int i) { return i * i; });
FString Result;
for (int32 i : Sqevens)
{
Result.AppendInt(i);
Result += TEXT(", ");
}
A better demonstration of IGRanges is shown in the following example.
The declarative syntax can make it easier to understand the intent of the code.
In this case, we have a collection of pointers to Actors (some of which might be null) & we want to get the World from one of them.
Imperative | Declarative |
---|---|
TArray<AActor*> MaybeActors = ...;
UWorld* World = nullptr;
for (const AActor* Actor : MaybeActors)
{
if (Actor != nullptr)
{
World = Actor->GetWorld();
break;
}
}
if (World != nullptr) ... |
TArray<AActor*> MaybeActors = ...;
UWorld* World = MaybeActors
| NonNull()
| Select(&AActor::GetWorld)
| FirstOrDefault();
if (World != nullptr) ...
|
Filtering & collecting elements also becomes very easy with IGRanges.
Imperative | Declarative |
---|---|
TArray<UObject*> SomeObjects = ...;
TArray<UFoo*> Foos;
for (UObject* Obj : SomeObjects)
{
if (UFoo* Foo = Cast<Foo>(Obj))
{
Foos.Add(Foo);
}
} |
TArray<UObject*> SomeObjects = ...;
TArray<UFoo*> Foos = SomeObjects | OfType<Foo>() | ToArray();
|
Where
,WhereNot
,SafeWhere
,SafeWhereNot
NonNull
,NonNullRef
Select
,SelectNonNull
Cast<T>
,CastExact<T>
,CastChecked<T>
,CastCheckedRef<T>
OfType<T>
,OfTypeRef<T>
,OfTypeExact<T>
,OfTypeExactRef<T>
,OfType
,OfTypeRef
FirstOrDefault
Count
Sum
Accumulate
ToArray
ToSet
All
,Any
,None
Selectors::CDO
Filters::IsChildOf<T>
,Filters::IsChildOf