C++ Scripting: Part 30 – Overloaded Types and Decimal
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
- 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
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.