Iterators

Table of contents

Explanation

Iterators, or more precisely lazy iterators, are an useful tool for more efficient and ergonomic processing of collections and procedural generators. Lazy iterators are lazy because on their own they do not yield any values until consumed.

// [0.0, 4.0, 16.0, 36.0, 64.0]
const TArray<float> Result = IterRange(0, 10)
								 .Filter([](const auto& Value) { return Value % 2 == 0; })
								 .Map<float>([](const auto& Value) { return static_cast<float>(Value * Value); })
								 .CollectArray();

This lazyness allows us to produce more elaborated processing over data without making unnecessary caching between iteration stages, and that also makes iteration code cleaner and easier to reason about.

Types of iterators

Creating custom iterators

While built-in iterators provided by SystemsArchitecture should be enough for common usecases, sometimes for more advanced solutions you might find yourself in a need of creating a custom iterator that cannot be solved using built-in ones.

Anatomy if iterators

Let's take a look at Repeat iterator:

#pragma once

#include "CoreMinimal.h"

// Make sure to include converters declaration header first.
#include "Systems/Public/Iterator/ConvertersDecl.h"

// Then converters, macros and size hint, order doesn't matter here.
#include "Systems/Public/Iterator/Converters.h"
#include "Systems/Public/Iterator/Macros.h"
#include "Systems/Public/Iterator/SizeHint.h"

// This iterator is generalized over type of values it yields.
template <typename T>
struct TIterRepeat
{
public:
	// We need to provide type aliases for iterators injected with macros will
	// properly understand this iterato when wrapping it internally.
	using Self = TIterRepeat<T>;
	using Item = typename T;

	TIterRepeat() : Value(TOptional<T>())
	{
	}

	TIterRepeat(T Data) : Value(TOptional<T>(Data))
	{
	}

	// Provide `Next` method that returns optional value of item type.
	TOptional<Item> Next()
	{
		return TOptional<T>(this->Value);
	}

	// Provide `Sizehint` method thar returns hint about estimated range of
	// items it can yield.
	IterSizeHint SizeHint() const
	{
		return IterSizeHint{0, TOptional<uint32>()};
	}

private:
	TOptional<T> Value = {};

public:
	// To make iterators chains we call special macro that injects all other
	// built-in iterators as this one methods.
	ITER_IMPL
};

// Provide handy construction function for ergonomics, just because of C++
// having easier times with types deduction on them.
template <typename T>
TIterRepeat<T> IterRepeat(T Value)
{
	return TIterRepeat<T>(Value);
}
  • Self type alias

    Iterators chaining is done by wrapping consecutive iterators in one another so when consumer calls Next, it internally calls Next of iterator it wraps, and so on. To make them easily injectable with macros, they use Self as an alias for their type.

  • Item type alias

    Type alias for type of value that this iterator yields. Not stores, not takes as an input - exactly one that it yields. This is again used for iterators wrapping purposes so converter iterators that wraps other iterators can identify value type of previous one in chain by typename ITERATOR::Item.

  • Next method

    Every iterator should implement TOptional<Item> Next() method. This method does the actual job of yielding a value.

    See TQuery::Next.

  • SizeHint method.

    Size hints are used by for example collector iterators to estimate the capacity of collection where yielded data will be stored. This at most removes reallocations when adding every next value into that collection, and at least reduces it to some reasonable number.

    Finite iterators will give lower bounds and upper bounds set, infinite iterators will give only lower bounds.

    See TQuery::SizeHint, IterSizeHint.

  • ITER_IMPL macro

    This macro injects other iterators as methods of this one. Without it user would be left with ugly iterators chains made by passing next stages as argument to iterator functions/constructors.

Iterator adapters

In some rare cases you might find yourself struggling to express your data processing with built-in iterators, or you must to make some optimizations that cannot be solved using provided ones. For this advanced usecase you can create custom iterator adapters that works basically the same way as converter iterators.

template <typename T>
struct TIterOddAdapter
{
public:
	template <typename I>
	TOptional<T> Next(I& Iter)
	{
		Iter.Next();
		return Iter.Next();
	}

	template <typename I>
	IterSizeHint SizeHint(const I& Iter) const
	{
		return Iter.SizeHint();
	}
};
// [1, 3, 5, 7, 9]
const TArray<int> Result = IterRange(0, 10).Adapt(TIterOddAdapter<int>()).CollectArray();

They differ from regular iterators in a way that they do not need type aliases and their Next and SizeHint methods require reference to previous stage iterator so they both consume and process its yielded values.


Documentation built with Unreal-Doc v1.0.8 tool by PsichiX