Upward and downward polymorphism in object oriented languages.

Let's say, for the sake of argument, that I have defined a (virtual) base type 'bird' containing a pure virtual function 'sing', so every (non-virtual) derived type of bird must provide an implementation of that function.

I derive three classes, 'wren' where it results in fairly complex twittery noises, and 'crow' where it results in relatively uniform 'caw' sounds, and 'lyrebird' which selects from a long list of other birdcalls, cell phone ringtones, doorbells, chainsaws, deisel engines, etc etc etc...

I then make a list of birds. I want to iterate down that list calling 'sing' for each bird. This is an example of 'downward polymorphism' because I want members of the base class to invoke the behavior of the child class.

The same thing is true if I provide an implementation of 'sing' in the base class, which plays 4 minutes and 33 seconds of silence. When I iterate down my list of birds, where there are no virtual birds, it is no more helpful. Instead every bird is silent. This is 'upward polymorphism,' where any member of a derived class behaves as a member of its parent class, ignoring implementations of functions overridden in the derived class.

I happen to think that upward polymorphism is almost useless. What's the point of having a class whose behavior varies according to subtype if you then have to keep track of what subtype some particular instance is in order to get its correct behavior?

I wind up implementing downward polymorphism myself. This means a *variable* field in the base class containing the function pointers, an implementation (for the base class) that transfers control through that pointer, and a constructor for each derived class that writes its own function pointer into that variable (via a series of nasty binary cast operations on copies of the 'self' and function pointer which my compiler has to be beaten over the head to allow).

In other words, using OO and polymorphism in C++ almost exactly the same nasty, unsafe way I use it in C. The only advantage, from my point of view, is the ability to provide the interface and calling conventions people expect if it's ever to be integrated into the same program with various libraries. The disadvantage is that c++ compilers require more beating over the head, ignoring more warnings, etc, to be tortured into allowing it.

Why isn't there a nice, safe, compiler-supported implementation of downward polymorphism? Is it really as dangerous and bizarre as the reactions of implementors and compilers seem to imply? Is the way I'm doing it by hand really better than just providing a language construct, or even a template library, that does it automatically?

Honest to god, transferring control through a pointer isn't significant overhead.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

vtables verses pointers of array of functions

vtables don't work how you describe if I read you correctly, basically objects have a pointer to the vtable of their class.

This is very inefficient on modern processors, using arrays of pointers to methods indexed by class or type is more efficient, and using call site optimized dispatch as in Self and Sun Java HOTSPOTS is more efficient again.

I have never heard of upward or downward polymorphism do you have any references please ?



***

***

I used the terms "upward"

I used the terms "upward" and "downward" because they seemed to be good words for what I was talking about, not because others have been using them. I was just trying to miss words people are already using for something else. IOW, trying to avoid confusion. Sorry for having failed to do so.

It has been my understanding that each object contains a pointer to the method table of its type. So every "crow" object in the world should have a pointer at the "crow" method table. So when "sing" is called, it looks up "sing" in the crow method table and gives a 'caw', right?

Except it doesn't. If that object happens to be part of a list of birds, somehow it fails to use its own method table.

I think that's crazy.

So I define the 'bird' class as containing a function pointer 'S', initialized to NULL. S is not inside the method table; it is inside each and every allocated 'bird' object.

I make the default method for 'sing' (the one that's in the method table for 'bird') check the object's 'S' pointer. And then execute some default if it's still NULL, and execute whatever it finds at that pointer otherwise.

And when I'm creating a 'crow,' the 'crow' constructor overwrite its 'S' pointer to the only 'sing' function that I EVER want called when it's a crow.

I don't understand why calling the 'bird' method is ever desirable. And because each object contains a pointer to its OWN type's method table, I don't understand why using the function pointer found there is hard.

Even though every 'crow' object contains a pointer to the 'crow' method table, under some circumstances it still calls the 'bird' method instead.

When is that not a mistake?

Ergh.

My problem is it's a waste of effort and makes c++ polymorphism no safer or easier than doing the same thing in C.

But it's worse than that, because I'm also allocating an extra pointer in the parent class and indirecting through it, where if the system simply used the vtable that the 'crow' object points it, that overhead wouldn't be necessary. I don't want each object carrying around its own method table; I want each object using the method table of its own type, all the time, instead of the method table of its parent type.

And I think that ought to be EASIER to implement in a language, so I don't get the problem.

virtual (polymorphism) and non virtual (method call)

I can only think you are talking about virtual and non virtual method calls. Non virtual are called directly like normal functions where as virtual methods are dispatched.

See the generated vtables and calling code :-

https://godbolt.org/z/66EKz8

I'm aware of that.

I know how they're doing it. I just think it's wrong. My question isn't what are they doing or how does it work, my question is why did they think it was a good idea to do it that way?

why oh why oh why

my question is why did they think it was a good idea to do it that way?

Ray, the "they" is the creator of the C++ mess, and his name is Bjarne Stroustrup. His thinking (giving him the benefit of the doubt, and based on his tome called "Design and Evolution of C++" or something like that) was that C++ could be a language to replace C, but with objects and object-orientation, because other people were getting attention at the time for using those terms. There were two fundamental problems with his assumption:

  • Bjarne didn't understand objects or OOP, and
  • C programmers hate the disorderly mess of C++, but Bjarne thought he could attract programmers by making C++ roughly as fast a C but with way more complexity!
  • Because he was convinced of his rightness, and was pitching his language to C developers at his company, he wanted C++ objects to be as fast as C function calls, so he disallowed polymorphism unless you added keywords and various nonsensical incantations (e.g. "virtual" and "= 0", among others).

    So robin.sing() and crow.sing() and bird.sing() are all the same function call.

    Because performance.

    As a result, anyone who tries to discuss object orientation in any form, and also accidentally mentions C++, is automatically suspect.

    Onward and upward

    Not to discourage criticism of C++, but I do recall once, in playing around with a multi-method mechanism, conceiving something not entirely unrelated (there's a common thread, anyway).

    Unusually for me, this was a type-based exploration. The idea was that to evaluate a list (surprise: it was a Lisp language), you work out the types of all the elements of the list, and then you query all the objects to see if any of them has a suggestion on how to handle a list of things with those types. Any suggestion from any of the elements may be used — because, frankly, the alternative would be a bottomless pit of complexity in choosing strategies for how to choose between suggestions by multiple elements. For this to work, it seems, you have to write all these methods in such a way that any method that claims to match the element-types is always an acceptable behavior for the system, because there's apparently no guarantee that a specialized method will be selected instead of a general one. I think this may have been at about the same time I was designing the number module for Kernel, so the type example that would likely have most readily come to mind would have been integer-rational-real-complex-quaternion.

    [incidental correction: no, it was several years before the number module of Kernel; though I think I had numerical types in mind anyway]