C++ Scripting: Part 25 – Full Type Hierarchy
So far we’ve had C++ classes that derive from other classes, but not their interfaces. Today we’ll make C++ classes implement all their interfaces to form a full type hierarchy. Along the way we’ll learn about how inheritance works in C++, specifically the esoteric form known as “virtual inheritance.”
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
We’ve had support for base classes in C++ since way back in part three when object-oriented support was added. For example, here’s List<String>
:
struct List<String> : Object { // ... contents };
That’s correct, but we know that List<String>
also implements a number of interfaces. So let’s naively just add them to the inheritance list:
struct List<String> : Object , ICollection<String> , IEnumerable<String> , IEnumerable , IList , ICollection { // ... contents };
Remember that C++ doesn’t have the concept of interfaces: only base classes. However, it supports multiple inheritance so that’s what we’re asking for here. This means that the List<String>
struct will now contain all of these interfaces inside itself. These interfaces also derive from each other and, ultimately, from Object
.
This leads to a lot of duplication that increases the size of List<String>
and, more importantly, leads to ambiguity. For example, Object
has it’s C# Handle
field so the Object
portion of List<String>
will include a Handle
. Then IEnumerable
also derives from Object
so it will also have a Handle
. So now when we write myList.Handle
, which should be used? This is known in C++ as the diamond problem.
The solution to this problem is to use “virtual inheritence.” This tells the compiler to modify the memory layout of the struct in such a way that there is only one of each base class. There are downsides to this, but they’re minor in this case. What’s more important is that virtual inheritance is the better match for what a C# type hierarchy actually looks like, so we’ll use it. To do so, we simply add the virtual
keyword to our base types:
struct List<String> : virtual Object , virtual ICollection<String> , virtual IEnumerable<String> , virtual IEnumerable , virtual IList , virtual ICollection { // ... contents };
Now we have exactly one of each base type inside of List<String>
. Unfortunately, this causes an issue with our constructors. Before when we had normal inheritance we could count on our base class to recursively call constructors up to the root of the type hierarchy. It looked like this:
List<String>::List<String>() : Object(nullptr) { // ... contents }
That would simply call the base Object
class’ constructor before, just like with a base(null)
call in C#. With virtual inheritance we need to explicitly call all our base types’ constructors. We need to call not only the ones we directly derive from, but all of their base types all the way up to the roots (plural) of the type hierarchy. For List<String>
, this can be extensive because it has so many interfaces:
List<String>::List() : System::Collections::IEnumerable(nullptr) , System::Collections::ICollection(nullptr) , System::Collections::IList(nullptr) , System::Collections::Generic::IEnumerable<String>(nullptr) , System::Collections::Generic::ICollection<String>(nullptr) , System::Collections::Generic::IList<String>(nullptr) { // ... contents }
If these base types have default constructors then we can omit all of these constructor calls. This is true of Object
, so it’s left out of the above list. It’s not true in the general case though as there are many types that don’t have a default constructor in C#. We also wouldn’t want to call that default constructor because we don’t want to create a base class and the derived class but instead just the derived class. This is why we use the innocuous nullptr
constructor as it simply sets Handle
to 0
.
The final requirement for us to handle with virtual inheritance is the ordering of the base type constructor calls. If we put them in the wrong order they’ll be rearranged by the compiler and we’ll get a warning. So we need to generate code that calls the base type constructors in the proper order.
We’re supposed to call our base type constructors starting with the first base type and ending with the last base type. However, at each of those base types we’re supposed to call the most base type in its own hierarchy followed by the next-most base type and so on down to the actual base type. We should also never call a base type’s constructor more than once. So for List<String>
, we have this type hierarchy:
- List<String>
- IList
- ICollection
- IEnumerable
- IEnumerable
- ICollection
- IList<String>
- ICollection<String>
- IEnumerable<String>
- IEnumerable
- IEnumerable<String>
- ICollection<String>
- IList
The above order is therefore correct. We start with IList
and find its most base type: IEnumerable
. Then it’s next-most base type ICollection
then finally IList
itself. We skip the IEnumerable
that IList
derives from because we already called its constructor. Then we move on to the next base type—IList<String>
—and do the same thing, remembering to skip IEnumerable
again.
Once we’ve applied this technique to all our generated types we’ll have a full type hierarchy. That leads to a much better match with the polymorphism we find in C# than we had by just listing a base class. For example, we can now pass a List<String>
to a function that takes a IEnumerable
parameter. We can even use all the methods, properties, indexers, fields, and events of all the interfaces it implements.
All of this is now available on the GitHub project. If you’ve got any questions about this or the project, feel free to post a comment!