C++ Scripting: Part 27 – Foreach Loops
C++ doesn’t have a foreach
keyword, but it does have an equivalent in “range for
loops”. Today we’ll implement support for them so we can easily loop over arrays and types implementing IEnumerable
and IEnumerable<T>
.
Table of Contents
- Part 1: C#/C++ Communication
- Part 2: Update C++ Without Restarting the Editor
- Part 3: Object-Oriented Bindings
- Part 4: Performance Validation
- Part 5: Bindings Code Generator
- Part 6: Building the C++ Plugin
- Part 7: MonoBehaviour Messages
- Part 8: Platform-Dependent Compilation
- Part 9: Out and Ref Parameters
- Part 10: Full Generics Support
- Part 11: Collaborators, Structs, and Enums
- Part 12: Exceptions
- Part 13: Operator Overloading, Indexers, and Type Conversion
- Part 14: Arrays
- Part 15: Delegates
- Part 16: Events
- Part 17: Boxing and Unboxing
- Part 18: Array Index Operator
- Part 19: Implement C# Interfaces with C++ Classes
- Part 20: Performance Improvements
- Part 21: Implement C# Properties and Indexers in C++
- Part 22: Full Base Type Support
- Part 23: Base Type APIs
- Part 24: Default Parameters
- Part 25: Full Type Hierarchy
- Part 26: Hot Reloading
- Part 27: Foreach Loops
- Part 28: Value Types Overhaul
- Part 29: Factory Functions and New MonoBehaviours
- Part 30: Overloaded Types and Decimal
C++’s “range for
loops” look like this:
for (String str : listOfStrings) { // ... do something with 'str' }
It’s just like C#’s foreach
except you just use for
and :
instead of foreach
and in
.
Just like foreach
, these loops are syntax sugar that gets expanded by the compiler into a normal for
loop:
for ( auto i = begin(listOfStrings), j = end(listOfStrings); i != j; ++i) { String& str = *i; // ... do something with 'str' }
Remember that auto
in C++ is like var
in C#, meaning the return type of the begin
and end
functions can be anything as long as they can take a single parameter of whatever type listOfString
is.
So let’s say listOfStrings
is a C# array that we’ve wrapped with Array1<String>
in C++. If we want to add support for “range for
loops”, we need to make begin
and end
functions like so:
namespace System { ArrayStringIterator begin(Array1<String>& array) { return ArrayStringIterator(array, 0); } ArrayStringIterator end(Array1<String>& array) { // We don't need the end iterator return ArrayStringIterator(nullptr, array.GetLength() - 1); } }
Now we need to implement ArrayStringIterator
with support for the operators the loop will use: ++
, !=
, and *
.
struct ArrayStringIterator { Array1<String>& array; int32_t index; ArrayStringIterator(Array1<String>& array, int32_t index) : array(array) , index(index) { } ArrayStringIterator& operator++() { index++; return *this; } bool operator!=(ArrayStringIterator& other) { return index != other.index; } String operator*() { return array[index]; } };
That’s all there is to arrays, so we’ll simply output the iterator type, begin
, and end
functions from the code generator for every type of array specified in the JSON.
Next up is IEnumerator<T>
. This works similarly, starting with begin
and end
:
namespace System { namespace Collections { namespace Generic { ListStringIterator begin(List<String>& list) { return ListStringIterator(list); } ListStringIterator end(List<String>& list) { return ListStringIterator(nullptr); } } } }
So far it’s basically the same except we’re using a different type of iterator. We also didn’t pass indexes to the iterator’s constructor because not all IEnumerable<T>
types can be indexed. We also passed nullptr
in end
to indicate that it won’t be used. This is a “forward iterator” only since IEnumerator<T>
doesn’t go backwards, unlike arrays. It’s also important that these functions be placed in the same namespace as the type they’re for. Since List<T>
is in System.Collections.Generic
, we have to put its corresponding begin
and end
there too.
Now let’s see how to make ListStringIterator
:
struct ListStringIterator { IEnumerator<String> enumerator; bool hasMore; // For the 'end' iterator // Just set to null ListStringIterator(decltype(nullptr)) : enumerator(nullptr) , hasMore(false) { } // For the 'begin' iterator // Get the IEnumerator<T> and start it iterating ListStringIterator(IEnumerable<T> enumerable) : enumerator(enumerable.GetEnumerator()) { hasMore = enumerator.MoveNext(); } ListStringIterator& operator++() { hasMore = enumerator.MoveNext(); return *this; } bool operator!=(ListStringIterator& other) { return hasMore; } String operator*() { return enumerator.GetCurrent(); } };
This is basically the same as what foreach
loops in C# expand to. We call GetEnumerator()
and then call MoveNext
repeatedly on it until it returns false
. We call the Current
property each time to get the current value.
Again we can add this to the code generator for any type that implements IEnumerable<T>
so we have “range for
loop” support on all of them that are specified in the JSON config file.
Finally, there’s the plain, non-generic IEnumerable
. Since there’s only one of these, it’s just added directly to the C++ bindings layer rather than going through the code generator. It looks very similar to the above code that we used for IEnumerable<T>
:
namespace System { namespace Collections { EnumerableIterator begin(IEnumerable& enumerable) { return EnumerableIterator(list); } EnumerableIterator end(IEnumerable& enumerable) { return EnumerableIterator(nullptr); } } } struct EnumerableIterator { IEnumerator enumerator; bool hasMore; // For the 'end' iterator // Just set to null EnumerableIterator(decltype(nullptr)) : enumerator(nullptr) , hasMore(false) { } // For the 'begin' iterator // Get the IEnumerator and start it iterating EnumerableIterator(IEnumerable enumerable) : enumerator(enumerable.GetEnumerator()) { hasMore = enumerator.MoveNext(); } EnumerableIterator& operator++() { hasMore = enumerator.MoveNext(); return *this; } bool operator!=(EnumerableIterator& other) { return hasMore; } Object operator*() { return enumerator.GetCurrent(); } };
Now that we have this available, let’s look at a few examples that use “range for
loops” starting with arrays:
// Make an int[3] and fill it with [10, 20, 30] Array1<int32_t> array(3); array[0] = 10; array[1] = 20; array[2] = 30; // Iterate over the elements of the array with a "range for loop" for (int32_t val : array) { // Box and log the current value Object msg = (Object)val; Debug::Log(msg); }
Now here’s IEnumerable<T>
, which looks almost the same:
List<int32_t> list; list.Add(100); list.Add(200); list.Add(300); for (int32_t val : list) { Object msg = (Object)val; Debug::Log(msg); }
Finally, let’s loop over a Transform
to see all its children. We can do this because Transform
implements IEnumerable
.
// Create a GameObject and get its transform GameObject go; Transform transform = go.GetTransform(); // Add child A String name = "Child A"; GameObject child(name); Transform childTransform = child.GetTransform(); childTransform.SetParent(transform); // Add child B name = "Child B"; child = GameObject(name); childTransform = child.GetTransform(); childTransform.SetParent(transform); // Add child C name = "Child C"; child = GameObject(name); childTransform = child.GetTransform(); childTransform.SetParent(transform); // Loop over the children of the transform with a "range for loop" for (System::Object obj : transform) { Debug::Log(obj); }
With that, we have support for the equivalent of foreach
in C++. It’s a simpler, terser, less error-prone kind of loop that many find attractive. The code is now available on the GitHub project if you’d like to check it out. Happy looping!