The Revised C++ Language Design Supporting .NET -- Part 2

The fundamental design choice in supporting the .NET reference type within C++ is to decide whether to remain within the existing language, or to extend the language, thereby breaking with the standard and opening ourselves up to potential criticism and rebuke. If one is unable to quantify measurements through which a choice can be based, the choice will remain controversial -- that is, subject to debate and criticism, and over time it is easy to loose sight of one's original resolve. Once that happens, the entire design seems indefensible, and things go from bad to worse.

     The criteria upon which to base this design choice, in my opinion, is a determination as to whether the additional language support represents a domain abstraction (think of concurrency and threads) or a paradigm shift (think of object-oriented type-subtype relationships and generics). In the original language design, support for the reference type (and .NET in general) was viewed as domain support, spoken of as the managed extensions, and so the design choice followed logically to remain within the existing language.
     Once we had committed ourselves to remain within the existing language, only three alternative approaches are really feasible -- remember, I've constrained our discussion to be that simply of how to represent a .NET reference type:
  1. Have the language support be transparent. The compiler will figure out the semantics contextually. Ambiguity results in an error, and the user will disambiguate the context through some special syntax (as an analogy, think of overload function resolution, with its hierarchy of precedence).
  2. Add support for the domain abstraction as a library (think of the standard template library as a possible model).
  3. Reuse some existing language element(s), qualifying the permissible usages and behavior based on the context of its use outlined in an accompanying specification (think of the initialization and downcast semantics of virtual base classes, or the multiple uses of the static keyword within a function, at file scope, and within a class declaration).

    Everyone's first choice is #1. "It's just like anything else in the language, only different. Just let the compiler figure this out." The big win here is that everything is transparent to users in terms of existing code. You just haul your existing application out, add an Object or two, compile it, and, ta-dah, it's done.  Wow. No muss, no fuss. Complete interoperability both in terms of types and source code. No one argues that scenario as being the ideal, much as no one argues the ideal of a perpetual motion machine. In physics, the obstacle is the second law of thermodynamics, and the existence of entropy. In a multi-paradigm programming language, the laws are considerably different, but the disintegration of the system can be equally pronounced. [I know, this is tough patch. Let me switch gears here, given that this is a blog and not a textbook, and see if I can drop into a more conversational mode.]

    In a multi-paradigm language, things work reasonably well within each paradigm, but tend to fall apart when paradigms are incorrectly mixed, leading to either the program blowing up or, even worse, completing but generating incorrect results. We run into this most commonly between support for independent object-based and polymorphic object-oriented class programming. Slicing, for example, drives every newbie C++ programming nuts:

    •     DerivedClass dc; // an object
    •     BaseClass &bc = dc; // ok: bc is really a dc
    •     BaseClass bc2 = dc; // ok: but dc has been sliced to fit into bc2

    So, the second law of language design, so to speak, is to make things that behave differently look different enough that the user will be reminded of it when he or she programs in order to avoid ... well, screwing up. It use to take half an hour of a two-hour presentation to make any dent in the C programmer's understanding of the difference between a pointer and a reference, and a great many C++ programmers still cannot clearly articulate when to use a reference declaration and when a pointer, and why.

    These confusions admittedly make programming more difficult, and there is always a significant trade-off between the simplicity of simply throwing them out, and the real-world power that their support provides. And the difference is the clarity of the design, as to whether they are usable or not. And usually the design is through analogy. When pointers to class members were introduced into the language, the member selection operators were extended ( -> to ->*, for example), and the pointer to function syntax was similarly extended ( int (*pf)() to int (X::*pf)() ). The same held true with the initialization of static class data members, and so on.

     References were necessary for the support of operator overloading (which itself is a controversial feature, of course, and one which Java chose to throw out). You could get the intuitive syntax of

    •     Matrix c = a + b;  // Matrix operator+( Matrix lhs, Matrix rhs );
    •    c = a + b + c;    

but that is hardly an efficient implementation. The C-language pointer alternative, while providing efficiency, broke apart with its non-intuitive syntax:

    •     Matrix c = &a + &b;  // Matrix operator+( const Matrix* lhs, const Matrix* rhs ); 
    •     c = &( &a + &b ) + &c;

The introduction of a reference provided the efficiency of a pointer, but the lexical simplicity of an directly accessible value type. Its declaration is analogous to the pointer, and that was easy to internalize,

    •     Matrix c = a + b;  // Matrix operator+( const Matrix& lhs, const Matrix& rhs );   

but its semantic behavior (as discussed in part 1) proved confusing to those habituated to the pointer.

    So, the question then is, how easily will be the C++ programmer, habituated to the static behavior of C++ objects, understand and correctly use the managed reference type? And, of course, what is the best design possible to aid the programmer in that effort?

    We felt that the differences between the two types were significant enough to warrant special handling, and therefore we eliminated choice #1. We stand by that choice, even in the language revision. Those that argue for it, and that includes most of us at one time or another, simply haven't sat down and worked through the problems sufficiently. It's not an accusation; it's just how things are. So, if you took the design challenge of Part 1, and came up with a transparent design, I am going to assert that it is not in our experience a workable solution, and press on.

    The second and third choices, that of resorting to either a library design, or reusing existing language elements, are both viable, and each have their strong proponents. The library solution became something of a litany within Bell Laboratories due to the easy accessibility of Stroustrup's cfront source. It was a case of, Here Comes Everybody, at one point. This person hacked on cfront to add concurrency, others hacked on cfront to add their pet domain extension, and each paraded their new Adjective-C++ language, and Stroustrup's correct response was, no, that is best handled by a library.

    So, why didn't we choose a library solution? Well, in part, it is just a feeling. Just as we felt that the differences between the two types were significant enough to warrant special handling, we felt that the similarities between the two types were as significant to warrant analogous treatment. A library type behaves in many ways as if it were a type built into the language, but it is not, really. It is not a first class citizen of the language. We felt, as best as we could, we had to make the reference type a first class citizen of the language, and therefore, we chose not to employ a library solution. This remains controversial.

    So, having discarded the transparent solution because of a feeling that the reference type and the existing type object model are too different, and having discarded the library solution because of a feeling that the reference type and the existing type object model need to be peers within the language, we are left with the problem of how to integrate the reference type into the existing language.

    If we were starting from scratch, of course, we could do anything we wished to provide a unified type system, and -- at least until we made changes to that type system -- anything we did would have the shine of a spanking brand-new widget. This is what we do in manufacturing and technology in general. We are constrained, however, and that is both a blessing and a curse. We can't throw out the existing C++ object model, so anything we do must fit into it. In the original language design, we further constrained ourselves not to introduce any new tokens; therefore, we must make use of those we already have. This doesn't give us a lot of wiggle-room.

 

    So, to cut to the chase, in the original design, given the constraints just enumerated (hopefully without too much confusion) the language designers felt that the only viable representation of the .NET reference type, was to reuse the existing pointer syntax -- references were not flexible enough since they cannot be reassigned and they are unable to refer to no object: 

    • Object * pobj = new Object; // the mother of all objects, allocated on the managed heap ...
    • string * pstr = new string; // the standard string class, allocated on the native heap ...

 

    These pointers are significantly different, of course. For example, when the Object entity addressed by pobj is moved through a compaction sweep through the managed heap, pobj is transparently updated. No such notion of object tracking exists for the relationship between pstr and the entity it addresses. The entire C++ notion of a pointer as a toggle between a machine address and an indirect object reference doesn't exist. A handle to a reference type encapsulates the actual virtual address of the object in order to facilitate the runtime garbage collector much as a private data member encapsulates the implementation of a class in order to facilitate extensibility and localization, except that the consequences of violating that encapsulation in a garbage collected environment is considerably more severe.

 

    So, while pobj look like a pointer, many common pointerish things are prohibited, such as pointer arithmetic and casts that step outside the type system. We can make the distinction more explicit if we use the fully qualified syntax of declaring and allocating a reference managed type:

    • Object __gc * pobj = __gc new Object; // ok, now this looks different ...
    • string * pstr = new string; //

     At first blush, the pointer solution seemed reasonable. After all, it seems the natural target of a new expression, and both support shallow copy. One problem is that a pointer is not a type abstraction, but a machine representation (with a tag type recommendation as to how to interpret the extent and internal organization of the memory following the address of the first byte), and this falls short of the abstraction the software runtime imposes on memory and the automation and security one can extrapolate from that. This is a historical problem between object models that represent different paradigms.

    A second problem is the [metaphor alert -- a strained metaphor is about to be attempted -- all weak-stomached readers are advised to hold on or jump to the next paragraph] necessary entropy of a closed language design which is constrained to reuse constructs that are both too similar and significantly different and result in a dissipation of the programmer's energy in the heat of a desert mirage. [metaphor alert end].

    Reusing the pointer syntax turned out to be a source of cognitive noise for the programmer: you have to make too many distinctions between the native and managed pointers, and this interferes with the flow of coding, which is best managed at a higher level of abstraction. That is, there are times when we need to, as system programmers, go down a notch to squeeze some necessary performance, but we don't want to dwell at that level.  

    The success of the original language design is that it supported the unmodified recompilation of existing C++ programs, and provided support for the Wrapper pattern of publishing an existing interface into the new .NET environment with a trivial amount of work. This could then add additional functionality in the .NET environment, and, as time and experience dictated, one could port this or that portion of the existing application directly into .NET. This is a magnificent achievement for C++ programmers with an existing code base and an existing base of expertise. There is nothing that we need to be ashamed of in this.

    However, there are significant weaknesses in the actual syntax and vision of the original language design. This is not due to inadequacies of the designers, but in the conservative nature of their fundamental design choice to remain within the existing language. And that resulted from a misapprehension that the .NET support represented not a domain abstraction but an evolutionary programming paradigm that required a language extension similar to that introduced by Stroustrup to support Object-Oriented and generic programming. This is what the revised language design represents, and why it is both necessary and reasonable despite the embarrassment one feels sometimes when one corrects some mistakes make in public.

I will be focusing on the differences between the original and revised language design in the coming entries, both detailing the differences and trying to motivate why the differences are there. [And I will try to minimize the metaphor alerts along the way.]  This is an exciting time not just for C++, but for the C++ programmer. 

See ya next time.

 

 

disclaimer: This posting is provided "AS IS" with no warranties, and confers no rights.