Why C++/CLI Supports both Templates for CLI Types and the CLI Generic Mechanism
I've been recently puzzling out a strategy for presenting the two mechanisms supporting parameterized types available to the C++/CLI programmer: she can use either the template mechanism adapted for use with CLI types, or the CLI generic mechanism. This is not unique to the support of parameterized types, of course, but it seems a lightening rod for pernicky questions:
(1) isn't the support for two mechanisms that are similar in intent but which differ in both gross and subtle semantic behavior confusing for a user?
(2) doesn't the dual nature of these constructs increase the liklihood of programmer error?
(3) isn't this a canonical illustration of the undisciplined nature of the C++ language design where everything but the kitchen sink seems to get thrown in? you guys just can't get your act together, can you?
Let's see what kind of answers I can offer. [Disclaimer: of course, these are my thoughts, and do not represent either the corporate views or policies of Microsoft.]
The C++ binding to the CLI which I refer to as C++/CLI represents an integration of two separate object models: the static object model of native C++ and the dynamic program model of the CLI. We've seen conflicts between these two models before – in particular between the native and CLI enum, the native and CLI array, and the native and CLI reference class.
Under the CLI object model, individual languages are – not to put too fine a point on it – somewhat diminished – much as how in a modern country, the individual states while soverign are constrained to the laws of the central authority. For example, the CLI defines the underlying type system within which a language operates, as well as the inheritance model. As we saw in an earlier blog, the CLI does not, for example, support private inheritance, value inheritance (that is, the inheritance of implementation but not of type), or multiple inheritance (MI). While a language can choose to support these aspects of inheritance, that support requires a mapping onto the existing CLI object model because there is no direct support.
The Eiffel language under CLI, for example, choose to provide an MI mapping because it felt that (a) MI is a valuable inheritance model, and (b) its users would be dimished without its support under the CLI – that is, it would give its users a dimished programming experience under the CLI than on native platforms, and this would likely deter them from migrating to the CLI itself – or at least of migrating to the CLI while continuing to exercise their Eiffel expertise and culture.
We did not feel the same imperative as Eiffel with regards to multiple inheritance. But we did feel that imperative towards deterministic finalization of reference types declared within a local block, and so we provided a mapping of a class destructor to a IDisposable::Dispose() method, which is the CLI pattern of reclaiming resources prior to garbage collection finalization. Similarly, we did feel the imperative towards automatic memberwise copy and initialization – as supported by a copy assignment operator and copy constructor – and so we provided a mapping. (But these mappings are constrained by the underlying CLI implementation. We could not map memberwise copy into a value class because we could not guarantee that it could be carried out in all circumstances – at least that is my understanding. I haven't myself verified that but taken it on faith.)
Again, we did this because without these mappings of essential aspects of the native C++ programming experience, we believe the C++ user would have a diminished experience of programming under the CLI than on the native platform, and that this would deter them from migrating to the CLI – or at least of migrating to the CLI while continuing to exercise their C++ expertise and culture. The template mechanism is another of the essential aspects of modern C++ programming. We believe its absence would represents a significant hole in quality of programmer life when using C++/CLI. Personally, that is my deep belief.
So, with regard to parameterized types, it felt imperative that we provide some mechanism beyond what was offerred by the System::Collections namespace. The first mechanim that naturally comes to mind to the C++ programmer, of course, is templates. But what about generics? Why couldn't C++/CLI use generics for containing the CLI types, and leave templates for the non-CLI types? Why map the template mechanism into C++/CLI to support the CLI reference class, value class, interface class, delegate, and function?
The honest answer is because we were left with no choice. One of the generic talking points in presentations, specifications, and hallway whispering, is that generics, while borrowing from C++ template, "does not suffer many of the complexities" of C++ templates.
What are considered as superfluous complexities of C++ templates and were therefore eliminated from the generic mechanism – partial template specialization, the ability to inherit from the type parameter, support for non-type and template type parameters, the ability to specialize either an entire or selected members of a template, and so on – are considered by professional C++ programmers and designers of the language as essential modern programming design patterns that are fundamental to existing production code and widely-used libraries, such as the STL, LOKI, and Boost.
The real problem is that although the C++ community and language designers and implementors have deep experience with parameterized types, that experience was not tapped while the design of generics were underway.
So, we did not have any choice but to provide support of templates for CLI types and to provide an STL.NET implementation. This is great stuff if you care about C++ and want to see it succeed under .NET. Except for performance issues, C++/CLI, in my biased opinion, is shaping up as the premier C++ experience available. Personally, I'm so keen on the new language that I'm planning to reimplement my mscfront translator into the C++/CLI code from native. I'll be reporting my progress on that in quite some detail in a series of blog entries once I get the C++/CLI text I'm working on in shape.
So, that's why we have templates. Why did we also provide support for generics? Generics are deeply integrated into the CLI, and for that reason solve a number of problems left unsolved in C++ – in particular, the instantiation model. Because there is no concept of a runtime within native C++, there is no native concept of how a template is instantiated – that is, when the binding of an actual parameter to the formal parameter occurs and to what extent. The work of the ISO committee in this area has not been stellar.
Generics provides a constraint mechanism, something whose absence is keenly felt in the template mechanism. A generic type is recognized by the CIL – the intermediate language; a template class is not, and so template classes are not cross-language and, it turns out, not cross-assembly as well. That is to say, every serious CLI language has to provide generic support. And that is what we do. Perhaps if we had been participants in the design, the outcome could have been different. But that, to repeat my aunt's favorite refrain, is water under the bridge.
So, from my perspective, that is why we support both the template and generic solutions for parameterized types, and have tried to integrate them into a elegant symmetry.
 Not surprisingly, the vocabulary for the different aspects and actions on a parameterized types are quite different between generics and templates – and I am not going down that path.
 Bjarne's original Usenix template paper included a discussion of constraint syntax, which he then chose not to incorporate into the design. It's absence means that there is no way to know prior to actual instantiation whether a type is qualified to be used with a particular template class – other than examining the source code – which is not really practicable.