Topic: overriding virtual functions under MI


Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Sun, 19 Dec 1993 09:38:29 GMT
Raw View
In article <1993Dec17.154514.28741@news.clarkson.edu> satish@gandalf.mcs.clarkson.edu (Satish Thatte) writes:
>When I first learned about polymorphism I understood a
>polymorphic function to be one with a single declaration
>(in the original declaring class) and possibly multiple
>implementations (based on overriding in derived classes)
>among which one is selected at run time.
>
>This understanding doesn't quite work for C++.
>The override policy in C++ seems to be:
>
> A function defined in class C overrides EVERY
> inherited virtual function with the same signature,
> even if there are several such, inherited from
> *unrelated* classes due to multiple inheritance.

 Yes. Its called a join.
>
>This means it is possible to have a single implementation for
>multiple independent virtual functions.

 Yes.
>
>The thing that bothers me about this (apart from having to
>change my understanding) is that this sort of thing could
>be quite dangerous, because one may end up unintentionally
>overriding functions from imperfectly understood base classes.

 Its possible.

>Since the compiler will not complain about ambiguity,
>it is difficult to track down this kind of problem.

 Its possible, but the reverse is worse: you
intend to do a join and mismatched the signatures,
so the default method is called instead of the
one you thought should have overridden it.

>
>I have not seen any rationale for this decision in the ARM.
>Perhaps I missed it?  Could some one please point me to
>a source for an explanation?

 The ARM gives a couple of examples where sibling call
is used. In conjunction with forking (use of share bases)
joining enables a powerful technique called mixins.

 With fork/join, the base function does relate
the functions being joined. So your question amounts to:
what does it mean if the joined functions are not
related by a shared base?

 Lets look at an example:

 struct Plane {
  virtual void Draw()const=0;
  virtual float Cost()const=0;
  virtual float MaxHeight()const=0;
  virtual float MaxSpeed()const=0;
  float FuelConsumption()const=0;
  ...
 };

I'm sure you can imagine an implementation or two.

 struct Ship {
  virtual void Draw()const =0;
  virtual void Cost()const =0;
  virtual void MaxDepth()const=0;
  virtual void MaxSpeed()const=0;
  float FuelConsumption()const;
  Position ...
 };

Again I'm sure you can imagine implementations, note that
submarines are counted as ships :-)

Now, you can imagine a BattleMap program that draws a map
and shows various ships (this is a computer game) and planes.
There are two lists: Planes and Ships. They're drawn in
different colours by setting an environment variable (PenColour).

Implementing an object of the type:

 struct SeaPlane : public virtual Ship
  public virtual Plane {};

is useful. Overland, its a Plane, and has to be linked into
the "Plane" list. Over sea, it could be either, presumably
the game player can choose.  Steering a SeaPlane designated
a Ship over the coastline causes a crash!

Notice a problematical design decision. The Fuel Consumption
is NOT a virtual function. Its meaning is deliberately
ambiguous for a SeaPlane. This is where upcasting,
that is, forgetting stuff, is useful. Because if you
know the SeaPlane as a Ship you get a different FuelConsumption
to a SeaPlane as a Plane.

So on the BattleMap, since the SeaPlane is known as either
a Ship or a Plane (it had better be in one list or the other)
its FuelConsumption is fixed not by polymorphism and
late binding but by its STATIC type. Which is correct.

The decision to make the MaxSpeed virtual is clearly
problematical here: Planes move on the ground too,
but most cant move very far on the sea: perhaps there
should have been three functions for speed?.

This kind of joining without a common base is a way of
forming a union of abstract methods: a sort of weak
signature based form of polymorphism. The design
method would be to define a global set of possible
signatures, and choose various useful subsets as classes.

When you implement an object, you can derive it from the
sets of useful classes for which it covers the signatures
(or not, if one combinaion is accidental).

[Comment: clearly, for the game player to know about
SeaPlanes, the GameControl would need to keep lists
of objects of various types -- including discriminating
the SeaPlane. But the BattleMap class need not know about this.
Thus, a form of reusability: BattleMap can be closed as a class,
and you dont need to reopen it and modify it when you invent
a SeaPlane: the GameControl class might well have to be
modified to account for every type of object.]

BTW: I've never tried this, it just sound right. Feel free
to tell me why it doesnt work or is not a good idea.

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: satish@gandalf.mcs.clarkson.edu (Satish Thatte)
Date: Fri, 17 Dec 1993 15:45:14 GMT
Raw View
When I first learned about polymorphism I understood a
polymorphic function to be one with a single declaration
(in the original declaring class) and possibly multiple
implementations (based on overriding in derived classes)
among which one is selected at run time.

This understanding doesn't quite work for C++.
The override policy in C++ seems to be:

 A function defined in class C overrides EVERY
 inherited virtual function with the same signature,
 even if there are several such, inherited from
 *unrelated* classes due to multiple inheritance.

This means it is possible to have a single implementation for
multiple independent virtual functions.

For instance, the following program works and produces
three "D"s as output:

#include <iostream.h>

struct B {
  virtual void f() {cout << "B\n";}
};

struct C {
  virtual void f() {cout << "C\n";}
};

struct D : public B, public C {
  void f() {cout << "D\n";}
};

main() {
D* d = new D;
C* c = d;
B* b = d;

d->f();
c->f();
b->f();
}

The thing that bothers me about this (apart from having to
change my understanding) is that this sort of thing could
be quite dangerous, because one may end up unintentionally
overriding functions from imperfectly understood base classes.
Since the compiler will not complain about ambiguity,
it is difficult to track down this kind of problem.

I have not seen any rationale for this decision in the ARM.
Perhaps I missed it?  Could some one please point me to
a source for an explanation?

Satish Thatte
___________________________________________________________________

Mathematics and Computer Science Department Tel: (315) 268-6521
Clarkson University    FAX: (315) 268-6670
Potsdam, NY 13699-5815       Internet: satish@sun.mcs.clarkson.edu