Today’s article continues the series by adding support for C++ to call the various overloaded operators and indexers that are written in C#. This includes support for all 24 overloadable operators in C# plus the explicit and implicit type conversion operators. Indexers aren’t quite overloaded operators, but they allow for array-like indexing into C# types so they’re included today. Read on to learn how all this support was implemented in the GitHub project!

Table of Contents

Let’s start off today by talking about indexers. Say we’re writing a class like List<T> and we want to provide array-like access. We can use an indexer to do this. Here’s a very simple array wrapper to illustrate:

class List<T>
{
	private T[] array;
 
	public T this[int index]
	{
		get { return array[index]; }
		set { array[index] = value; }
	}
}

This syntax looks just like the syntax for a property. As it turns out, indexers are actually implemented as properties. There are really just two differences between them and other properties. First, this property takes an additional parameter to specify the index. We can use whatever type we want here, not just int. This is how classes like Dictionary<TKey, TValue> can allow users to index into them with a TKey. The second difference is the syntax for using an indexer. Instead of var x = obj.Property; or obj.Property = x; we use var x = obj[index]; and obj[index] = x;.

As usual, we want game programmers to be able to write C++ code that’s just like the C# code they would normally have written. Unfortunately, there’s a difference in this case that makes that difficult. C# is essentially providing two methods: T GetItem(int index) and void SetItem(int index, T value). C++ provides the opportunity to overload the array index operator: []. Normally, this overloaded operator returns a reference to the indexed value so it can be read from or written to. That means the overloaded operator doesn’t know if the caller wants to use GetItem or SetItem. We may be able to work around this paradigm difference in a future update to the system, but for now we’ll just expose two functions: GetItem and SetItem. That means the C++ code to use a List<T> looks like this:

List<String> list;
list.Add("one");
list.Add("two");
list.Add("three");
 
// Use the "set" indexer
list.SetItem(0, "new one");
 
// Use the "get" indexer
String first = list.GetItem(0);
Debug::Log(first); // prints "new one"

That’s really all there is to implement for indexers. With this support we can “index into” common types like List<T>, Dictionary<TKey, TValue>, and even Vector3. Now let’s move on to support the operator overloading. Here are all 24 operators we can overload in C#:

  • +x
  • -x
  • !x
  • ~x
  • x++
  • x–
  • (true)x
  • (false)x
  • x+y
  • x-y
  • x*y
  • x/y
  • x%y
  • x&y
  • x|y
  • x^y
  • x<<y
  • x>>y
  • x==y
  • x!=y
  • x<y
  • x>y
  • x<=y
  • x>=y

Overloaded operators are actually just methods in .NET. The compiler generates strange names for them starting with “op_”. For example, operator-- turns into “op_Decrement”. So instead of these strange names, the code generator will accept the above list of names as they’re a lot easier to memorize.

Since overloaded operators are just methods, most of the existing work to support methods applies here. Like with indexers, we need to modify the syntax we use to call them. We can’t simply call Vector3.op_Multiply(x, y) because that’s not legal C# even if the .NET name really is “op_Multiply”. We have to write bindings code that calls x * y to us the overloaded operator. Likewise, we want the C++ game code to use these overloaded operators just like they would be used in C#. Luckily, there’s a one-to-one mapping between C# and C++ for almost all of these operators.

This one-to-one mapping means that we don’t have to do much to support all these operators in C++. First, we need to write out C++ code with names like operator/ instead of op_Division. (Yes, the opposite of “op_Multiply” really isn’t “op_Divide”). Second, we need to make them instance methods instead of static methods because that’s how they’re specified in C++.

Let’s step through an example to illustrate how this all ties together. Say we want to expose a C# class like this to C++:

class IntWrapper
{
	public int Value;
 
	public static IntWrapper operator+(IntWrapper a, IntWrapper b)
	{
		return new IntWrapper { Value = a.Value + b.Value };
	}
}

We’ll have a C# binding function like this:

public static IntWrapperOperatorPlus(int aHandle, int bHandle)
{
	// Get instances from ObjectStore using object handles
	IntWrapper a = (IntWrapper)ObjectStore.Get(aHandle);
	IntWrapper b = (IntWrapper)ObjectStore.Get(bHandle);
 
	// Call the overloaded operator
	IntWrapper returnValue = a + b;
 
	// Store the return value in ObjectStore to get an object handle
	int returnValueHandle = ObjectStore.Store(returnValue);
 
	// Return the return value as an object handle
	return returnValueHandle;
}

Then we’ll have a C++ binding function like this:

struct IntWrapper
{
	IntWrapper operator+(IntWrapper other)
	{
		// Call the C# binding function and get the return value's object handle
		int32_t returnValueHandle = IntWrapperOperatorPlus(Handle, other.Handle);
 
		// Convert the return value's object handle to a real object and return it
		return IntWrapper(returnValueHandle);
	}
};

Now the C++ game code can use this just like it normally would use overloaded operators in C++:

IntWrapper a = new IntWrapper { Value = 123 };
IntWrapper b = new IntWrapper { Value = 456 };
IntWrapper sum = a + b;

Unfortunately, there are a couple of problematic operators: operator true and operator false. These are largely redundant with the type conversion operators we’ll support below. C++ has no concept of them, either. They’re also exceedingly rare as I can’t find any usage at all in the .NET or Unity APIs. Still, for the sake of completeness we’ll support them by generating C++ functions named TrueOperator and FalseOperator.

Finally, let’s talk about those type conversion operators that make operator bool possible in C#. The syntax is a little different from normal overloaded operators:

class IntWrapper
{
	public int Value;
 
	public static implicit operator int(IntWrapper iw)
	{
		return iw.Value;
	}
}
 
void Foo()
{
	IntWrapper a = new IntWrapper { Value = 123 };
	int i = a;
}

There are a couple of details to notice here. First is that we don’t specify a return type for the type conversion operator. That’s because it’s obvious: we’re returning the type to convert to. Second, we have to specify whether we want conversion to be explicit or implicit. If it’s explicit then users will need a cast: int i = (int)a. If it’s implicit then there’s no cast required.

C++ has type conversion operators that are just like this, which is great news because it’ll make binding very simple. For the IntWrapper, we can simply generate a C++ equivalent like this:

struct IntWrapper
{
	// Note: C++ default is "implicit"
	operator int32_t()
	{
		// Call the C++ binding function
		return IntWrapperOperatorImplicitInt(Handle);
	}
};

Then C++ game code can use it like this:

void Foo()
{
	IntWrapper a;
	int32_t i = a;
}

To specify type conversion operators in the code generator JSON, use the names “implicit” and “explicit”.

So now we have support for indexers, overloaded operators, and type conversion operators in C++. We’re one step closer to being able to use anything that C# can use from the C++ side and one step closer to a viable C# alternative. Check out the GitHub project for the full source code if you’re curious to see all the details or just want to try it out for yourself.

Reminder: if you’re interested in collaborating on this project, please contact me by e-mail or comment.