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

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.