C++ Scripting: Part 18 – Array Index Operator
When we covered arrays in part 14, we skipped implementing the []
operator with them. Instead, we opted for a simpler pair of GetItem
and SetItem
functions. Today we’ll address that oversight so our C++ game code can index arrays just like in C#.
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
When we implemented arrays in part 14, we used GetItem
and SetItem
functions to access them. The generated C++ class would look like this:
struct Array1<float> : Array { float GetItem(int32_t index); void SetItem(int32_t index, float item); };
Then we’d use it like this:
// Create an array of 10 floats Array1<float> floats(10); // Set the first element to pi floats.SetItem(0, 3.14f); // Get the first element float pi = floats.GetItem(0);
GetItem
and SetItem
are clumbsy names invented to avoid needing to overload the array index operator: []
. Today we’ll replace them so we can write much more natural code that uses arrays like this:
// Create an array of 10 floats Array1<float> floats(10); // Set the first element to pi floats[0] = 3.14f; // Get the first element float pi = floats[0];
This is actually not as straightforward as it looks. The reason is that overloaded array index operators in C++ look like this:
Type& operator[](int32_t index)
Both the return value and the parameter list turn out to be problematic. Let’s start with the return value. Normally, a C++ class would return a reference (Type&
) to a value it contains. The caller would then either dereference this to get or set the value. In the case of an array class that represents an array stored in C#, there’s no value to return a reference to since C++ can’t get a pointer to values in a C# array.
To work around this, we introduce a proxy class. It’ll represent an array and an index into it, but not the action of getting or setting the element. So the array class will look like this:
struct Array1<float> : Array { ArrayElementProxy<float> operator[](int32_t index); };
Then we can overload operators on the proxy class to allow for natural-looking reading and writing:
struct ArrayElementProxy<float> { // Implicit conversion operator to get elements operator float(); // Assignment operator to set elements operator=(float item); };
Now code that uses the array can look like this:
Array<float> floats(10); // Index into the array to get the proxy ArrayElementProxy<float> element = floats[0]; // Use the proxy's assignment operator to set the element element = 3.14f; // Use the proxy's implicit conversion operator to get the element float got = element;
Usually we wouldn’t explicitly use the proxy type though. Normally it would just be a temporary variable like so:
Array<float> floats(10); // Index into the array to get the proxy // Then use its assignment operator to set the element floats[0] = 3.14f; // Index into the array to get the proxy // Then use its implicit conversion operator to get the element float got = floats[0];
This is just the syntax we want! So how do we implement the proxy? A simple one like this is actually quite simple to implement. We just need the array to pass its handle and the index to the proxy so it has what it needs to make the same calls that GetItem
and SetItem
used to make. Let’s start by looking at the proxy:
struct ArrayElementProxy<float> { // The array's handle int Handle; // Index of the element to get and set int Index; operator float() { return FloatArray1GetItem(Handle, Index); } operator=(float item) { FloatArray1SetItem(Handle, Index, item); } };
Now it’s easy to implement the array index operator in the array class:
struct Array1<float> : Array { operator[](int32_t index) { ArrayElementProxy<float> proxy; proxy.Handle = Handle; proxy.Index = index; return proxy; } };
That effectively takes care of the return value part of the problem. Unfortunately, the parameter list also contains a problem. The issue here is that C++ array index operators must be binary operators. They take this
as the first parameter and the index as the second parameter. That’s fine for the above Array1<float>
class because it only takes one index to specify an element. However, multi-dimensional arrays require multiple indexes to specify an element and we can’t add multiple parameters or the operator wouldn’t be binary anymore.
The workaround here is to introduce multiple levels of proxy classes. The array returns a proxy with the first index and another array index operator to get the next proxy. When enough indexes are collected, the final proxy is returned and it looks just like above. Here’s an example of the chain of proxy classes:
struct Array3<float> : Array { // Array class returns a proxy with 1 of 3 required indexes ArrayElementProxy1_3 operator[](int32_t index) { ArrayElementProxy1_3<float> proxy; proxy.Handle = Handle; proxy.Index0 = index; return proxy; } }; // 1 of 3 required indexes struct ArrayElementProxy1_3<float> : Array { int Handle; int Index0; // Return a proxy with 2 of 3 required indexes ArrayElementProxy2_3 operator[](int32_t index) { ArrayElementProxy2_3<float> proxy; proxy.Handle = Handle; proxy.Index0 = Index0; proxy.Index1 = index; return proxy; } }; // 2 of 3 required indexes struct ArrayElementProxy2_3<float> : Array { int Handle; int Index0; int Index1; // Return the final proxy ArrayElementProxy3_3 operator[](int32_t index) { ArrayElementProxy3_3<float> proxy; proxy.Handle = Handle; proxy.Index0 = Index0; proxy.Index1 = Index1; proxy.Index2 = index; return proxy; } }; // The final proxy. Has all required indexes. struct ArrayElementProxy3_3<float> { int Handle; int Index0; int Index1; int Index2; operator float() { return FloatArray3GetItem(Handle, Index0, Index1, Index2); } operator=(float item) { FloatArray3SetItem(Handle, Index0, Index1, Index2, item); } };
Thankfully, using this chain of proxy classes is really straightforward. It’s almost like the C# syntax and exactly like the C++ syntax for accessing a multi-dimensional array’s elements. Here’s how it looks:
// Create the array with 1 element in the first rank, 2 in the second, 3 in the third Array3<float> floats(1, 2, 3); // Index to get the ArrayElementProxy1_3 // Index that to get the ArrayElementProxy2_3 // Index that to get the ArrayElementProxy3_3 // Use its assignment operator to set the element floats[0][1][2] = 3.14f; // Index as above // Use the implicit conversion operator to get the element float val = floats[0][1][2];
Note that it’s important to not use the proxy classes after all references to the array are released because the handles they contain will be invalid at that point. We could fix this issue by making the proxy classes increment and decrement the reference count for those handles, but the cost of doing so seems too high because the proxy classes aren’t usually stored and this shouldn’t ever be an issue. Still, keep this in mind whenever writing advanced code that stores the proxy classes.
Now we have much more natural syntax compared to the old GetItem
and SetItem
functions! By getting rid of these awkward functions, we’re one step closer to a C++ scripting environment that’s pleasant to use. As usual, all the code for this is now in the GitHub project so feel free to try it out or check out the full source code.