Electronic Ink
TECH NOTES   ADVANCED LINGO   VIRTUAL FUNCTIONS  
Home
Products
Tech Notes
Contact Us

Using C++ Style "Virtual Functions" in Lingo Objects

Foreword

This article details a technique for creating hierarchies of parent/child scripts in Lingo that contain C++ style "virtual functions". If you are unfamiliar with the C++ concepts of inheritance and virtual functions, and/or haven't used "ancestors" much in parent/child scripting, you might have to read it a few times to fully understand the technique. Furthermore, you might need to use this technique for a while (without really knowing why) until the reason it's so powerful and useful suddenly dawns on you. Trust me, it's useful. Try this technique, and it will help you.

Hopefully, those readers who are familiar with the power of C++ virtual functions will be as excited and relieved as I was when I finally figured this out. It makes "inheritance" in Lingo much more useful and powerful than it otherwise was.

NOTE: all the code examples in this paper use the Director 5 and later new keyword instead of the Director 4 birth syntax, which is now obsolete. When using this code with Director 4, subtitute the birth keyword everywhere you see a new keyword in the code examples.

Virtual Functions in Lingo Objects

The concept of "virtual functions" goes hand-in-hand with the idea of inheritance (or "ancestors" in Lingo). The basic idea (in Lingo-speak) is to provide a mechanism for an "ancestor" object to call scripts in its own "descendants" that override their ancestors' scripts. This allows the descendants of an object to change its behavior in a way that's transparent to the ancestor itself.

Those of you who are shaking your heads saying, "but wait, Lingo does this already," are partly right. When a script is called from outside the object itself, the topmost descendant's script gets called first, because it's the first script that Lingo finds as it moves down the chain of ancestors.

But this doesn't work when scripts are called from within the object itself. Consider the following flawed parent scripts, foo and bar, where foo is the ancestor of bar:

-- script "foo" (version 1)

on new me
   -- automatically add myself to the actorList
   add the actorList, me
   return me
end

on stepFrame me
   doSomething me
   doSomethingElse me
end

on doSomething me
  -- do some type of operation you need to do every frame
end

on doSomethingElse me
  -- do another operation you need to do every frame
end


-- script "bar" (version 1)

property ancestor

on new me
   set ancestor = new(script "foo")
   return me
end

on doSomethingElse me
   -- the bar object wants to something even more than doSomethingElse
   -- we're hoping this script will override the doSomethingElse
   -- script of my ancestor

   -- first, let my ancestor "foo" do the normal thing
   doSomethingElse ancestor

   -- then do my own thing
   doMyOwnThing me
end

on doMyOwnThing me
   -- do something extra that only a "bar" can do
end
The foo object provides some basic behavior, such as adding itself to the actorList so it gets a chance to do something at every frame, and providing two default actions that will happen when stepFrame is called: doSomething and doSomethingElse.

The bar object is supposed to behave exactly like the foo object, except that its doSomethingElse script does a little bit more than its ancestor. First, bar, tells its ancestor foo to doSomethingElse as usual, then it tells itself to doMyOwnThing. The scripts above won't work for two reasons:

  1. The doSomethingElse call in the stepFrame script of the foo object will never call the doSomethingElse script in bar. Foo has no idea that bar even exists, so it's impossible for foo to call any scripts within its "descendant", bar.

  2. The actorList doesn't know that bar exists either; it only has a reference to the foo object that's contained in the ancestor property of bar. This means that even if we try to add a stepFrame script to bar, it will never be called on stepFrame like it should be.
To get the behavior we really want, we need to make the foo "ancestor" object smart enough to know about its "descendants". It doesn't need to know about their details: it just needs to "plan ahead" for any future descendants that might override parts of its functionality:
-- script "foo" (version 2)

property _me

on new me, descendant
   -- set the property _me to point to either 
   -- myself or my last descendant
   if not objectP(descendant) then set descendant = me
   set _me = descendant

   -- add my *last descendant* to the actorList, instead of just "me"
   add the actorList, the _me of me

   return me
end

on stepFrame me
   -- give my descendants an opportunity to override what I do
   -- by using "the _me of me"

   doSomething the _me of me
   doSomethingElse the _me of me
end

on doSomething me
  -- do some type of operation you need to do every frame
end

on doSomethingElse me
  -- do another operation you need to do every frame
end
The new script of foo now takes an additional parameter: a reference to its final descendant. Now any time a foo object needs to call one of its own functions, it uses the _me of me instead of just me (Within the last ancestor, you can actually just use the syntax _me instead of the _me of me.)

If foo has any descendants, this technique allows the descendant objects to get first crack at running scripts that overrides the behavior of foo. If foo has no descendants, the _me of me is the same as me, so things work normally. If none of foo's descendants override the script being called, the script within foo will get called, with just a little bit more overhead than before.

A beneficial side effect of this is that in the new script of foo, the actorList also gets a reference to the last descendant of foo, instead of just the foo object. So if any descendant ever wants to override the stepFrame script, it will be able to do so with no trouble at all.

Here's a bar script that works properly in combination with the foo script above:

-- script "bar" (version 2)

property ancestor

on new me, descendant
   -- if there's no descendant, just set the descendant to myself
   -- and pass it on down the chain of inheritance
   if not objectP(descendant) then set descendant = me
   set ancestor = new(script "foo", descendant)
   return me
end

on doSomethingElse me
   -- the bar object wants to something even more than doSomethingElse
   -- this script will override the doSomethingElse
   -- script of my ancestor

   -- first, let my ancestor "foo" do the normal thing
   doSomethingElse ancestor

   -- then do my own thing
   -- if I ever have any descendants, they will be able to
   -- override "doMyOwnThing" as well, because I'm using "the _me of me"!
   doMyOwnThing the _me of me
end

on doMyOwnThing me
   -- do something extra that only a "bar" can do
end
This bar script does what we originally intended bar to do. The bar object behaves just like a foo, except it does its own special thing (doMyOwnThing) when it's time to doSomethingElse. All the other messy functionality relating to the actorList and the stepFrame script is handled properly by bar's ancestor, the foo object.

Now this is how the object-oriented inheritance is supposed to work.

Avoiding Circular Object References

One potentially dangerous side-effect of using this technique is that it creates a circular object reference, which means that the object stores a reference to itself (that's what the _me property is for). This means that Director won't automatically get rid of the object until the _me property no longer contains a reference to the object. If you create and dispose a lot of objects, this can cause a significant memory leak in your project! Consider the following code snippet:

-- example script that creates a LEAK!
set x = new (script "foo")
doSomething x
set x = 0
If x was a normal Lingo child object, calling set x = 0 is all you'd need to do to get rid of the object. However, because in this case, the _me property of x still contains a reference to the object, the object will stick around in memory until Director quits. This, of course, can be A Very Bad Thing.

What we need to do is add a condemn routine to all of our objects that must be called right before setting the object reference to zero. This condemn routine will set the _me property to zero, and therefore allow Director to get rid of the object.

on condemn me
     -- set the _me property to 0 to avoid circular object references
     -- and prevent memory leaks. Only call this routine right before
     -- you want to get rid of the object!
     set _me = 0
end
With a condemn routine added to the foo parent script, the following example code will cause the object to be disposed properly:
-- example script that doesn't leak
set x = new (script "foo")
doSomething x
condemn x
set x = 0

Rules for Using Virtual Functions

Here is a set of rules for using the technique demonstrated above to create smart and powerful hierarchies of objects. It's as complete a set of rules as I've been able to deduce from my use of this technique so far:
  1. The final ancestor ("base class" in C++ terminology) must have a property called _me which stores a reference its final "descendant" in the hierarchy.

  2. The new scripts of all inherited objects must pass either a reference to themselves or their descendants downward to the new scripts of their ancestors.

  3. Give birth to your ancestor before doing anything else in a new script. (Gee, Freud would have a field day with this terminology!)

  4. Always use the _me of me to call scripts within the same object, or scripts that could be overridden by an object's descendants.

  5. Always use ancestor to explicitly call the scripts of your ancestors. Never use the syntax "the ancestor of me" to do this, since it won't call the scripts you expect in objects that have long chains of descendants.

  6. Be very careful in using the value of _me during new scripts. Remember that _me won't refer to a useable, complete object until after all new scripts are done executing. Specifically, none of an object's "descendants" will have their ancestor property set up yet, because the ancestors are still in the middle of being created. It appears to be okay to store the value of _me for later use during a new script (such as adding it to the actorList) but otherwise use _me with extreme caution within new scripts.

Feedback

At the time this summary was written, the technique has been used successfully in a number of class hierarchies and projects for over a year. I am very interested in hearing about corrections, caveats, and extensions to this technique. Please e-mail any feedback to me and I will integrate the information into future versions of this article.

References

  1. Booch, G. 1994. Object Oriented Analysis and Design With Applications. Second Edition. Redwood City, CA: Benjamin/Cummings.

  2. Stroustrup, B. 1991. The C++ Programming Language, Second Edition. Reading, MA: Addison-Wesley.
©1994-2024 Electronic Ink. All rights reserved.