C# allows for overloading not just function names, but also type names. This is used throughout the .NET and Unity APIs for interfaces like IEnumerable and IEnumerable<T>, classes like UnityEvent<T0> and UnityEvent<T0, T1>, and delegates like Action<T1, T2> and Action<T1, T2, T3>. C++, however, does not support type overloading. Today’s article explores how to deal with this and, once we’ve solved the issue, what extra C# features we’ll have access to in C++.

Table of Contents

Type overloading is surprisingly common in C#, despite not being supported at all in other popular languages like Java and C++. So far, this has limited us in some areas. For example, in part 28 we added support for boxing to and unboxing from all of the interfaces that value types supposedly implement. Since int needs to support boxing to IComparable as well as IComparable<int>, we ran into an issue. There was no way for C++ to have both IComparable and IComparable<int> since it doesn’t support type overloading. So we simply left off support for boxing to IComparable<int> until a solution could be found.

Today we’ll solve the issue with a technique similar to the one we used in part 14 when we implemented arrays. With arrays, we can have single-dimensional arrays (int[]) which are known as “vectors” in .NET and we can have multi-dimensional arrays (int[,,]) to make N-dimensional tables. Because of the lack of support for type overloading in C++, we named these Array1, Array2, Array3, etc. The number suffix indicates the number of dimensions of the array: 1, 2, 3, etc.

A similar technique can be easily applied to disambiguate generic types like IEnumerable<T> from their non-generic counterparts like IEnumerable. We’d name these IEnumerable_1<T> and IEnumerable, leaving off the 0 for non-generic types. We can also disambiguate generic types that have different numbers of type parameters, like UnityEvent<T0> and UnityEvent<T0, T1> by naming them UnityEvent_1<T0> and UnityEvent_2<T0, T1>. In fact, this is how generic types are named in .NET except that a an _ is used instead of ` to comply with the rules for identifier names.

With this in place, we can now add support for boxing to IComparable<T> and IEquatable<T>. All of the primitive types like int can be boxed to them and unboxed from them. Struct and enum types that “implement” these interfaces can also be boxed to and unboxed from these types.

This was also the last reason that we couldn’t support decimal from the code generator. Now that it can box to and from IComparable and IComparable_1<int>, we can simply add it to the code generator’s JSON config file. Here we add constructors from double and ulong and the + operator.

{
	"Types": [
		{
			"Name": "System.Decimal",
			"Constructors": [
				{
					"ParamTypes": [
						"System.Double"
					]
				},
				{
					"ParamTypes": [
						"System.UInt64"
					]
				}
			],
			"Methods": [
				{
					"Name": "x+y",
					"ParamTypes": [
						"System.Decimal",
						"System.Decimal"
					]
				}
			]
		}
	]
}

We also have to add in IEquatable<decimal> and IComparable<decimal> to support boxing to these types. Notice the `1 that .NET uses to disambiguate overloaded type names:

{
	"Types": [
		{
			"Name": "System.IEquatable`1",
			"GenericParams": [
				{
					"Types": [
						"System.Decimal"
					]
				}
			]
		},
		{
			"Name": "System.IComparable`1",
			"GenericParams": [
				{
					"Types": [
						"System.Decimal"
					]
				}
			]
		},
	]
}

Now that we have decimal in C++, let’s try it out!

// Make some decimals out of doubles
Decimal pi(3.14);
Decimal two(2.0);
 
// Add them together
Decimal sum = pi + two;
 
// Pass one as a parameter
// This prints it to a string
StringWriter writer;
writer.Write(sum);
 
// Get the string printed to
String str = writer.ToString();
 
// Log it
Debug::Log(str); // 5.14

It works!

In C#, we could have typed Decimal pi = 3.14m because the language itself supports decimal literals. C++ doesn’t support decimal literals because that’s a .NET concept, so we can’t use the m suffix. It does, however support user-defined literals which we can use to create our own version of the m suffix.

System::Decimal operator"" _m(long double x)
{
	return System::Decimal((System::Double)x);
}
 
System::Decimal operator"" _m(unsigned long long x)
{
	return System::Decimal((System::UInt64)x);
}

operator"" is the C++ way to indicate a user-defined literal. The _m part is the literal’s suffix. User-defined literals must start with _ since suffixes like f and s are reserved by the language itself and the standard library (a.k.a. STL). Other than that, these are just regular functions that we implement by calling the appropriate constructor.

Now that we have these, we can rewrite the first two lines of the above example:

Decimal pi = 3.14_m;
Decimal two = 2.0_m;

It’s worth noting at this point that this way of supporting decimal is quite inefficient. We need to call into C# code to perform every operation from construction to addition. We’re reference-counting them in C++ and putting them in a StructStore in C#. This is far from just using them directly like we’d do with a float in C++. However, decimal usage is rare in most Unity games so optimizing it is by no means critical. Future updates to the C++ scripting system can address this if necessary.

Finally, let’s look at the boxing support for decimal that enabled us to use it in the first place. First, here’s IComparable:

Decimal pi = 3.14_m;
Decimal two = 2.0_m;
 
// Box 'pi' to an 'IComparable'
IComparable piComparable = (IComparable)pi;
 
// Box 'two' to an 'object'
Object twoObj = (Object)two;
 
// Call the 'CompareTo' method of 'IComparable' to compare to an 'object'
Int32 comparison = piComparable.CompareTo(twoObj);
 
// Box and print the result
System::Object msg = (Object)comparison;
Debug::Log(msg); // 1 (because 3.14 is greater than 2.0)

Similarly, we can now use IComparable<decimal>:

Decimal pi = 3.14_m;
Decimal two = 2.0_m;
 
// Box 'pi' to an 'IComparable<decimal>'
IComparable_1<Decimal> piComparable = (IComparable_1<Decimal>)pi;
 
// Call the 'CompareTo' method of 'IComparable' to compare to a 'decimal'
// No need to box 'two' to an 'object'
Int32 comparison = piComparable.CompareTo(two);
 
// Box and print the result
System::Object msg = (Object)comparison;
Debug::Log(msg); // 1 (because 3.14 is greater than 2.0)

Disambiguating overloaded type names for C++ has made a lot of tricky areas of C# accessible to us. Supporting decimal, IComparable<T>, and IEquatable<T> are just a few examples. This is all available now on the GitHub project if you’d like to check it out.