comp.lang.ada
 help / color / mirror / Atom feed
* Taft answers C++ person
@ 1995-02-09 20:56 Bard Crawford
  0 siblings, 0 replies; only message in thread
From: Bard Crawford @ 1995-02-09 20:56 UTC (permalink / raw)


Tucker Taft recently answered my private message containing 
some questions from an associate with a C++ background.  I am 
posting this because I think Tucker's answers may be of general 
interest.  

Bard Crawford
-----------------------------------------------------
Message to Taft and his Answers
-----------------------------------------------------
> I recently wrote an internal memo here at TASC, with a 4-page
> attachment containg two excerpts from Magnus Kempe's FAQ for  
> Ada Programmers.  The excerpts contained:
>  
>   Your answer to Question 5.1:
>   Why does Ada have "tagged types" instead of classes?
>  
>   Robert Dewar's answer to Question 4.2:
>   Ada seems large and complex, why is it this way?
>  
> I thought both answers were very interesting and very  
> well written. (Translation: I could understand them.)
>  
> I received the following reply from Steve Baumgartner, who  
> has taught an internal course here on OOD and C++.  I don't  
> have the necessary background to give him proper answers.  
> I am hoping you are willing to take a shot.  

I will try.

> Thanks,
>  
> Bard Crawford
> TASC
> bscrawford@tasc.com
> ----------------------------------------
> Beginning of Baumgartner's Questions
> ----------------------------------------
> Thanks for the article.  There were a couple of items in the  
> discussion that for lack of Ada expertise I am not sure I  
> followed - perhaps you can clarify?  As usual, sorry for the  
> length of this missile - intended for mutual enlightenment,  
> not as a flame :-).
>  
> 1.  In Taft's discussion he presents a Print_In_Bold procedure  
> as an example.  What does the example actually mean?
>  
>         - I assume that T'Class is Ada for "any type from  
> class T"?

Yes, that's basically right.  T'Class is called a "class-wide" type, as 
distinguished from a "specific" type like T.
The only time you get "dispatching" (aka run-time polymorphism, 
virtual function call, etc.) is when the actual "controlling"  operand 
in a call on a primitive of some tagged type
T is of the class-wide type T'Class.

>         - Is X passed by value or by reference based on this  
> declaration?  

Operands that are of a tagged type are always passed by reference.   
What C++ calls "slicing" (or implicit truncation) never occurs on 
parameter passing.

> ... If by value, I don't understand the point of  
> making the local Copy_of_X since there was already a copy  
> made to pass the argument?  

As mentioned above, operands of a tagged type are always passed by 
reference.  Furthermore, an IN parameter may not be written on in 
Ada; it is read-only (which is one reason why in general the compiler 
is allowed to use by-reference for IN parameters of an array or 
record type).

> ... This might depend on unstated  
> semantics of T - for instance, it might have done a "shallow  
> copy" for the argument passing, but we need a "deep copy" to  
> prevent Make_Bold from altering the original.

This was not the reason.  If deep copying is required, then the type 
or its "deep" components should have an "Adjust" procedure (which is 
a post-copy fixup routine to do deep copying, reference count 
bumping, etc, as necessary).

>         - I assume that Make_Bold and Print have T'Class  
> parameters also?  Presumably the compiler saw declarations  
> to this effect in details omitted for brevity?

More likely they have parameters of type T, but I don't have the 
examples in front of me to check.  

In general the "primitive" operations of a  type T are those 
subprograms declared in the same package as T that have T as a 
parameter or result type.
All the primitive operations of a tagged type T are called
"dispatching operations" (analagous to C++ "virtual functions" or
Smalltalk "methods").  When they are passed an operand of a class-
wide type T'Class, they automatically "dispatch" to the "appropriate" 
implementation of the operation.  It might be that for T, or it might 
be that for some type T2 derived from T that overrode the 
implementation of the operation.
This is determined by the "run-time tag" of the operand of type 
T'Class.  

If a dispatching operation is called with operands of type T instead 
of T'Class, then it is treated like a "normal" subprogram and the call 
is statically bound to the implementation associated with type T.  
To reiterate: when a dispatching operation is called with class-wide 
operands, it dispatches to the appropriate body, based on the run-
time tag of the operand(s).  When it is called with a "specific" 
operand, it is a statically bound call to the directly associated body.

>         - Does the compiler detect usages with specific  
> types and generate distinct instances of Print_In_Bold as  
> needed, or is there a single instance with some sort of  
> run-time type magic which decides how to create Copy_Of_X  
> and how to invoke Make_Bold and Print based on the actual  
> type of X?  Given that Ada is a "strongly typed" language,
> I would expect the former, but I don't know.  There are  
> implications about memory size and layout which make the  
> run-time approach hard to implement.

There is only one copy of a given body (in the absence of generic 
instantiations).  Run-time dispatching is used based on run-time 
type tags.  The expected implementation is that every value of a 
tagged type will have an extra implicit component called the "tag" 
which is initialized to point to a run-time type descriptor, 
essentially equivalent to the virtual-function table used by C++.  It 
also has a little bit of run-time type information so that 
membership tests (e.g. if X in T2'Class then ...) and tag checks  (on 
"narrowing" conversions such as "T2(X)") can be performed.

> For not quite understanding the above, I am not certain  
> whether the following C++ is "close enough" (certainly it  
> equals the Ada in terseness and simplicity of syntax):
>  
> template <class T> void Print_In_Bold(T X)
> {
>         T Copy_of_X(x);
>         Make_Bold(Copy_of_X);
>         Print(Copy_of_X);
> }
>  
> This isn't strictly equivalent to the semantics of the Ada:  
> this is a generic function with no explicit restriction on T.   
> There is an implicit restriction that there are declarations  
> of a copy ctor,Make_Bold, and Print for the actual T, just  
> as I assume Ada would need.

For what it's worth, Ada generics require that any such  assumptions 
about the formal type T be explicit in the declaration of the generic.  
But in any case, this example was not using generics.  The ability to 
make a copy of an object whose size and tag are not known at 
compile-time is a basic feature of Ada 95, and is actually not much 
harder than what Ada 83 already provides in terms of making copies 
of arrays whose bounds are not known at compile-time.
One difference is that the copy can involve calling one or more user-
written "Adjust" procedures if any deep copying or ref-count 
adjusting is required.

> I am groping for understanding of what is gained by  
> hard-wiring a restriction to an Ada class rather than  
> accepting any type with the required declarations (I can't  
> accept the idea that types outside class T are prohibited  
> from having compatible declarations)?  Why would you want  
> to reject something which will compile and run; am I missing  
> some way the code would break without the restriction?
> Perhaps there's a better example...?

This example was presumably not illustrating generics/templates, 
but rather tagged types and dynamic binding, where the parameter  
must be in a specific class since it is relying on the equivalent of a 
virtual-function table to reach the operations like Print  and 
Make_Bold.

If you wanted to do something similar with Ada generics, you don't 
need to specify that the type be in a specific class.  Any  
(nonlimited) type is permitted, and you instead specify what 
particular set of operations are required (they can have any names, 
though the use of the "is <>" notation means that by default they are 
presumed to be called "Print" and "Make_Bold."

For example:

    generic
       type T is private;
       with procedure Print(X : T) is <>;
       with procedure Make_Bold(X : in out T) is <>;
    procedure Print_In_Bold(X : T);
    procedure Print_In_Bold(X : T) is
        Copy_Of_X : T := X;
    begin
	Make_Bold(Copy_Of_X);
	Print(Copy_Of_X);
    end Print_In_Bold;
>  
> 2.  I had trouble understanding Taft's discussion of Ada  
> "access" types.
>  
>         - I assume this is the Ada analog to a pointer or  
> reference in C++?  Taft seems to say so.

Yes, that's right.

>         - When you have an object itself (as opposed to an  
> access), does Ada dispatch dynamically or statically?  Can  
> you control this explicitly or is it fixed by the language  
> definition (per C++)?  Has Taft thrown a red herring into  
> the pointer soup by accusing C++ of not doing something  
> that Ada doesn't do either?

If the "designated" type (target type) of an access type is  class-
wide (i.e. it is an access-to-T'Class type), then you  get dynamic 
dispatch.  If the designated type is specific
(i.e. it is an access-to-T type), then you get static binding.
In general, static vs. dynamic binding is determined by the *types* 
of the operands, not the parameter passing mode, or pointer-vs.-
value.

>         - Do I understand correctly that the point is to  
> select between dynamic and static binding based on the type  
> of access employed?  As a broad issue, programming this way  
> seems to me to muddy the semantics of the operation - if  
> the derived type provided a specialized override, why do  
> you think you want to veto it and revert to the base?  

Normally you would never do anything, and you would get the "right" 
thing.  However, if you want to override the type-based choice, then 
an explicit conversion can be used to do so.  This is analagous to the 
:: operator in C++, and the use of "self" vs. "super" in Smalltalk.

> Stroustrup explicitly denigrates this sort of coding as
> "difficult to maintain" in his discussion of the C++  
> mechanism for explicit defeat of dynamic dispatch  
> (ARM 10.2, p. 210), so there may be some element of  
> academic/philosophical disagreement here.

The "software engineering" issue is that if an operation uses  
dynamic binding internally, then its external behavior depends not  
just on the "values" of the operands, but also on the run-time
"tag" of the operands.  This means that the routine cannot be reused 
as a "black box" by a descendant of the original type.
Since the use of dynamic binding is externally visible, we felt it 
was important that the default not be dynamic binding when the 
operand is of a specific type; if dynamic binding is used,  the 
programmer should be aware of it, by explicitly converting the 
operand to a class-wide type.

It was interesting to attend a panel at OOPSLA a few years ago, 
where various presenters were discussing maintenance issues with 
large class libraries.  There was a lot of discussion of the issue of 
"self-dependencies" or "hidden dependencies" in  the code for 
methods/virtual functions.  The net effect was that due to the 
default use of dynamic binding, it was very difficult to upgrade a 
class library, because clients had unknown amounts of dependence on 
the particular uses of dynamic binding within various methods in 
calling other methods.  With Ada 95, such dynamic binding in calls 
from one method to another would be more visible, and ideally, more 
likely to be intentional, and more likely to be documented.

> The C++ equivalent is based on name scoping the method at the  
> invocation, rather than by having a different kind of pointer:
>  
>         class Base {virtual blah();};
>         class Der: public Base {virtual blah();};
>         Der d;
>         Base *p = &d;
>         p->blah();          // fires Der's version dynamically
>         p->Base::blah();    // fires Base's version statically
>  
> The comparison here is a bit unusual for C++ vs Ada: the C++  
> syntax is more verbose than the Ada since you would need to  
> repeat the scoping qualifier at each invocation rather than  
> having it implicit based on type ;-)!  

Agreed this is roughly the equivalent.  The question is which is the 
default.  For calls from one primitive operation to another, the 
default is static binding in Ada 95, and dynamic binding in C++.  Our 
view is that the Ada 95 default makes it easier to maintain class 
libraries, since the choice to use dynamic binding is more explicit.

> 3.  Amused observation regarding Dewar's discussion of Ada's  
> complexity: I read nearly identical thoughts recently from  
> one of the ANSI C++ committee members - stuff about how the  
> language definition was balanced between academic  
> finickiness (??) and practical programming.  The C++er's  
> (who, I agree, are at peril of including the kitchen sink)  
> would generally argue that Ada bent too far toward the  
> academic side; that it became complex by incorporating  
> mandatory syntax for distinctions which the average programmer  
> doesn't understand (as opposed to C++, which added optional  
> syntax to support operations which nobody understands ;-) !).

Hmmm...  I guess no comment is necessary.

> Steve Baumgartner  
> <slbaumgartner@tasc.com - no matter what it says above>

-Tucker Taft



^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~1995-02-09 20:56 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
1995-02-09 20:56 Taft answers C++ person Bard Crawford

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox