From mboxrd@z Thu Jan 1 00:00:00 1970 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on polar.synack.me X-Spam-Level: X-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00 autolearn=ham autolearn_force=no version=3.4.4 X-Google-Language: ENGLISH,ASCII-7-bit X-Google-Thread: 103376,f835758b70579867,start X-Google-Attributes: gid103376,public X-Google-ArrivalTime: 1995-02-09 12:56:32 PST Path: nntp.gmd.de!newsserver.jvnc.net!darwin.sura.net!pirates!gatech!howland.reston.ans.net!news.sprintlink.net!uunet!newsserver.tasc.com!newsserver.read.tasc.com!bscrawford.read.tasc.com!user From: bscrawford@tasc.com (Bard Crawford) Newsgroups: comp.lang.ada Subject: Taft answers C++ person Followup-To: comp.lang.ada Date: 9 Feb 1995 20:56:32 GMT Organization: TASC Distribution: world Message-ID: NNTP-Posting-Host: bscrawford.read.tasc.com Date: 1995-02-09T20:56:32+00:00 List-Id: 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 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 > -Tucker Taft