* Merits of re-dispatching [LONG] @ 2002-02-07 10:26 Dmitry A. Kazakov 2002-02-07 15:03 ` Hyman Rosen ` (4 more replies) 0 siblings, 5 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-07 10:26 UTC (permalink / raw) Hi! C++ and Ada 95 support re-dispatching, which I think is inherently unsafe. Consider the following: type A is new Ada.Finalization.Limited_Controlled with ... procedure Foo (Object : A); procedure Finalize (Object : in out A) ; Now let AA derived from A override Foo and Finalize: type AA is new A with record ... -- Some components, which may use components of A end record; procedure Foo (Object : AA) ; procedure Finalize (Object : in out AA) is begin ... -- Finalize components, maybe accessing components of A Finalize (A (Object)); -- Finalize parent end Finalize; Consider the following implementation of A.Finalize: procedure Finalize (Object : in out A) is begin Foo (A'Class (Object)); -- Re-dispatch to the child's Foo end Finalize; When A is finalized as a part of AA, it re-dispatches to AA.Foo, which may access already finalized parts of AA! A user of A has no way to learn the problem from the specification of A. He must look into the implementation of A.Finalize. The same is valid for Initialize as well. Actually it is a common error among fresh baked C++ programmers to call virtual functions (dispatch) from constructors. Though merits of re-dispatching seem to me suspicious, it is easy to implement it without casting. Let we really want object's IDENTITY, i.e. objects which always "know" their actual type. Ada 95 perfectly supports this without any casting! package Self_Identifying is type A is new Ada.Finalization.Limited_Controlled with private; type A_Ptr is access all A'Class; procedure Finalize (Object : in out A); procedure Foo (Object : in out A); private type A is new Ada.Finalization.Limited_Controlled with record Self : A_Ptr := A'Unchecked_Access; -- Class wide self-reference end record; end Self_Identifying; The implementation of Finalize: procedure Finalize (Object : in out A) is begin Foo (Object.Self.all); -- Dispatch to child's Foo end Finalize; Now, if type AA is new A with null record; procedure Foo (Object : in out AA); Then an object of AA will dispatch to AA.Foo during finalization exactly like in the first example. The difference is that self-identification can be explicitly exposed and documented: function Identity (Object : A['Class]) return A_Ptr is begin return Object.Self; end Identity; With re-dispatching the contract of Finalize is not clear. According to its specification it receives an argument of type A. Thus there should be no legal way for Finalize to discover that actually an instance of AA was passed. I.e. Foo (A'Class (Object)) should always statically dispatch to A.Foo. Yet it dispatches to AA.Foo. This is a violation of substitutability. [Not that I hold LSP for a sacral cow, but there is no reason to encourage its violation.] I think that there should be no exceptions from the rule "Want dispatching in a subprogram? Make the argument class wide." What is worse is that implementation of re-dispatching requires that the type tag be accessible for not only class wide objects but also for specific ones. Thus tagged types are reference types, type tag shall be a part of a tagged object. As a consequence elementary types cannot be tagged and so they are excluded from OOP. Maybe I have missed something, but I see no other reason why not to allow for all types inheritance, dispatching subroutines and class wide objects. Methodically re-dispatching breaks segregation of class wide and specific types returning us to the languages like C++, where there is no difference between them. So Ada lingers somewhere in-between. Is there examples where re-dispatching is really unavoidable? Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-07 10:26 Merits of re-dispatching [LONG] Dmitry A. Kazakov @ 2002-02-07 15:03 ` Hyman Rosen 2002-02-08 1:29 ` Matthew Heaney 2002-02-07 23:40 ` Nick Roberts ` (3 subsequent siblings) 4 siblings, 1 reply; 36+ messages in thread From: Hyman Rosen @ 2002-02-07 15:03 UTC (permalink / raw) "Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message news:3c62524f.93369796@News.CIS.DFN.DE... > C++ and Ada 95 support re-dispatching, which I think is inherently unsafe. I don't know about Ada, but there is nothing unsafe about C++'s "redispatching". > Consider the following: > > type A is new Ada.Finalization.Limited_Controlled with ... > procedure Foo (Object : A); > procedure Finalize (Object : in out A) ; > > Now let AA derived from A override Foo and Finalize: > > type AA is new A with record > ... -- Some components, which may use components of A > end record; > procedure Foo (Object : AA) ; > procedure Finalize (Object : in out AA) is > begin > ... -- Finalize components, maybe accessing components of A > Finalize (A (Object)); -- Finalize parent > end Finalize; In C++, Foo(Object:AA) is *not* an overrider of Foo(Object:A), just another function that happens to share the same name. If you want to override Foo in AA, the parameter type must remain A. > Consider the following implementation of A.Finalize: > > procedure Finalize (Object : in out A) is > begin > Foo (A'Class (Object)); -- Re-dispatch to the child's Foo > end Finalize; Again, this is false for C++. In a C++ constructor or destructor, the type of the object is the type of the class of which the constructor or destructor is a member. So in the C++ equivalent of Finalize(A), the call to Foo(A'Class) would call Foo(A), not Foo(AA), because in the destructor of A, the object is an A, not an AA, even when we are destructing the A component of an AA. This is good, since, as you point out, the AA part is no longer there! > Actually it is a common error among fresh baked C++ programmers > to call virtual functions (dispatch) from constructors. True, but the error is that (from their point of view) the wrong function gets called, not that the right function gets called on destructed pieces of objects. I guess this is another entry for my "why C++ is better than Ada" file :-) (Or maybe it's the first? :-) ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-07 15:03 ` Hyman Rosen @ 2002-02-08 1:29 ` Matthew Heaney 2002-02-08 9:16 ` Dmitry A. Kazakov 0 siblings, 1 reply; 36+ messages in thread From: Matthew Heaney @ 2002-02-08 1:29 UTC (permalink / raw) "Hyman Rosen" <hyrosen@mail.com> wrote in message news:1013094178.985786@master.nyc.kbcfp.com... > Again, this is false for C++. In a C++ constructor or destructor, the > type of the object is the type of the class of which the constructor or > destructor is a member. So in the C++ equivalent of Finalize(A), the > call to Foo(A'Class) would call Foo(A), not Foo(AA), because in > the destructor of A, the object is an A, not an AA, even when we > are destructing the A component of an AA. This is good, since, as > you point out, the AA part is no longer there! Yes, that's true, but it's easy to think that the virtual member function call will dispatch, because that's how it works for every other virtual member function: class B { public: virtual ~B(); virtual void f(); virtual void g(); }; void B::f() { g(); //dispatches } B::~B() { g(); //does not dispatch } You have to learn that the call to g() doesn't dispatch in the dtor. This behavior is certainly not immediately obvious, especially for those new to the language. You seemed to have missed the point about this error-prone feature of C++: a naive programmer is going to depend on dispatching to occur. He has to learn --the hard way-- that the dtor is special. > True, but the error is that (from their point of view) the wrong function > gets called, not that the right function gets called on destructed pieces > of objects. But the point is that it is an error, that is very easy to make. > I guess this is another entry for my "why C++ is better than Ada" file :-) Again, you seemed to have missed the point. The scenario Dmitry described cannot happed by accident in Ada95: the programmer must have taken special action for it to occur. This is not the case in C++. (Note that I like C++, and I use it every day. But by any measure Ada95 is a much safer language than C++.) ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 1:29 ` Matthew Heaney @ 2002-02-08 9:16 ` Dmitry A. Kazakov 2002-02-08 18:30 ` Hyman Rosen 2002-02-08 23:51 ` Merits of re-dispatching [LONG] Matthew Heaney 0 siblings, 2 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-08 9:16 UTC (permalink / raw) On Thu, 7 Feb 2002 20:29:42 -0500, "Matthew Heaney" <mheaney@on2.com> wrote: >"Hyman Rosen" <hyrosen@mail.com> wrote in message >news:1013094178.985786@master.nyc.kbcfp.com... >> Again, this is false for C++. In a C++ constructor or destructor, the >> type of the object is the type of the class of which the constructor or >> destructor is a member. So in the C++ equivalent of Finalize(A), the >> call to Foo(A'Class) would call Foo(A), not Foo(AA), because in >> the destructor of A, the object is an A, not an AA, even when we >> are destructing the A component of an AA. This is good, since, as >> you point out, the AA part is no longer there! > >Yes, that's true, but it's easy to think that the virtual member function >call will dispatch, because that's how it works for every other virtual >member function: > >class B >{ >public: > virtual ~B(); > virtual void f(); > virtual void g(); >}; > >void B::f() >{ > g(); //dispatches >} > >B::~B() >{ > g(); //does not dispatch >} > >You have to learn that the call to g() doesn't dispatch in the dtor. This >behavior is certainly not immediately obvious, especially for those new to >the language. > >You seemed to have missed the point about this error-prone feature of C++: a >naive programmer is going to depend on dispatching to occur. He has to >learn --the hard way-- that the dtor is special. Yes, but there are two things here. First C++ does not make any difference between class wide and specific objects. The same type B is a class wide in B::f () and specific in B::~B (). So the difference in how B::f and B::~B are dealing with calls to g(). This is IMO bad. Second. The behaviour of B::~B is safe. One should never expect dispatching in a specific subroutine like destructor. My point is that the language should prohibit dispatching from specific subroutines. Therefore in my opinion the behaviour of B::f is unsafe. It is declared as virtual (dispatching) which means that inside B::f the type is already *known*. Thus there is no need in [re-]dispatching. A call to g() can be statically resolved. The reason why C++ dispatches in B::f (), is that otherwise it would impossible to have class wide subroutines [see the point 1]. In contrary to this Ada 95 does have class wide routines, so my question, why [explicit] re-dispatching is supported in Ada 95? Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 9:16 ` Dmitry A. Kazakov @ 2002-02-08 18:30 ` Hyman Rosen 2002-02-09 0:10 ` Matthew Heaney 2002-02-12 8:32 ` Dmitry A. Kazakov 2002-02-08 23:51 ` Merits of re-dispatching [LONG] Matthew Heaney 1 sibling, 2 replies; 36+ messages in thread From: Hyman Rosen @ 2002-02-08 18:30 UTC (permalink / raw) "Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message news:3c6392e8.2400843@News.CIS.DFN.DE... > First C++ does not make any difference between class wide and specific > objects. The same type B is a class wide in B::f () and specific in > B::~B (). So the difference in how B::f and B::~B are dealing with > calls to g(). This is IMO bad. This is an incorrect view of what is happening. The type is class-wide in both B::f and B::~B. Calls to B::g are dispatching in both. It's just that while B::~B is running, the actual type of the object is B, so any dispatching happens according to B's virtual functions, even if the B part happened to be part of a derived class. Here's an example: struct A { virtual void foo() { cout << "A::foo\n"; }void call_foo() { foo(); } virtual ~A() { call_foo(); } }; struct B : A { void foo() { cout << "B::foo\n"; } ~B() { call_foo(); } } struct C : B { void foo() { cout << "C::foo\n"; } ~C() { call_foo(); } }; When a C object is destructed, it will print "C::foo"., "B::foo", "A::foo". > type is already *known*. Thus there is no need in [re-]dispatching. A > call to g() can be statically resolved. As in my example, not if the object is passed to some other routine which makes the dispatching call. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 18:30 ` Hyman Rosen @ 2002-02-09 0:10 ` Matthew Heaney 2002-02-12 8:32 ` Dmitry A. Kazakov 1 sibling, 0 replies; 36+ messages in thread From: Matthew Heaney @ 2002-02-09 0:10 UTC (permalink / raw) "Hyman Rosen" <hyrosen@mail.com> wrote in message news:1013192956.289787@master.nyc.kbcfp.com... > This is an incorrect view of what is happening. Yes, we all know this is an incorrect view. But the programmer has to learn that this view is not the correct view. He learns it because something doesn't work the way he expects. You learn by forming a mental model of how something works, and then testing that model via experience. When something behaves in a way that is inconsistent with your model, then your model has been falsified, and you have to change it so that there are no more surprises (you can make accurate predications). The issue in C++ is that when programmers first learn that language, they very naturally form a mental model that says, "when I call a virtual member function (through a ref or ptr, which may be implicit), the call is dynamically bound." This model is very successful at making accurate predications. However, in a dtor, the language behaves in a way that is inconsistant with this otherwise good model of reality. Once you learn why, then the fact that it does behave that way makes sense, but it does surprise programmers. The whole issue of "affordance" in user interface design concerns exactly this issue. When you use a tool, you form a mental model what to do to make a certain thing happen. But when you use the tool, and it fails to do what you expect, then you are naturally surprised. Good user interfaces are designed to that the user is never surprised. Ada95 has a flaw here: predefined equality "reemerges" when the type is passed as a generic formal type, and when it's the component of a composite type. If you've overriden predefined equality for a type, this is *never* what you expect. (You naturally assume that your overridden operator is that one that will be called, but it isn't, and this is a surprise. From a human engineering point of view, this is a flaw.) ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 18:30 ` Hyman Rosen 2002-02-09 0:10 ` Matthew Heaney @ 2002-02-12 8:32 ` Dmitry A. Kazakov 2002-02-12 21:37 ` Hyman Rosen 2002-02-13 19:58 ` Dave Harris 1 sibling, 2 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-12 8:32 UTC (permalink / raw) On Fri, 8 Feb 2002 13:30:04 -0500, "Hyman Rosen" <hyrosen@mail.com> wrote: >"Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message news:3c6392e8.2400843@News.CIS.DFN.DE... >> First C++ does not make any difference between class wide and specific >> objects. The same type B is a class wide in B::f () and specific in >> B::~B (). So the difference in how B::f and B::~B are dealing with >> calls to g(). This is IMO bad. > >This is an incorrect view of what is happening. The type is class-wide >in both B::f and B::~B. Calls to B::g are dispatching in both. It's just >that while B::~B is running, the actual type of the object is B, so any >dispatching happens according to B's virtual functions, even if the B >part happened to be part of a derived class. That is one of several possible interpretation of what happens in C++ using Ada terms. However, I prefer mine, because it is consistent with the fact that the type tag [= vtab] is constant, thus the actual specific type is also constant. Like in Ada it is only a view conversion. >struct A { virtual void foo() { cout << "A::foo\n"; }void call_foo() { foo(); } virtual ~A() { call_foo(); } }; >struct B : A { void foo() { cout << "B::foo\n"; } ~B() { call_foo(); } } >struct C : B { void foo() { cout << "C::foo\n"; } ~C() { call_foo(); } }; > >When a C object is destructed, it will print "C::foo"., "B::foo", "A::foo". > >> type is already *known*. Thus there is no need in [re-]dispatching. A >> call to g() can be statically resolved. > >As in my example, not if the object is passed to some other routine which >makes the dispatching call. When you pass it to some other routine an implicit specific->class wide conversion happens, so the routine can dispatch again. An Ada 95 equivalent would be: procedure Y (A : in out Object'Class); procedure X (A : in out Object) is begin Y (A'Class (A)); end X; Exactly this behaviour is IMO unsafe. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-12 8:32 ` Dmitry A. Kazakov @ 2002-02-12 21:37 ` Hyman Rosen 2002-02-13 9:29 ` Dmitry A. Kazakov 2002-02-13 19:58 ` Dave Harris 1 sibling, 1 reply; 36+ messages in thread From: Hyman Rosen @ 2002-02-12 21:37 UTC (permalink / raw) Dmitry A. Kazakov wrote: > That is one of several possible interpretation of what happens in C++ > using Ada terms. However, I prefer mine, because it is consistent with > the fact that the type tag [= vtab] is constant, thus the actual > specific type is also constant. Like in Ada it is only a view > conversion. It is not "one of several possible interpretation", it is what actually happens in C++! For implementations which use vtables, the compiler generates code to change the vtable pointer of the object as it runs through its chains of destructors. (Presumably the compiler may detect cases where it doesn't need to do this, but this is what happens in principle.) ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-12 21:37 ` Hyman Rosen @ 2002-02-13 9:29 ` Dmitry A. Kazakov 2002-02-13 14:32 ` Hyman Rosen 0 siblings, 1 reply; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-13 9:29 UTC (permalink / raw) On Tue, 12 Feb 2002 16:37:50 -0500, Hyman Rosen <hyrosen@mail.com> wrote: >Dmitry A. Kazakov wrote: >> That is one of several possible interpretation of what happens in C++ >> using Ada terms. However, I prefer mine, because it is consistent with >> the fact that the type tag [= vtab] is constant, thus the actual >> specific type is also constant. Like in Ada it is only a view >> conversion. > >It is not "one of several possible interpretation", it is what actually >happens in C++! For implementations which use vtables, the compiler >generates code to change the vtable pointer of the object as it runs >through its chains of destructors. (Presumably the compiler may detect >cases where it doesn't need to do this, but this is what happens in >principle.) Does the C++ standard require this? I do not know, but if yes, then C++ is even "better" than I used to think. (:-)) I remember one C run-time library implementation, which had printf modifying the format string and restoring it before return. It was a GREAT idea. Unfortunately, the compiler allocated string literals in the read-only memory. (:-)) Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-13 9:29 ` Dmitry A. Kazakov @ 2002-02-13 14:32 ` Hyman Rosen 0 siblings, 0 replies; 36+ messages in thread From: Hyman Rosen @ 2002-02-13 14:32 UTC (permalink / raw) Dmitry A. Kazakov wrote: > Does the C++ standard require this? I do not know, but if yes, then > C++ is even "better" than I used to think. (:-)) The standard doesn't specify an implementation, of course, but yes, it requires this behavior. Under the covers, this, combined with multiple inheritance and a desire for efficiency in the most common cases, makes the implementation of constructors and destructors potentially very complicated. Fortunately, this just has to be figured out once by the compiler vendor. To the user, everything looks and acts just like they expect (assuming their expectations are correct :-). The gcc team has developed an ABI for C++ that discusses these issues. See <http://www.codesourcery.com/cxx-abi/abi.html>. It has some relevance to Ada since GNAT's tagged types are compatible with equivalent C++ classes. > I remember one C run-time library implementation, which had printf > modifying the format string and restoring it before return. It was a > GREAT idea. Unfortunately, the compiler allocated string literals in > the read-only memory. (:-)) C++, like Ada, integrates its libraries into the language specification, so vendors are allowed to use whatever system knowledge or magic they want to implement them. It helps if the library team and compiler team talk to each other :-) ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-12 8:32 ` Dmitry A. Kazakov 2002-02-12 21:37 ` Hyman Rosen @ 2002-02-13 19:58 ` Dave Harris 2002-02-14 15:06 ` Dmitry A. Kazakov 1 sibling, 1 reply; 36+ messages in thread From: Dave Harris @ 2002-02-13 19:58 UTC (permalink / raw) dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote (abridged): > That is one of several possible interpretation of what happens in C++ > using Ada terms. However, I prefer mine, because it is consistent with > the fact that the type tag [= vtab] is constant, thus the actual > specific type is also constant. > [...] > When you pass it to some other routine an implicit specific->class > wide conversion happens, so the routine can dispatch again. This doesn't work. If you look at the 3 calls to call_foo() in Hyman Rosen's example, you'll see it dispatches to 3 different places. Therefore we cannot view the object has having a constant type with 2 kinds of dispatching. A specific->class-wide conversion would have it printing C::foo 2 or 3 times. Your view doesn't match the visible behaviour. If you think in terms of a conversion, the different calls to call_foo() must convert to different types. It's not enough to switch the vtable on and off, you need different conceptual vtables. Given this, its simpler to think of the conversion as happening as soon as the constructor is entered, rather than when the constructor calls out. > An Ada 95 equivalent would be: Your example only shows 2 types, so doesn't capture the richness of behaviour of Hyman's example which has 3. I suspect you have misunderstood it. Dave Harris, Nottingham, UK | "Weave a circle round him thrice, brangdon@cix.co.uk | And close your eyes with holy dread, | For he on honey dew hath fed http://www.bhresearch.co.uk/ | And drunk the milk of Paradise." ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-13 19:58 ` Dave Harris @ 2002-02-14 15:06 ` Dmitry A. Kazakov 2002-02-16 12:10 ` Dave Harris 0 siblings, 1 reply; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-14 15:06 UTC (permalink / raw) On Wed, 13 Feb 2002 19:58 +0000 (GMT Standard Time), brangdon@cix.co.uk (Dave Harris) wrote: >dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote (abridged): >> That is one of several possible interpretation of what happens in C++ >> using Ada terms. However, I prefer mine, because it is consistent with >> the fact that the type tag [= vtab] is constant, thus the actual >> specific type is also constant. >> [...] >> When you pass it to some other routine an implicit specific->class >> wide conversion happens, so the routine can dispatch again. > >This doesn't work. If you look at the 3 calls to call_foo() in Hyman >Rosen's example, you'll see it dispatches to 3 different places. Therefore >we cannot view the object has having a constant type with 2 kinds of >dispatching. A specific->class-wide conversion would have it printing >C::foo 2 or 3 times. Your view doesn't match the visible behaviour. > >If you think in terms of a conversion, the different calls to call_foo() >must convert to different types. It's not enough to switch the vtable on >and off, you need different conceptual vtables. Yes. C has a tag, B in C has another and A in B in C has the third. This is why I do not like embedded tags. >Given this, its simpler to think of the conversion as happening as soon as >the constructor is entered, rather than when the constructor calls out. Surely. There is a type conversion, but it is an "in-place" conversion. It changes neither the object C nor its type. It only shifts the reference to C.B. >> An Ada 95 equivalent would be: > >Your example only shows 2 types, so doesn't capture the richness of >behaviour of Hyman's example which has 3. I suspect you have misunderstood >it. That there are three types changes nothing. It works pretty well in Ada 95: type A is ... procedure Foo (Object : A) is begin Put_Line ("A.Foo"); end Foo; procedure Finalize (Object : in out A) is begin Foo (Object); -- No dispatch here, A.Foo is called end Finalize; -------------------- type B is new A ... procedure Foo (Object : B) is begin Put_Line ("B.Foo"); end Foo; procedure Finalize (Object : in out B) is begin Foo (Object); -- No dispatch here, B.Foo is called Finalize (A (Object)); -- C++ adds this automatically end Finalize; --------------------------- type C is new B... procedure Foo (Object : C) is begin Put_Line ("C.Foo"); end Foo; procedure Finalize (Object : in out C) is begin Foo (Object); -- No dispatch here, C.Foo is called Finalize (B (Object)); -- C++ adds this automatically end Finalize; When C is finalized, then C.Foo, B.Foo, A.Foo will be printed. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-14 15:06 ` Dmitry A. Kazakov @ 2002-02-16 12:10 ` Dave Harris 2002-02-18 8:57 ` Dmitry A. Kazakov 0 siblings, 1 reply; 36+ messages in thread From: Dave Harris @ 2002-02-16 12:10 UTC (permalink / raw) dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote (abridged): > That there are three types changes nothing. It works pretty well in > Ada 95: That still doesn't capture the full complexity, because although you have Foo you don't have a Call_Foo. The call to Foo from within Call_Foo must be dispatching, because it must dispatch to different places. Given that it can do this, it's simplest to say the call to Foo from within the destructor is dispatching too, in the same way. So there is no non-dispatching call in the C++. I don't know Ada, but I think it would look like: type A is ... procedure Foo (Object : A) is begin Put_Line ("A.Foo"); end Foo; procedure Call_Foo (Object : A) is begin Foo( Object ); -- Dispatching, A.Foo or B.Foo called. end Foo; procedure Finalize (Object : in out A) is begin Call_Foo (Object); Foo (Object); end Finalize; type B is new A ... procedure Foo (Object : B) is begin Put_Line ("B.Foo"); end Foo; procedure Finalize (Object : in out B) is begin Call_Foo (Object); Foo( Object ); Finalize (A (Object)); end Finalize; This should produce B.Foo B.Foo A.Foo A.Foo. The call to Foo from B.Finalize can use exactly the same implementation as the call to Foo from A.Call_Foo. Both end up in B.Foo when the object is of type B. Both are dispatching calls. Dave Harris, Nottingham, UK | "Weave a circle round him thrice, brangdon@cix.co.uk | And close your eyes with holy dread, | For he on honey dew hath fed http://www.bhresearch.co.uk/ | And drunk the milk of Paradise." ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-16 12:10 ` Dave Harris @ 2002-02-18 8:57 ` Dmitry A. Kazakov 2002-02-18 19:47 ` Merits of re-dispatching Dave Harris 0 siblings, 1 reply; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-18 8:57 UTC (permalink / raw) On Sat, 16 Feb 2002 12:10 +0000 (GMT Standard Time), brangdon@cix.co.uk (Dave Harris) wrote: >dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote (abridged): >> That there are three types changes nothing. It works pretty well in >> Ada 95: > >That still doesn't capture the full complexity, because although you have >Foo you don't have a Call_Foo. The call to Foo from within Call_Foo must >be dispatching, because it must dispatch to different places. Given that >it can do this, it's simplest to say the call to Foo from within the >destructor is dispatching too, in the same way. So there is no >non-dispatching call in the C++. > >I don't know Ada, but I think it would look like: > > type A is ... > procedure Foo (Object : A) is > begin > Put_Line ("A.Foo"); > end Foo; > procedure Call_Foo (Object : A) is > begin > Foo( Object ); -- Dispatching, A.Foo or B.Foo called. One correction, it should be: Foo (A'Class (Object)); -- Re-dispatch to "native" Foo > end Foo; > procedure Finalize (Object : in out A) is > begin > Call_Foo (Object); > Foo (Object); > end Finalize; > > type B is new A ... > procedure Foo (Object : B) is > begin > Put_Line ("B.Foo"); > end Foo; > procedure Finalize (Object : in out B) is > begin > Call_Foo (Object); > Foo( Object ); > Finalize (A (Object)); > end Finalize; > >This should produce B.Foo B.Foo A.Foo A.Foo. With my correction it will produce: B.Foo B.Foo B.Foo A.Foo. \ \ \ \ \ \ \ Foo from A.Finalize -> A.Foo \ \ Call_Foo form A.Finalize -> B.Foo (dispatch) \ Foo from B.Finalize -> B.Foo Call_Foo form B.Finalize -> B.Foo (dispatch) Alternatively, you could make Call_Foo class wide: procedure Call_Foo (Object : A'Class) is begin Foo (Object); -- Dispatching end Call_Foo; This is the official way to say that we want to dispatch on A from it and thus it is defined for all A derivates. [=>it cannot be overridden] Then a call to Call_Foo form B.Finalize could look as follows: Call_Foo (A'Class (Object)); -- Prepare Object to re-dispatching The result output will be same. [Ada is a consistent language (:-))] >The call to Foo from B.Finalize can use exactly the same implementation >as the call to Foo from A.Call_Foo. If both are dispatching, but they are not. Only the call from Call_Foo is dispatching. >Both end up in B.Foo when the object is of type B. Both are >dispatching calls. That's the point. Why on earth a call to Foo from B.Finalize should dispatch? The type is known, so there is only one version of Foo to be called. It can be resolved at compile time and even inlined if necessary. In Ada it does not dispatch. And that is good. But my point was that additionally to that there must be no way to enforce dispatching [like in Call_Foo], because it is unsafe and inefficient. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-18 8:57 ` Dmitry A. Kazakov @ 2002-02-18 19:47 ` Dave Harris 2002-02-19 9:20 ` Dmitry A. Kazakov 0 siblings, 1 reply; 36+ messages in thread From: Dave Harris @ 2002-02-18 19:47 UTC (permalink / raw) dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote (abridged): > > This should produce B.Foo B.Foo A.Foo A.Foo. > > With my correction it will produce: > > B.Foo B.Foo B.Foo A.Foo. Right. So you see that Ada is different from C++, and your description of C++ in Ada terms gives the wrong results. You said earlier that: That is one of several possible interpretation of what happens in C++ using Ada terms. but the other interpretation you offered doesn't correspond to how C++ behaves. That was my only point, really. > Why on earth a call to Foo from B.Finalize should dispatch? The type > is known, so there is only one version of Foo to be called. Let's not muddle the issue with compiler optimisations. A C++ compiler is allowed to replace dynamic dispatch with static if it can deduce the dynamic type of the object at compile-time. Constructors are not a special case (although the deduction is unusually simple for them). All of this is under the "as if" rule. C++ must behave as if it dynamically dispatched in constructors. Dave Harris, Nottingham, UK | "Weave a circle round him thrice, brangdon@cix.co.uk | And close your eyes with holy dread, | For he on honey dew hath fed http://www.bhresearch.co.uk/ | And drunk the milk of Paradise." ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-18 19:47 ` Merits of re-dispatching Dave Harris @ 2002-02-19 9:20 ` Dmitry A. Kazakov 2002-02-21 5:49 ` Hyman Rosen 0 siblings, 1 reply; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-19 9:20 UTC (permalink / raw) On Mon, 18 Feb 2002 19:47 +0000 (GMT Standard Time), brangdon@cix.co.uk (Dave Harris) wrote: >dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote (abridged): >> > This should produce B.Foo B.Foo A.Foo A.Foo. >> >> With my correction it will produce: >> >> B.Foo B.Foo B.Foo A.Foo. > >Right. So you see that Ada is different from C++, and your description of >C++ in Ada terms gives the wrong results. You said earlier that: > > That is one of several possible interpretation of what happens > in C++ using Ada terms. > >but the other interpretation you offered doesn't correspond to how C++ >behaves. My interpretation was that if C++ dispatches then it treats the type as class wide,, otherwise as specific. Then the object is always treated as specific in all calls from destructor. So what? Maybe it is hard to simulate, but well possible to explain. >That was my only point, really. > >> Why on earth a call to Foo from B.Finalize should dispatch? The type >> is known, so there is only one version of Foo to be called. > >Let's not muddle the issue with compiler optimisations. A C++ compiler is >allowed to replace dynamic dispatch with static if it can deduce the >dynamic type of the object at compile-time. Which is almost impossible in general case. However it has nothing to do with optimisation. It is the language design issue. >Constructors are not a special >case (although the deduction is unusually simple for them). All of this is >under the "as if" rule. C++ must behave as if it dynamically dispatched in >constructors. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-19 9:20 ` Dmitry A. Kazakov @ 2002-02-21 5:49 ` Hyman Rosen 2002-02-21 9:04 ` Dmitry A. Kazakov 0 siblings, 1 reply; 36+ messages in thread From: Hyman Rosen @ 2002-02-21 5:49 UTC (permalink / raw) Dmitry A. Kazakov wrote: > My interpretation was that if C++ dispatches then it treats the type > as class wide,, otherwise as specific. Then the object is always > treated as specific in all calls from destructor. So what? Maybe it is > hard to simulate, but well possible to explain. I don't know what it is you think that C++ does, but in fact, all calls to virtual methods in C++ are dispatching, unless you ask for a specifc version using the pointer->CLASS::FUNCTION() notation. In constructors and destructors, the type of the object is the type whose constructor or destructor is running, and that is the type used for dispatching. If the compiler can deduce the actual type of an object in a dispatching call, it is of course free to call the proper method directly, and it may be the case that such deduction is easier in a constructor or destructor, but that is completely irrelevant to understanding what is happening from the language viewpoint. You may persist in maintaining your incorrect mental model of what is going on, but the only effect will be to lead you astray in complicated caes. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-21 5:49 ` Hyman Rosen @ 2002-02-21 9:04 ` Dmitry A. Kazakov 2002-02-21 18:17 ` Hyman Rosen 0 siblings, 1 reply; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-21 9:04 UTC (permalink / raw) On Thu, 21 Feb 2002 05:49:10 GMT, Hyman Rosen <hyrosen@mail.com> wrote: >Dmitry A. Kazakov wrote: >> My interpretation was that if C++ dispatches then it treats the type >> as class wide,, otherwise as specific. Then the object is always >> treated as specific in all calls from destructor. So what? Maybe it is >> hard to simulate, but well possible to explain. > >I don't know what it is you think that C++ does, but in fact, all calls >to virtual methods in C++ are dispatching, unless you ask for a specifc >version using the pointer->CLASS::FUNCTION() notation. In constructors >and destructors, the type of the object is the type whose constructor >or destructor is running, and that is the type used for dispatching. OK, it dispatches as if it didn't. Therefore I say that it does not. What happens internally is of no interest. >If the compiler can deduce the actual type of an object in a dispatching >call, it is of course free to call the proper method directly, and it >may be the case that such deduction is easier in a constructor or >destructor, but that is completely irrelevant to understanding what is >happening from the language viewpoint. The viewpoint of C++ language, but I am not talking about C++ definition of "dispatch" and its implementation issues. >You may persist in maintaining your incorrect mental model of what is >going on, but the only effect will be to lead you astray in complicated >caes. I still prefer a model where types of objects are not arbitrarily changed [no matter physically or mentally] depending on what kind of method is used. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-21 9:04 ` Dmitry A. Kazakov @ 2002-02-21 18:17 ` Hyman Rosen 2002-02-22 9:21 ` Dmitry A. Kazakov 0 siblings, 1 reply; 36+ messages in thread From: Hyman Rosen @ 2002-02-21 18:17 UTC (permalink / raw) Dmitry A. Kazakov wrote: > OK, it dispatches as if it didn't. Therefore I say that it does not. Well, you can say anything you like, but it certainly does dispatch using the type of the constructor or destructor which is running. > What happens internally is of no interest. If that is so, then stop saying that it does not dispatch, because that is true *only* internally. > The viewpoint of C++ language, but I am not talking about C++ > definition of "dispatch" and its implementation issues. You began this discussion by claiming that the equivalent of redispatching from Finalize was unsafe in C++, and I have been correcting your mistaken impressions about C++ ever since. This has nothing whatever to do with implementations of C++, only the definition of its behavior according to its standard. > I still prefer a model where types of objects are not arbitrarily > changed [no matter physically or mentally] depending on what kind of > method is used. You may prefer this, but that is not the way C++ works. It is the way Ada works, which leads to the problem you originally noticed, of redispatching to code which manipulates already Finalized members. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-21 18:17 ` Hyman Rosen @ 2002-02-22 9:21 ` Dmitry A. Kazakov 2002-02-22 16:59 ` Hyman Rosen 0 siblings, 1 reply; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-22 9:21 UTC (permalink / raw) On Thu, 21 Feb 2002 13:17:48 -0500, Hyman Rosen <hyrosen@mail.com> wrote: >Dmitry A. Kazakov wrote: >> OK, it dispatches as if it didn't. Therefore I say that it does not. > >Well, you can say anything you like, but it certainly does dispatch >using the type of the constructor or destructor which is running. > >> What happens internally is of no interest. > >If that is so, then stop saying that it does not dispatch, because >that is true *only* internally. That's the whole point. Dispatching vs. not dispatching is not an implementation detail, but a part of the contract. It seems that we are using different definitions of dispatch, thus we'll never agree. >> The viewpoint of C++ language, but I am not talking about C++ >> definition of "dispatch" and its implementation issues. > >You began this discussion by claiming that the equivalent of >redispatching from Finalize was unsafe in C++, No. I said exactly the opposite: "The behaviour of B::~B is safe." I.e. that C++ behaviour is safe in destructors, because it does not <dispatch> [= dispatches as if it didn't]. >and I have been >correcting your mistaken impressions about C++ ever since. >This has nothing whatever to do with implementations of C++, >only the definition of its behavior according to its standard. > >> I still prefer a model where types of objects are not arbitrarily >> changed [no matter physically or mentally] depending on what kind of >> method is used. > >You may prefer this, but that is not the way C++ works. It is the >way Ada works, which leads to the problem you originally noticed, >of redispatching to code which manipulates already Finalized members. The single problem with Ada approach is that its application was in my view not enough puristic and consequent as it could be. Namely, no re-dispatching should be allowed at all. I definitely do not want that self-contradictory mixture of types and their closures as in C++. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-22 9:21 ` Dmitry A. Kazakov @ 2002-02-22 16:59 ` Hyman Rosen 2002-02-25 8:51 ` Dmitry A. Kazakov 0 siblings, 1 reply; 36+ messages in thread From: Hyman Rosen @ 2002-02-22 16:59 UTC (permalink / raw) Dmitry A. Kazakov wrote: > That's the whole point. Dispatching vs. not dispatching is not an > implementation detail, but a part of the contract. It seems that we > are using different definitions of dispatch, thus we'll never agree. Yes, it's part of the contract, and in C++ it always dispatches. There is only one definition of dispatch that I'm aware of - when a virtual method is called through a pointer to an object, the method actually called is determined by the actual type of the object, not the declared type of the pointer. In C++, it is this actual type which changes as control progresses through the hierarchy of constructors or destructors. > No. I said exactly the opposite: > "The behaviour of B::~B is safe." Here is what you said in your original message: C++ and Ada 95 support re-dispatching, which I think is inherently unsafe > I definitely do not want that self-contradictory mixture > of types and their closures as in C++. I don't even know what this means. C++ does not have "self-contradictory" types. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching 2002-02-22 16:59 ` Hyman Rosen @ 2002-02-25 8:51 ` Dmitry A. Kazakov 0 siblings, 0 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-25 8:51 UTC (permalink / raw) On Fri, 22 Feb 2002 11:59:46 -0500, Hyman Rosen <hyrosen@mail.com> wrote: >Dmitry A. Kazakov wrote: >> That's the whole point. Dispatching vs. not dispatching is not an >> implementation detail, but a part of the contract. It seems that we >> are using different definitions of dispatch, thus we'll never agree. > >Yes, it's part of the contract, and in C++ it always dispatches. >There is only one definition of dispatch that I'm aware of - when >a virtual method is called through a pointer to an object, the method >actually called is determined by the actual type of the object, not >the declared type of the pointer. Compare: the actual type is class wide [no matter pointers, reference, value], the parameter is dispatching. >In C++, it is this actual type which >changes as control progresses through the hierarchy of constructors >or destructors. > >> No. I said exactly the opposite: >> "The behaviour of B::~B is safe." > >Here is what you said in your original message: > C++ and Ada 95 support re-dispatching, > which I think is inherently unsafe Yes this is may point that re-dispatching is unsafe. Also my point is that destructors in C++ are safe because [in your terminology] they dispatche as if the actual type were the formal type. This behaviour is safe, because [according to my terminology] they do not dispatch at all. Which IMO should be always so in a specific subroutine [destructors cannot be class wide]. >> I definitely do not want that self-contradictory mixture > > of types and their closures as in C++. > >I don't even know what this means. C++ does not have >"self-contradictory" types. This means, I quote you : "... actual type which changes as control progresses through the hierarchy of constructors or destructors." If there is no distinction between specific and class wide types, then somersaults like morphing types are inevitable. One cannot mix sets and its elements without a punishment. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 9:16 ` Dmitry A. Kazakov 2002-02-08 18:30 ` Hyman Rosen @ 2002-02-08 23:51 ` Matthew Heaney 2002-02-12 9:02 ` Dmitry A. Kazakov 1 sibling, 1 reply; 36+ messages in thread From: Matthew Heaney @ 2002-02-08 23:51 UTC (permalink / raw) "Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message news:3c6392e8.2400843@News.CIS.DFN.DE... > First C++ does not make any difference between class wide and specific > objects. The same type B is a class wide in B::f () and specific in > B::~B (). So the difference in how B::f and B::~B are dealing with > calls to g(). This is IMO bad. It's bad to the extent that it falsifies the programmer's mental model of how he thinks it should work. It violates the "principle of least astonishment." > Second. The behaviour of B::~B is safe. One should never expect > dispatching in a specific subroutine like destructor. My point is that > the language should prohibit dispatching from specific subroutines. > Therefore in my opinion the behaviour of B::f is unsafe. It is > declared as virtual (dispatching) which means that inside B::f the > type is already *known*. Thus there is no need in [re-]dispatching. A > call to g() can be statically resolved. Well, the programmer can turn off the dispatching: void B::f() { B::g(); //no dispatching } Here C++ and Ada95 diverge wrt syntactic overhead: in Ada95 you pay to turn dispatching on, and in C++ you pay to turn dispatching off. > The reason why C++ dispatches in B::f (), is that otherwise it would > impossible to have class wide subroutines [see the point 1]. In > contrary to this Ada 95 does have class wide routines, so my question, > why [explicit] re-dispatching is supported in Ada 95? Because you may want to let a derived type override the algorithm in the parent type. A class-wide operation typically declares an overall algorithm, and dispatching is used to provide the details: package P is type T is tagged limited null record; procedure Op1 (O : T); procedure Op2 (O : T); procedure Op (O : T'Class); end P; package body P is procedure Op (O : T'Class) is --static begin if <something> then Op1 (O); --dynamic else Op2 (O); --dynamic end if; end Op; ... end P; This is fine, but if you want to change the algorithm used by class-wide operation Op, you can't. If you want to allow a derived type to replace the whole algorithm (not just the parts done by Op1 and Op2), then you have to make the operation primitive for the type, and then turn on the dispatching: package body P is procedure Op (O : T) is begin if <something> then Op1 (T'Class (O)); else Op2 (T'Class (O)); end if; end Op; ... end P; Now a derived type (say, P.C.NT) can override Op. In general, an operation --primitive or class-wide-- should document which dispatching operations it calls, so that a derived type knows what is expected of it. This was the problem with your original example: you seemed to argue that it was some kind of language flaw in that you could call a dispatching operation in the Finalize operation. But this isn't bad because of any flaw in the language, this is bad because it's a dumb thing to do. The idiom for implementing a primitive Finalize operation is to finalize the specific type, and then call the Finalize operation of its parent. In particular, this means you shouldn't call any dispatching operations. In practice this isn't an issue, because operations are statically bound by default. If a Finalize operation does call dispatching operations, then of course this is a programmer error, no different from any other kind of programmer error. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 23:51 ` Merits of re-dispatching [LONG] Matthew Heaney @ 2002-02-12 9:02 ` Dmitry A. Kazakov 0 siblings, 0 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-12 9:02 UTC (permalink / raw) On Fri, 8 Feb 2002 18:51:00 -0500, "Matthew Heaney" <mheaney@on2.com> wrote: >"Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message >news:3c6392e8.2400843@News.CIS.DFN.DE... > > The reason why C++ dispatches in B::f (), is that otherwise it would >> impossible to have class wide subroutines [see the point 1]. In >> contrary to this Ada 95 does have class wide routines, so my question, >> why [explicit] re-dispatching is supported in Ada 95? > >Because you may want to let a derived type override the algorithm in the >parent type. > >A class-wide operation typically declares an overall algorithm, and >dispatching is used to provide the details: > >package P is > type T is tagged limited null record; > procedure Op1 (O : T); > procedure Op2 (O : T); > procedure Op (O : T'Class); >end P; > >package body P is > procedure Op (O : T'Class) is --static > begin > if <something> then > Op1 (O); --dynamic > else > Op2 (O); --dynamic > end if; > end Op; >... >end P; > >This is fine, but if you want to change the algorithm used by class-wide >operation Op, you can't. And this is good, because a class-wide operation is not defined for the root type. It is defined for the closure of all types derived from the root. Thus it cannot be overriden. It is a progam design flaw if you need to do so. What you want is actually multiple implementations, but this is not the realm of class-wided routines, but of dispatching ones. Consider class-wide routines as an [good] alternative to generic routines. You cannot override a generic routine, you can only overload it. Purely theoretically it is thinkable to build a type system with one additional type level: class-class-wide type. Then one could have an ability to dispatch on class-wide routines: procedure MetaOp (O : T'Class'Class) is begin Op (O); -- Meta dispatch end MetaOp; Though I do not think that such thing will appear in Ada very soon (:-)) >In general, an operation --primitive or class-wide-- should document which >dispatching operations it calls, so that a derived type knows what is >expected of it. It is a maintenace disaster. Though LSP cannot be enforced, one should support it as much as possible. However, my point was that the language should not support re-dispatching. If a programmer want it he/she must do some work, for instance, add a type identification member. It is very similar to the Unchecked_Conversion case. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-07 10:26 Merits of re-dispatching [LONG] Dmitry A. Kazakov 2002-02-07 15:03 ` Hyman Rosen @ 2002-02-07 23:40 ` Nick Roberts 2002-02-08 8:56 ` Dmitry A. Kazakov 2002-02-08 1:06 ` Matthew Heaney ` (2 subsequent siblings) 4 siblings, 1 reply; 36+ messages in thread From: Nick Roberts @ 2002-02-07 23:40 UTC (permalink / raw) On Thu, 07 Feb 2002 10:26:00 GMT, dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote: >Hi! > >C++ and Ada 95 support re-dispatching, which I think is inherently >unsafe. Consider the following: > >type A is new Ada.Finalization.Limited_Controlled with ... >procedure Foo (Object : A); >procedure Finalize (Object : in out A) ; > >Now let AA derived from A override Foo and Finalize: > >type AA is new A with record > ... -- Some components, which may use components of A >end record; What do you mean "which may use components of A"? -- Nick Roberts -- Nick Roberts ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-07 23:40 ` Nick Roberts @ 2002-02-08 8:56 ` Dmitry A. Kazakov 0 siblings, 0 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-08 8:56 UTC (permalink / raw) On Thu, 07 Feb 2002 23:40:42 GMT, nickroberts@ukf.net (Nick Roberts) wrote: >On Thu, 07 Feb 2002 10:26:00 GMT, dmitry@elros.cbb-automation.de (Dmitry A. >Kazakov) wrote: > >>Hi! >> >>C++ and Ada 95 support re-dispatching, which I think is inherently >>unsafe. Consider the following: >> >>type A is new Ada.Finalization.Limited_Controlled with ... >>procedure Foo (Object : A); >>procedure Finalize (Object : in out A) ; >> >>Now let AA derived from A override Foo and Finalize: >> >>type AA is new A with record >> ... -- Some components, which may use components of A >>end record; > >What do you mean "which may use components of A"? It was a bad wording , I admit. Should be "some components, which may depend on components of A". Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-07 10:26 Merits of re-dispatching [LONG] Dmitry A. Kazakov 2002-02-07 15:03 ` Hyman Rosen 2002-02-07 23:40 ` Nick Roberts @ 2002-02-08 1:06 ` Matthew Heaney 2002-02-08 9:48 ` Dmitry A. Kazakov 2002-02-08 18:10 ` Hyman Rosen 2002-02-08 18:33 ` Nick Roberts 2002-02-14 20:57 ` Tucker Taft 4 siblings, 2 replies; 36+ messages in thread From: Matthew Heaney @ 2002-02-08 1:06 UTC (permalink / raw) "Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message news:3c62524f.93369796@News.CIS.DFN.DE... > procedure Finalize (Object : in out A) is > begin > Foo (A'Class (Object)); -- Re-dispatch to the child's Foo > end Finalize; > > When A is finalized as a part of AA, it re-dispatches to AA.Foo, which > may access already finalized parts of AA! A user of A has no way to > learn the problem from the specification of A. He must look into the > implementation of A.Finalize. The same is valid for Initialize as > well. Actually it is a common error among fresh baked C++ programmers > to call virtual functions (dispatch) from constructors. Yes, but in Ada95, operations are static by default. You have to take active steps in order to force the dispatching to occur. So the scenario you describe cannot happen accidently. This is not the case in C++, where virtual member functions called through a reference or pointer automatically dispatch. So yes you can very easily have the problem you cite in C++. (Which is why many C++ books point out this feature of the language.) > Though merits of re-dispatching seem to me suspicious, it is easy to > implement it without casting. Let we really want object's IDENTITY, > i.e. objects which always "know" their actual type. Ada 95 perfectly > supports this without any casting! > > package Self_Identifying is > type A is new > Ada.Finalization.Limited_Controlled with private; > type A_Ptr is access all A'Class; > procedure Finalize (Object : in out A); > procedure Foo (Object : in out A); > private > type A is new > Ada.Finalization.Limited_Controlled with > record > Self : A_Ptr := A'Unchecked_Access; -- Class wide self-reference > end record; > end Self_Identifying; This is a horrible way to do this. Don't use a named access type when an access discriminant will do: type Handle_Type (O : access A'Class) is limited null record; type A is new Limited_Controlled with record H : Handle_Type (A'Access); end record; > Is there examples where re-dispatching is really unavoidable? There is a very simple solution to your problem: don't do it. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 1:06 ` Matthew Heaney @ 2002-02-08 9:48 ` Dmitry A. Kazakov 2002-02-09 0:16 ` Matthew Heaney 2002-02-08 18:10 ` Hyman Rosen 1 sibling, 1 reply; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-08 9:48 UTC (permalink / raw) On Thu, 7 Feb 2002 20:06:42 -0500, "Matthew Heaney" <mheaney@on2.com> wrote: >"Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message >news:3c62524f.93369796@News.CIS.DFN.DE... >> >> package Self_Identifying is >> type A is new >> Ada.Finalization.Limited_Controlled with private; >> type A_Ptr is access all A'Class; >> procedure Finalize (Object : in out A); >> procedure Foo (Object : in out A); >> private >> type A is new >> Ada.Finalization.Limited_Controlled with >> record >> Self : A_Ptr := A'Unchecked_Access; -- Class wide self-reference >> end record; >> end Self_Identifying; > >This is a horrible way to do this. Don't use a named access type when an >access discriminant will do: > > type Handle_Type (O : access A'Class) is limited null record; > > type A is new Limited_Controlled with record > H : Handle_Type (A'Access); > end record; I like it, but it has its own drawbacks. If only the component H should be exposed and all others be hidden, then one should make a lot of equilibristic: -- -- Public part of A -- type A_Public; type A_Handle (O : access A_Public'Class) is limited null record; type A_Public is abstract new Ada.Finalization.Limited_Controlled with record H : A_Handle (A_Public'Access); end record; procedure Foo (Object : in out A_Public) is abstract; -- -- Full A -- type A is new A_Public with private; type A_Ptr is access all A'Class; procedure Foo (Object : in out A); private type A is new A_Public with record ... -- private components; end record; But this is another question of course. >> Is there examples where re-dispatching is really unavoidable? > >There is a very simple solution to your problem: don't do it. So there is no such examples! (:-)) Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 9:48 ` Dmitry A. Kazakov @ 2002-02-09 0:16 ` Matthew Heaney 0 siblings, 0 replies; 36+ messages in thread From: Matthew Heaney @ 2002-02-09 0:16 UTC (permalink / raw) "Dmitry A. Kazakov" <dmitry@elros.cbb-automation.de> wrote in message news:3c6397e5.3677906@News.CIS.DFN.DE... So there is no such examples! (:-)) As I pointed out in a previous message, you want the language to allow you to do this (redispatching), in order to allow the derived type to override the entire algorithm, instead of just parts of it. But you should always advertise which operations dispatch in an operation (especially if the operation is primitive for the type). Calling a dispatching operation from inside another method is not simply an implementation detail. (Unlike the case in which the call is static, which really is an implemenation detail.) ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 1:06 ` Matthew Heaney 2002-02-08 9:48 ` Dmitry A. Kazakov @ 2002-02-08 18:10 ` Hyman Rosen 2002-02-09 0:41 ` Matthew Heaney 1 sibling, 1 reply; 36+ messages in thread From: Hyman Rosen @ 2002-02-08 18:10 UTC (permalink / raw) "Matthew Heaney" <mheaney@on2.com> wrote in message news:u668rmhvhdub11@corp.supernews.com... > Yes, but in Ada95, operations are static by default. You have to take > active steps in order to force the dispatching to occur. So the scenario > you describe cannot happen accidently. > > This is not the case in C++, where virtual member functions called through a > reference or pointer automatically dispatch. So yes you can very easily > have the problem you cite in C++. (Which is why many C++ books point out > this feature of the language.) I don't understand the argument you are making. The problem seems to me that when you make a dispatching call in the destructor (automatically in C++ or by request in Ada), C++ will take you to a safe function, whereas Ada will take you to a function which can manipulate already finalized submembers. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 18:10 ` Hyman Rosen @ 2002-02-09 0:41 ` Matthew Heaney 0 siblings, 0 replies; 36+ messages in thread From: Matthew Heaney @ 2002-02-09 0:41 UTC (permalink / raw) "Hyman Rosen" <hyrosen@mail.com> wrote in message news:1013191766.895310@master.nyc.kbcfp.com... > I don't understand the argument you are making. The problem seems to me that > when you make a dispatching call in the destructor (automatically in C++ or by > request in Ada), C++ will take you to a safe function, whereas Ada will take > you to a function which can manipulate already finalized submembers. First of all, the programmer may depend on the derived class member function being called, so saying that it's "safe" to call the base class member function instead of the derived class member function is specious. Yes, we all know why the language behaves this way, and we agree that the language should behave this way, but that doesn't help the programmer who mistakenly believes otherwise. Even if the base class member function is called, his class is still broken. Second of all, you can't make a strict one-for-one comparison of the languages wrt destruction (C++) and finalization (Ada95). In C++, the derived part has been destroyed, and who knows what mayhem would ensue if a member function were called after that had happened. So of course C++ does the right thing by not calling the derived class member function. However, in Ada95, the Finalize operation of the parent is called while the Finalize operation of the derived type is still executing (per the idiom, it's the last thing you do before returning). So the derived part of the type is still there. To call a dispatching operation in the parent's Finalize amounts to a contract violation (a precondition wasn't satisfied by the caller -- here, the parent type). This is bad, but it's no different then any other contract violation. And who knows? You could easily implement the Finalize operation using dispatching calls -- it's up to the designer of the class. For example: package P is type T is new Finalization.Limited_Controlled with null record; procedure Op1 (O : in out T); procedure Op2 (O : in out T); procedure Finalize (O : in out T); end P; package body P is procedure Finalize (O : in out T) is begin Op1 (T'Class (O)); --dispatch Op2 (T'Class (O)); end; end P; package P.C is type NT is new T with null record; procedure Op1 (O : in out NT); procedure Op2 (O : in out NT); procedure Finalize (O : in out NT); end P.C: package body P.C is procedure Finalize (O : in out NT) is begin Finalize (T (O)); --call parent's version end; end P.C; Here the derived type implements Finalize by calling the parent's Finalize, who then calls the derived type back. This is perfectly legal, and perfectly safe. It's also idiosyncratic, but that's another matter. ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-07 10:26 Merits of re-dispatching [LONG] Dmitry A. Kazakov ` (2 preceding siblings ...) 2002-02-08 1:06 ` Matthew Heaney @ 2002-02-08 18:33 ` Nick Roberts 2002-02-09 4:07 ` Nick Roberts 2002-02-12 10:13 ` Dmitry A. Kazakov 2002-02-14 20:57 ` Tucker Taft 4 siblings, 2 replies; 36+ messages in thread From: Nick Roberts @ 2002-02-08 18:33 UTC (permalink / raw) On Thu, 07 Feb 2002 10:26:00 GMT, dmitry@elros.cbb-automation.de (Dmitry A. Kazakov) wrote: >C++ and Ada 95 support re-dispatching, which I think is inherently >unsafe. Consider the following: >... >procedure Finalize (Object : in out A) is >begin > Foo (A'Class (Object)); -- Re-dispatch to the child's Foo >end Finalize; This is what I would certainly describe as a 'pathalogical' case. It is a basic principle in Finalize (it should be bloody obvious, but all the same it should be taught) that you normally assume any and all extensions to the Object have already been finalised, and so you don't try something like the above redispatching! >Though merits of re-dispatching seem to me suspicious, I'd like you to expand on that statement, please! > it is easy to >implement it without casting. >... >With re-dispatching the contract of Finalize is not clear. According >to its specification it receives an argument of type A. Thus there >should be no legal way for Finalize to discover that actually an >instance of AA was passed. I.e. Foo (A'Class (Object)) should always >statically dispatch to A.Foo. Yet it dispatches to AA.Foo. This is a >violation of substitutability. [Not that I hold LSP for a sacral cow, >but there is no reason to encourage its violation.] If the implementor is going to do the above, it is his responsibility to ensure it will work; otherwise he must not do it (that is the 'contract' implicit in implementing Finalization). I would say a compiler should give a strong warning about redispatching on Object in Finalization; maybe it should be explicitly made illegal in the next language revision. >I think that there >should be no exceptions from the rule "Want dispatching in a >subprogram? Make the argument class wide." A nice idea, but it would be too restrictive. There are many genuine cases where it is necessary to be able to dispatch on an object passed in as a specific type (to a primitive operation of that type). >What is worse is that implementation of re-dispatching requires that >the type tag be accessible for not only class wide objects but also >for specific ones. Thus tagged types are reference types, type tag >shall be a part of a tagged object. As a consequence elementary types >cannot be tagged and so they are excluded from OOP. Maybe I have >missed something, but I see no other reason why not to allow for all >types inheritance, dispatching subroutines and class wide objects. Except that it is really very unlikely in practice to be particularly useful. How many times, in real software, is a primary type not going to have to be a record type anyway? Very few! >Methodically re-dispatching breaks segregation of class wide and >specific types returning us to the languages like C++, where there is >no difference between them. So Ada lingers somewhere in-between. To be honest, I think Ada lingers somewhere inbetween the equally onerous extremes of the pig trough and the ivory tower. The fact that Ada is pragmatic enough to permit a primitive operation with two or more parameters of the same (tagged) type, and a primitive operation of a tagged type with futher parameters of that type's class, is as commendable as the fact that it eschews features, such as multiple inheritance, which may be very sexy, but have serious practical difficulties. >Is there examples where re-dispatching is really unavoidable? Consider a tagged type (class) that represents a 'window' in a GUI subsystem. It is derived from a 'drawable' type (class), that has a (virtual) primitive operation 'redraw', which is called (by the window manager/executive) in order to make the object redraw itself, whenever necessary. Our 'window' type (class) is a kind of container: it can contain a number of other drawables. Perhaps it also has a background image that it draws behind its components. Now consider its implementation of 'redraw'. This must surely be on the lines of: (a) draw the background image; (b) for each component drawable, call its 'redraw' (with appropriate parameters). In Ada, the calls to the 'redraw' of each component will be explicitly dispatching calls. (In C++, they will be dispatched to implicitly.) The point to understand is that one of those components could be of the 'window' type (representing a window-within-a-window presumably); thus the primitive redraw operation of the window type ends up redispatching to the redraw of another window object. And yes, this situation does threaten the danger of an infinite loop (many programming situations do). I think, Dmitry, you will find in practice many genuine cases where this kind of redispatching is impractical to avoid. All the best, -- Nick Roberts ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 18:33 ` Nick Roberts @ 2002-02-09 4:07 ` Nick Roberts 2002-02-12 10:13 ` Dmitry A. Kazakov 1 sibling, 0 replies; 36+ messages in thread From: Nick Roberts @ 2002-02-09 4:07 UTC (permalink / raw) On Fri, 08 Feb 2002 18:33:31 GMT, nickroberts@ukf.net (Nick Roberts) strongly typed: >This is what I would certainly describe as a 'pathalogical' case. It is a Although maybe I ought to spell it "pathological" ;-) -- Nick Roberts ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-08 18:33 ` Nick Roberts 2002-02-09 4:07 ` Nick Roberts @ 2002-02-12 10:13 ` Dmitry A. Kazakov 1 sibling, 0 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-12 10:13 UTC (permalink / raw) On Fri, 08 Feb 2002 18:33:31 GMT, nickroberts@ukf.net (Nick Roberts) wrote: >On Thu, 07 Feb 2002 10:26:00 GMT, dmitry@elros.cbb-automation.de (Dmitry A. >Kazakov) wrote: > >>C++ and Ada 95 support re-dispatching, which I think is inherently >>unsafe. Consider the following: >>... >>procedure Finalize (Object : in out A) is >>begin >> Foo (A'Class (Object)); -- Re-dispatch to the child's Foo >>end Finalize; > >This is what I would certainly describe as a 'pathalogical' case. It is a >basic principle in Finalize (it should be bloody obvious, but all the same >it should be taught) that you normally assume any and all extensions to the >Object have already been finalised, and so you don't try something like the >above redispatching! > >>Though merits of re-dispatching seem to me suspicious, > >I'd like you to expand on that statement, please! I see no pressing necessity in supporting re-dispatching. At the same time the price of its supporting is high. >> it is easy to >>implement it without casting. >>... >>With re-dispatching the contract of Finalize is not clear. According >>to its specification it receives an argument of type A. Thus there >>should be no legal way for Finalize to discover that actually an >>instance of AA was passed. I.e. Foo (A'Class (Object)) should always >>statically dispatch to A.Foo. Yet it dispatches to AA.Foo. This is a >>violation of substitutability. [Not that I hold LSP for a sacral cow, >>but there is no reason to encourage its violation.] > >If the implementor is going to do the above, it is his responsibility to >ensure it will work; otherwise he must not do it (that is the 'contract' >implicit in implementing Finalization). I would say a compiler should give >a strong warning about redispatching on Object in Finalization; maybe it >should be explicitly made illegal in the next language revision. This is the C++'s way. I would prefer a more radical aproach, i.e. removing the language support of re-dispatching. It means that the language will not require that the conversion class->specific is a view conversion. Just remove this requirement and re-dispatching will disappear. >>I think that there >>should be no exceptions from the rule "Want dispatching in a >>subprogram? Make the argument class wide." > >A nice idea, but it would be too restrictive. There are many genuine cases >where it is necessary to be able to dispatch on an object passed in as a >specific type (to a primitive operation of that type). But you still can do it explicitly by providing a type identification member. Then generally, I have a strong feeling [not a proof of course] that all that genuine cases are in fact more or less design flaws. >>What is worse is that implementation of re-dispatching requires that >>the type tag be accessible for not only class wide objects but also >>for specific ones. Thus tagged types are reference types, type tag >>shall be a part of a tagged object. As a consequence elementary types >>cannot be tagged and so they are excluded from OOP. Maybe I have >>missed something, but I see no other reason why not to allow for all >>types inheritance, dispatching subroutines and class wide objects. > >Except that it is really very unlikely in practice to be particularly >useful. How many times, in real software, is a primary type not going to >have to be a record type anyway? Very few! 1. It would simplify the language. More regularity is IMO better. And yes, I would like to have class-wide task and protected object types (:-)) 2. Extensible enumeration types are good candidates for class-wide programming. 3. Let I want some special overflow processing for a numeric type? I just derive from it and then override +. I can do it even in Ada 83, but I cannot write a class-wide routine which will work with both types. So I must call the devil = generics. (:-)) 2 .& 3. Consider class-wide routines as an alternative to generics. In many cases I would definitely prefer Integer'Class to type Int range <>; 4. Attributes like Integer'Image (x) should be overridable dispatching primitive routines: Image (x : Integer). 5. Interface inheritance. Forget for a minute that in Ada implementation is always inherited. Consider you are developing a type MyString. String is an array in the sense that it *looks* like an array. In other words it has the interface of an array. So you derive your type from an array type, provide a private implementation [which possibly would be a record type], but the interface is of an array. So you have indexing, slices etc. >>Methodically re-dispatching breaks segregation of class wide and >>specific types returning us to the languages like C++, where there is >>no difference between them. So Ada lingers somewhere in-between. > >To be honest, I think Ada lingers somewhere inbetween the equally onerous >extremes of the pig trough and the ivory tower. The fact that Ada is >pragmatic enough to permit a primitive operation with two or more >parameters of the same (tagged) type, and a primitive operation of a tagged >type with futher parameters of that type's class, is as commendable as the >fact that it eschews features, such as multiple inheritance, which may be >very sexy, but have serious practical difficulties. Well, I was always for MI and MD! (:-)) >>Is there examples where re-dispatching is really unavoidable? > >Consider a tagged type (class) that represents a 'window' in a GUI >subsystem. It is derived from a 'drawable' type (class), that has a >(virtual) primitive operation 'redraw', which is called (by the window >manager/executive) in order to make the object redraw itself, whenever >necessary. > >Our 'window' type (class) is a kind of container: it can contain a number >of other drawables. Perhaps it also has a background image that it draws >behind its components. Now consider its implementation of 'redraw'. This >must surely be on the lines of: (a) draw the background image; (b) for each >component drawable, call its 'redraw' (with appropriate parameters). I would make "redraw" class-wide and represent all child windows by class-wide pointers. Another example, where re-dispatching appears is layered protocols. You have dispatching abstract Get to read one byte, then in some derived type you have dispatching Get to read the mantra flux distribution. You must somehow dispatch from there to read your bytes. Oops, that would be re-dispatching! However, in this case one could decouple the low-level I/O primitives in some separate type Driver and use it as an access discriminant. >In Ada, the calls to the 'redraw' of each component will be explicitly >dispatching calls. (In C++, they will be dispatched to implicitly.) The >point to understand is that one of those components could be of the >'window' type (representing a window-within-a-window presumably); thus the >primitive redraw operation of the window type ends up redispatching to the >redraw of another window object. And yes, this situation does threaten the >danger of an infinite loop (many programming situations do). >I think, Dmitry, you will find in practice many genuine cases where this >kind of redispatching is impractical to avoid. Of course this is a question of balance. It is not that re-dispatching should be disallowed. There is no way to do it as long a class-wide self-pointer can be put into the object. My point is that there is no need to support re-dispatching at the language level [the requirement that the conversion to A'Class when applied to the formal parameter gives you the actual type) Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-07 10:26 Merits of re-dispatching [LONG] Dmitry A. Kazakov ` (3 preceding siblings ...) 2002-02-08 18:33 ` Nick Roberts @ 2002-02-14 20:57 ` Tucker Taft 2002-02-15 15:43 ` Dmitry A. Kazakov 4 siblings, 1 reply; 36+ messages in thread From: Tucker Taft @ 2002-02-14 20:57 UTC (permalink / raw) "Dmitry A. Kazakov" wrote: > > Hi! > > C++ and Ada 95 support re-dispatching, which I think is inherently > unsafe. > ... Redispatching is the default in most OO languages. In some, like Eiffel and Java, there is no alternative. In C++, it is possible to explicitly prevent redispatching by using the :: operator. By default, if you use a reference, a pointer (e.g. "this"), or just the virtual function name by itself, you get (re)dispatching. In Ada 95, we chose to make static binding the default, with (re)dispatching happening only when the actual parameter is of the classwide type (T'Class). This was a conscious decision, and means that places where (re)dispatching is occurring are visible to the reader, and requires some explicit intent by the programmer. This reduces the cases of unintentional redispatching, and (hopefully) encourages the programmer to mention internal redispatching as part of the semantics of the operation. In your example, you have used redispatching in a finalization routine. As you point out, that is an unusual time to do so, and is probably unwise, because the descendant's finalization activity has probably already taken place. Hence, your example illustrates an inappropriate use of redispatching. On the other hand, there are many examples of appropriate use of redispatching. However, these all are cases where the semantics of the routine should be explicitly idenfitied as including redispatching. For example, consider the following: type T is tagged ... function Image(X : T) return String; -- Return string representation of T procedure Put(X : T); -- Display T on standard output; by default, -- put result of (redispatch to) Image function In this example, we have defined the "default" semantics of "Put" to be a Text_IO.Put of a redispatching call to Image. This would allow a programmer to override Image only, and have the desirable effect of having Put's results altered as well. Here is the implementation of this "default" Put: procedure Put(X : T) is -- by default, put result of calling Image begin Text_IO.Put(Image(T'Class(X)); end Put; On the other hand, for some complex types, it may be desirable to take advantage of the fact we are displaying on standard output, and we might create a multi-line, nicely indented version of the display. In that case, we would want to override Put to not simply put out the result of calling Image. Clearly this relationship between the Image and Put primitive operations needs to be explicit in the spec for "T". Otherwise, when we release a new version, we might choose to change Put to effectively "inline" the semantics of Image, rather than making a redispatching call. In that case, the programmer could choose to inherit the "Put" semantics as is while changing "Image", but could no longer override only Image and expect Put to change "correspondingly." This is a fairly simple example, but with a complex type, the relationships can get very complicated. If no redispatching is done, then all of the operations are independent "black boxes," and they can be inherited if they already have the desired effect, or overridden if they don't. When redispatching is present, the programmer needs to know the relationships between the primitive operations, so they can make appropriate decisions about inheriting versus overriding. As mentioned above, in Ada 95, we chose to make static binding the default for calls from one primitive to the next, thereby providing minimal "coupling" between the primitives, and allowing them to be reused as black boxes. However, we felt it was essential that we also supported explicit redispatching, for the cases where "pass-the-buck" semantics were appropriate, where by default one primitive should redispatch to another, allowing a fewer number of primitives to be overridden when extending a type. In any case, we agree that redispatching can be the wrong thing to do, and making it the default opens the door for unintentional coupling between primitives, effectively requiring that a programmer see the source code for the primitives of a parent type to determine the effect of overriding some but not all of the primitives in a derived type. > ... > Is there examples where re-dispatching is really unavoidable? If you are always willing to override all primitive operations, then re-dispatching is avoidable. However, if you want to override only some of the primitives, and rely on the others being defined by default in terms of these, you can simplify the job of extending a type. But as indicated above, we certainly agree that redispatching is part of the *visible* semantics of a primitive operation, when extending a type. > > Regards, > Dmitry Kazakov -- -Tucker Taft stt@avercom.net http://www.avercom.net Chief Technology Officer, AverCom Corporation (A Titan Company) Bedford, MA USA (AverCom was formerly the Commercial Division of AverStar: http://www.averstar.com/~stt) ^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: Merits of re-dispatching [LONG] 2002-02-14 20:57 ` Tucker Taft @ 2002-02-15 15:43 ` Dmitry A. Kazakov 0 siblings, 0 replies; 36+ messages in thread From: Dmitry A. Kazakov @ 2002-02-15 15:43 UTC (permalink / raw) On Thu, 14 Feb 2002 15:57:05 -0500, Tucker Taft <stt@avercom.net> wrote: >"Dmitry A. Kazakov" wrote: >> >> Hi! >> >> C++ and Ada 95 support re-dispatching, which I think is inherently >> unsafe. >> ... > >Redispatching is the default in most OO languages. In some, like >Eiffel and Java, there is no alternative. In C++, it is possible >to explicitly prevent redispatching by using the :: operator. By default, >if you use a reference, a pointer (e.g. "this"), or just >the virtual function name by itself, you get (re)dispatching. > >In Ada 95, we chose to make static binding the default, with (re)dispatching >happening only when the actual parameter is of the classwide type (T'Class). >This was a conscious decision, and means that places where (re)dispatching >is occurring are visible to the reader, and requires some explicit intent >by the programmer. This reduces the cases of unintentional redispatching, >and (hopefully) encourages the programmer to mention internal redispatching >as part of the semantics of the operation. I consider class-wide types as the major advance in OO in recent years, but it is another issue (:-)). >In your example, you have used redispatching in a finalization routine. >As you point out, that is an unusual time to do so, and is probably >unwise, because the descendant's finalization activity has probably already >taken place. Hence, your example illustrates an inappropriate use >of redispatching. On the other hand, there are many examples of appropriate >use of redispatching. However, these all are cases where the semantics >of the routine should be explicitly idenfitied as including redispatching. > >For example, consider the following: > > type T is tagged ... > function Image(X : T) return String; -- Return string representation of T > procedure Put(X : T); -- Display T on standard output; by default, > -- put result of (redispatch to) Image function > >In this example, we have defined the "default" semantics of "Put" to >be a Text_IO.Put of a redispatching call to Image. This would allow a programmer >to override Image only, and have the desirable effect of having Put's results altered >as well. Here is the implementation of this "default" Put: > > procedure Put(X : T) is -- by default, put result of calling Image > begin > Text_IO.Put(Image(T'Class(X)); > end Put; > >On the other hand, for some complex types, it may be desirable to take advantage >of the fact we are displaying on standard output, and we might create a multi-line, >nicely indented version of the display. In that case, we would want to override >Put to not simply put out the result of calling Image. This resembles the example of Nick Roberts. Of course, such small examples have little to do with real systems, so it is had to judge. But I think that here there are [should be (:-))] alternatives to redispatching as well. 1. Methodically, if we are going to override Put, then we honestly believe that the place where it displays its output, is a property of the type. Thus it would be logical to have: type T (Output : access Display'Class) is ...; Display is some type representing the variety of output devices. then Put could be made class-wide: procedure Put (X : T'Class) is Text : constant String := Image (X); -- Dispatch on representation begin Write (Output, Text); -- Dispatch on devices end Put; 2. If it is not a property of the type, then Put could have an additional parameter: procedure Put (X : T'Class; Output: in out Display'Class) is Text : constant String := Image (X); -- Dispatch on representation begin Write (Output, Text); -- Dispatch on devices end Put; Only if multiple implementations of Put cannot be factored out as a set of types [an OO-fundamentalist would claim that it is always possible, but I am not so sure], then we are in trouble. Anyway Ada 95 supports redispatching without implicit conversion to T'Class. >Clearly this relationship between the Image and Put primitive operations >needs to be explicit in the spec for "T". Otherwise, when we release a new >version, we might choose to change Put to effectively "inline" the semantics >of Image, rather than making a redispatching call. In that case, the >programmer could choose to inherit the "Put" semantics as is while changing "Image", >but could no longer override only Image and expect Put to change "correspondingly." > >This is a fairly simple example, but with a complex type, the relationships >can get very complicated. If no redispatching is done, then all of the >operations are independent "black boxes," and they can be inherited if they >already have the desired effect, or overridden if they don't. When redispatching >is present, the programmer needs to know the relationships between the primitive >operations, so they can make appropriate decisions about inheriting versus >overriding. > >As mentioned above, in Ada 95, we chose to make static binding the default for calls >from one primitive to the next, thereby providing minimal "coupling" between >the primitives, and allowing them to be reused as black boxes. Amen! (:-)) >However, we >felt it was essential that we also supported explicit redispatching, for the >cases where "pass-the-buck" semantics were appropriate, where by default >one primitive should redispatch to another, allowing a fewer number of primitives to >be overridden when extending a type. > >In any case, we agree that redispatching can be the wrong thing to do, and >making it the default opens the door for unintentional coupling between >primitives, effectively requiring that a programmer see the source code >for the primitives of a parent type to determine the effect of overriding >some but not all of the primitives in a derived type. > >> ... >> Is there examples where re-dispatching is really unavoidable? > >If you are always willing to override all primitive operations, >then re-dispatching is avoidable. However, if you want to override >only some of the primitives, and rely on the others being defined >by default in terms of these, you can simplify the job of extending >a type. But as indicated above, we certainly agree that redispatching >is part of the *visible* semantics of a primitive operation, when extending >a type. Regards, Dmitry Kazakov ^ permalink raw reply [flat|nested] 36+ messages in thread
end of thread, other threads:[~2002-02-25 8:51 UTC | newest] Thread overview: 36+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2002-02-07 10:26 Merits of re-dispatching [LONG] Dmitry A. Kazakov 2002-02-07 15:03 ` Hyman Rosen 2002-02-08 1:29 ` Matthew Heaney 2002-02-08 9:16 ` Dmitry A. Kazakov 2002-02-08 18:30 ` Hyman Rosen 2002-02-09 0:10 ` Matthew Heaney 2002-02-12 8:32 ` Dmitry A. Kazakov 2002-02-12 21:37 ` Hyman Rosen 2002-02-13 9:29 ` Dmitry A. Kazakov 2002-02-13 14:32 ` Hyman Rosen 2002-02-13 19:58 ` Dave Harris 2002-02-14 15:06 ` Dmitry A. Kazakov 2002-02-16 12:10 ` Dave Harris 2002-02-18 8:57 ` Dmitry A. Kazakov 2002-02-18 19:47 ` Merits of re-dispatching Dave Harris 2002-02-19 9:20 ` Dmitry A. Kazakov 2002-02-21 5:49 ` Hyman Rosen 2002-02-21 9:04 ` Dmitry A. Kazakov 2002-02-21 18:17 ` Hyman Rosen 2002-02-22 9:21 ` Dmitry A. Kazakov 2002-02-22 16:59 ` Hyman Rosen 2002-02-25 8:51 ` Dmitry A. Kazakov 2002-02-08 23:51 ` Merits of re-dispatching [LONG] Matthew Heaney 2002-02-12 9:02 ` Dmitry A. Kazakov 2002-02-07 23:40 ` Nick Roberts 2002-02-08 8:56 ` Dmitry A. Kazakov 2002-02-08 1:06 ` Matthew Heaney 2002-02-08 9:48 ` Dmitry A. Kazakov 2002-02-09 0:16 ` Matthew Heaney 2002-02-08 18:10 ` Hyman Rosen 2002-02-09 0:41 ` Matthew Heaney 2002-02-08 18:33 ` Nick Roberts 2002-02-09 4:07 ` Nick Roberts 2002-02-12 10:13 ` Dmitry A. Kazakov 2002-02-14 20:57 ` Tucker Taft 2002-02-15 15:43 ` Dmitry A. Kazakov
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox