The Revised C++ Language Design - Part 2

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).  

  
  1. Add
    support for the domain abstraction as a library (think of the standard template library
    as a possible model).

  2. 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; //

"urn:schemas-microsoft-com:office:powerpoint" />

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.

 

 

prefix = o ns = "urn:schemas-microsoft-com:office:office" />

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