C++ Scripting: Part 13 – Operator Overloading, Indexers, and Type Conversion
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
- 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
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.