Virtual Functions

Chapter: Virtual Functions

The Scheme-- object system provides an elegant and clear virtual function mechanism. Understanding the idea of virtual functions may be best accomplished by first exploring a simple example:

Consider the following hierarchy of classes where <A-class> is a superclass of <B-class>:

(define <A-class>
  (class ()
    ()
    (<object>)
    ([height (method () (printf "tall~%"))]
     [A-height-this (method () (call height this))])))

(define <B-class>
  (class ()
    ()
    (<A-class>)
    ([height (method () (printf "medium~%"))]
     [B-height-super (method () (call height super))])))

Now observe the following results:

> (define a (<A-class>))
> (define b (<B-class>))
> (call height a)
tall
> (call height b)
medium

So far these are the results one would expect, we are simply calling the height methods of each instance. Now consider this:


> (call A-height-this a)
tall
> (call A-height-this b)
medium

The first call gives the result we would expect, but what about the call (call A-height-this b)? Let's trace through the calling sequence:

  1. The method A-height-this is called on the object b, which is of class <B-class>.

  2. Since <B-class> does not override the method A-height-this, the call is delegated to the superclass <A-class>.

  3. The A-height-this method is called, which in turn makes the following call, (call height this).

  4. Because the variable this always refers to the original object, which in this case is b, the height method of <B-class> is called.

  5. The height method of <B-class> prints the string "medium".

The key to the virtual function mechanism is in step 4. Because this is always bound to the instance from the originating call (step 1), the height method as defined in <B-class> is called rather than the height method in <A-class>.

This makes sense because the object we are calling is b, and it should maintain the behavior of an instance of class < B-class>, even if it called from a superclass. When we override a method such as height we want calls to this to refer to the overriden method. In this example, we are using height as a virtual function. We can think of height as "virtual" because it takes on differing behavior depending on the instance type of our original object.

In the exmaple

> (call A-height-this a)
tall
> (call A-height-this b)
medium

height takes on two different behaviors depending on whether we call instance a or b.

Q. 4
What would happen in the example:
> (call A-height-this b)
if this referred to the local class definition?


Q. 5
Suppose we want to use A's height method in a B object. Is this possible?


In Scheme--, we get virtual function behavior whenever we use the keyword this. Any method g can be virtual because the result of (call g this) from within a base class depends on whether any derived classes override g. Note that virtual behavior only becomes an issue when the original call invokes a method which in turn calls another method.

The following diagram illustrates the general case of virtual function calls. Arrows indicate the passing of calls between class instances.

In C++ the keyword virtual is required to indicate that we want the function to be overriden in calls from the superclass. That is not necessary in Java.

Look at the file to see how the above Scheme example might be implemented in C++.

If you'd like you can copy the file cexample.cpp into your directory and compile it with the command
g++ cexample.cpp -o cexample, and then run the executable to see the results for yourself.

Q. 6

What is the output of the program if height is not declared to be virtual?


Now consider a similar but slightly different example:

(define <A-class>
  (class ()
    ()
    (<object>)
    ([weight (method () (printf "heavy~%"))]
     [A-height-this (method () (call height this))]
     [A-character-this (method ()
	(call height this)
	(call weight this))])))	

(define <B-class>
  (class ()
    ()
    (<A-class>)
    ([height (method () (printf "medium~%)")]
     [B-height-this (method () (call height this))])

(define <C-class>
  (class ()
    ()
    (<B-class>)
    ([weight (method () (printf "feathery~%"))]
     [C-height-this (method () (call height this))])

Notice that <A-class> no longer has a method for height. Additionally note that each class has a method which calls height this. We have also added a method called weight. Consider the following results:

> (define a (<A-class>))
> (define b (<B-class>))
> (define c (<C-class>))
> (call height b)
medium
> (call B-height-this b)
medium
> (call weight a)
heavy
> (call weight b)
heavy
> (call weight c)
feathery

Now here are some calls which invoke calls to the "virtual" method height:

  1. (call C-height-this c) ==> medium. This example results in a call to height as inherited from B-class. height is treated as a virtual function of a sort because height is not explicitly defined in <C-class>.

  2. (call A-height-this b) ==> medium. The method A-height-this is inherited and results in a call to height as defined in the subclass <B-class>.

  3. (call A-character-this b) ==> medium heavy. Again the method is inherited and results in two calls. The call to height is as in number 2 above, causing the string "medium" to be printed, and the call to weight is handled by <A-class>.

And now we demonstrate using both height and weight as virtual methods:

> (call A-character-this c)
medium
feathery
> (call A-character-this a)

Error in call: Bad Method: height.
Type (debug) to enter the debugger.
>

Note that when we try to invoke (call A-character-this a) we get an error. This is because a is of type <A-class> and thus has no implementation of height. However, even though <A-class> itself has no implementation of height it can still call it virtually through the self referential object this. The definition of the methods A-character-this and A-height-this within <A-class> depend on height being implemented in some derived class. This is exactly what happens when we call object c: the height method is implemented in the derived class <B-class>.

Borrowing terminology from C++, we say that the method height is being used as a pure virtual function. This is a more specific case of virtual functions in which the superclass calls a method self-referentially for which it has no definition. The method is "purely virtual" because its implementation only exists in derived classes. In C++, we indicate that a function is pure virtual by declaring the function to be virtual (as before) and then setting its value to be zero. For example we would define < A-class> like this:


class A_class {
public:
    void weight() { cout << "heavy\n"; }
    void A_height_this() { this->height(); }
    void A_character_this() {
        this->height();
        this->weight();
    }
    virtual void height() = 0;
};

Notice that in C++, we must explicitly declare every pure virtual function we would like to call from the superclass.

Click for some more notes on C++ and virtual functions.

Now let's consider a concrete example using virtual functions. We return to our object oriented graphics system as developed in preceding sections.

Suppose we wish to add a method at the top level which does automated color changes, providing an animated color effect for each object.

Note that the <shape> class includes a method called animate-hsv. This method takes one argument steps which specifies the number of color steps to animate. The method consists of a loop which iterates steps times, and on each iteration a new color is computed and applied to the object. Open your solutions to Exercise 6, then try out this new method:

(init-graphics)
> (init-graphics)
> (define sq (<square> 2.0 2.0 3.0))
> (call animate-hsv sq 100)

Notice how the colors change.

Let's look briefly at the definition of the animate-hsv method:

(animate-hsv
  (method (steps)
    (let loop ((i 0))
      (when (< i steps)
            (call color this
                  (hsv->xcolor (exact->inexact (/ i steps)) 1.0 1.0))
	    (call draw this)
            (wait 50)
            (loop (add1 i))))))

The body of the loop calls the color method with the newly computed color, calls the draw method of the shape, and then waits 50 milliseconds before continuing. Note that the draw method is purely virtual, because it not defined at all in this class! The color method is used as a virtual function because it is redefined in the derived class <filled-shape>. Filled shape overrides color in order to set both the fill and outline colors. For example try the following and note the color behavior:

> (define sq (<square> 2.0 2.0 3.0))
> (call draw sq)
> (call width sq 0.2)
> (call outline-color sq "pink")
> (call animate-hsv sq 10)
Note that because we call the color method as a virtual function, both the outline and fill colors change at once.

Q. 7
What would happen if color was not virtual?



Exercise 7

Write a new class <framedsquare> which has for its superclass your already defined square but which overrides square's color method by its own color method which only changes the fill color. If you do this correctly, then if you type:
> (define s (<framedsquare> 5.0 5.0 6.0))
> (call width s .5)
> (call outline-color s "pink")
> (call draw s)

you should see a large black square with a pink border. Now, however, if you type:

> (call animate-hsv s 100)

you will see the interior of the square going through its myriad colors, but the frame will remain consistently, obdurately, wilfully, stubbornly, obstinately pink.

You obviously have to use the new version of <filled-shape> from Exercise 6 (otherwise outline-color is not defined). Save everything in the file ex7.ss.




rms@cs.oberlin.edu