Brian Long (www.blong.com)
This month Brian Long looks in more depth at the OOP support offered by Kylix's compiler and IDE.
This article first appeared in Linux Format Issue 31, September 2002.
Click here to download the files associated with this article.
If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.
Last month we saw the syntax that allows us to make use of encapsulation, (single) inheritance and polymorphism whilst creating classes in Kylix. We also saw how to create properties to provide a convenient form of data hiding. This month we'll continue our exploration into Kylix's OOP support.
Last time we saw that polymorphism allows you to treat objects from any part of a given class hierarchy branch in a generic fashion and still get the correct behaviour from method calls that have been re-implemented in some or all of the inherited classes.
The idea is to simplify programming by treating different objects as if they are all the same, but still get correct behaviour from method calls. The point is that for this to work you have to mark all the methods that should exhibit this "correct", late bound behaviour with the reserved words virtual or override, otherwise the compiler will try and optimise the calls using the type information provided (the common base class) and thereby cause "incorrect" methods to execute.
In the example we used last month we had a small class hierarchy of cars based on a generic TCar base class that inherited from TObject (see Figure 1). In this example the TCar class declared a polymorphic method (or virtual method) called Drive, which simply displayed a thoroughly generic driving message. Both descendant classes (TMorrisMinor and TPorsche911) override this method to provide a suitable driving message for the given car type.
Figure 1: Our class hierarchy from last month
It is intended that all car classes will re-implement this method to provide appropriate driving messages. As such, the generic implementation is a bit pointless since it is never intended to be called directly and should not be called by any descendant class version of it.In such a case we can declare the method as abstract in the class definition. This means that we no longer have to supply a dummy implementation just to keep the compiler happy. We can declare the method in the base class and then all descendant classes will have to override the method and provide an implementation of it.
On the disk this month is a new version of last month's project called ObjectsEg2.dpr. In the unit that defines the car classes (ObjectsEg2CarClasses.pas) the base class TCar has been modified to look like this (note the reserved word abstract):
An abstract method by definition has to be virtual, otherwise it runs a high risk of being called. If an abstract method is called you get an EAbstractError exception raised. However, if this was to occur due to some code mistakenly constructing an instance of the base class (which is not intended to happen, as it represents a generic common base for all the real car classes) the compiler generates a warning: Constructing instance of 'TCar' containing abstract method 'TCar.Drive'.
An example of a useful polymorphic method is Assign. This provides a mechanism to copy the contents of one object to another. In Kylix, if A and B are variables that refer to objects you can't just write something like: A := B;
That won't copy the second object's data to the first; it will simply copy the address of the second object and store it in the first object reference variable. This means that A and B will both refer to the second object and you lose access to the first object (unless another variable still refers to it).
Instead, you should use Assign:
Assign is a virtual method that is intended to be overridden to cater for all the data that needs to be copied. It is defined in class TPersistent, a TObject descendant which is a base class for objects that can copy be stored in and retrieved from a stream. Each class that needs to support being copied to should override Assign. The code should check that the object being copied to it (the source object) is of a suitable type and then copy the data items across as appropriate. If the source object is not recognised as a suitable type Assign should defer to its inherited version.
The idea is that potential destination objects implement support for the source objects that can be copied to them. Note that whilst the programmer always calls the Assign method, there is another method that may be called upon to help the object copying process.
If it is not feasible to override the Assign method of a potential destination class to understand about a new source class (because, for example, you cannot modify the source code, then you can turn the tables around and override the protected AssignTo method. AssignTo is called by TPersistent.Assign if the source object was not recognised. AssignTo allows you to implement the object copying semantics in the source class instead of the destination class, where needed.
To allow object copying between our car objects we can use the following simple implementation (this modified version of the class can be found in the ObjectsEg3.dpr project on the disk). Note that in this case there is no need to implement AssignTo.
A statement such as Porsche.Assign(Morris) will copy the Parked and Speed properties from a TMorrisMinor object to a TPorsche911 object (if that's what you wanted to do).
Whilst it is not necessary to understand how polymorphism works in order to benefit from using it, it might help clarify things if you understand the underlying mechanism it uses.
If you recall from last month, the compiler normally does early binding on method calls. This involves it identifying where the method will be placed in the generated executable and translating the call into a machine code instruction to jump to that specific address. It uses the type of the object reference to help calculate this direct jump address.
However, if the compiler finds a method that has been marked with the virtual or override directive it knows it is dealing with a polymorphic method that needs to be late bound; in other words the specific address to jump to should be calculated at runtime after examining the actual type of object behind the object reference. After all, the type could be the one specified in the object reference definition or could be any class inherited from it; for example a TCar variable could actually be referring to a TMorrisMinor or TPorsche911 at runtime. The method that should be executed may well be different for the different possible classes.
To accommodate these requirements the compiler builds a Virtual Method Table (VMT) for each class. A VMT is a sequential list of addresses of all polymorphic methods in a class (an array of code pointers), which will be the same for each object instantiated from the class, so each object of a given type shares the same VMT.
Note that the VMT contains all polymorphic method addresses (both those defined in the class and those inherited from its ancestor class). Each descendant class will have a VMT at least as large as the ancestor class's. In Kylix every single class has a VMT since the base class TObject defines virtual methods.
The effect of the virtual directive on a method is to add a new entry (or slot) to the VMT for that class containing the address of the method. Each inherited class will contain the address of that method in the same slot.
The override directive is used to redefine a polymorphic method. The compiler replaces the address in the VMT slot for the inherited method with that of the new method (so using override does not change the size of the VMT).
You can see the layout of the VMTs for our three classes and their immediate ancestor in Figure 2. The first one shows TPersistent, which defines three virtual methods (AssignTo, DefineProperties and Assign). TCar overrides the Assign method and introduces two new virtual methods (SetTopSpeed and Drive). TMorrisMinor and TPorsche911 override the Drive method but add no new virtual methods, so their VMT size is the same as that of TCar.
Figure 2: Our class VMTs
If you're that way inclined you can see the VMT whilst your program is running in the CPU Window (View | Debug Windows | CPU Window). This presents a machine level view of what's going on in your application, complete with disassembly, memory dump, CPU registers, stack and flags.
Figure 3 shows the TMorrisMinor VMT on display in the memory dump pane (bottom left). The addresses of the five methods shown in the VMT illustration in Figure 2 are listed (they begin at address $80B4160). The selected VMT entry (address $809B574) is for the fifth virtual method, TMorrisMinor.Drive. You can see a disassembly of this method in the disassembly pane (the largest pane).
Figure 3: Looking at the TMorrisMinor VMT in the CPU Window
Many developers prefer to stick at the Object Pascal level but if you want to get low down and dirty you can. Covering the CPU window is out of the scope of this tutorial series but if you want to know more about how it can be used and what you can do with it, there is an online article on the subject. The article is called Debugging With More Than Watches And Breakpoints (or How To Use The CPU Window) and can be found at http://www.blong.com (click on the Articles link in the left hand frame, then click on the Borland Kylix articles link in the right hand frame).
To complete the subject of polymorphic methods we should say a few words about dynamic methods. These are declared using the reserved word dynamic instead of virtual but provide exactly the same polymorphism. The only difference between virtual and dynamic is the way the polymorphism is implemented.
Virtual methods are implemented using VMTs. When you inherit from a class, your new class VMT is at least as large as that of your ancestor. In the case of a large class hierarchy with many polymorphic routines declared in the base classes, all the inherited classes will have large VMTs. All these VMTs will increase the size of your executable, but they provide a quick way for late binding to operate (a simple indirect jump through a VMT slot).
Dynamic methods are implemented using DMTs (Dynamic Method Tables). These are similar to VMTs but a class DMT only contains addresses of dynamic routines introduced or overridden in that class. This means DMTs are much smaller than VMTs, which may have an impact in a large class hierarchy. However, the downside is that they aren't as quick to enable a polymorphic routine to execute. When a dynamic method of an object is called the DMT of that class is checked for the address of the method. If it is not found the ancestor class is checked. If it is still not found the ancestor's ancestor class is checked. This iteration continues up the class hierarchy until the address of the dynamic method is found.
In summary, virtual methods are quicker to execute but take up a bit more space, whereas dynamic methods are more space efficient by are slower to invoke.
A useful application of object principles can be taken advantage of in the Kylix IDE. Writing your own classes for use in your applications is one thing but when you are developing a Kylix application the IDE creates form classes for you. By default, each form you create inherits from the TForm base class, but with a little know-how you can develop a variety of customised base classes for your forms to allow common behaviour and attributes to be inherited into fresh forms instead of having to replicate it repeatedly.
When you create a new form in Kylix it generates a form class that looks like this:
As a simple first attempt at inheriting from other form classes you might try simply replacing the base class TForm with another TForm descendant class you have created. This will certainly work with respect to methods that you add in your custom form ancestor class, but you will have a big problem with any changes you've made to form properties and components you've added.
All that cosmetic stuff is managed through the form file that is linked into the executable and if you have two form files for a single form (the custom ancestor form file and the form file for your new form) things tend to get somewhat messy.
Instead you should make use of the IDE support for Visual Form Inheritance (typically abbreviated to VFI) which makes things work rather smoothly. Let's take an example scenario. Perhaps you know you will be making several applications that require their forms to have common behaviour. For example, each application form should:
To make a form that can be used as an ancestor we start by making a form as normal. A sample form demonstrating this behaviour is on this month's disk as BaseFormU.pas and can be seen as it is at design-time in Figure 4.
Figure 4: The base form
The event handlers used to implement the form's behaviour look like this:
If the form is to be inherited from in multiple projects then we need to design the form and install it in the Object Repository. The Object Repository is a mechanism that allows you to store forms, dialogs, data modules (more appropriate in the commercial versions of Kylix) and projects to be re-used at later points. When you choose File | New... in Kylix you are presented with a dialog (Figure 5). The default page, New, contains wizards for creating components, shared objects, units, applications and so on. The Forms, Dialogs and Projects pages contain the items stored in the Object Repository.
Figure 5: The Object Repository dialog
To add your own form to the Object Repository, right-click on the Form Designer and choose Add to Repository... and fill in the dialog (Figure 6 shows our example form being added). Note that you can add your form to any of the existing Object Repository pages (by selecting one from the Page combobox) or you can add a new page (by entering its name).
Figure 6: Adding a form to the Object Repository
Once the form is in the Object Repository you can make use of it. Make a new project (File | New Application) and then invoke the Object Repository dialog (File | New...). Select the appropriate page (Forms in this example) and you will see your form represented by the icon you chose (see Figure 7).
Figure 7: Inheriting from a form in the Object Repository
You can right-click on the list view control containing the icons to produce a context menu that allows you to change the way they are displayed. Figure 8 shows the Object Repository displaying details for each item, rather than just large icons.
Figure 8: An alternative view of the Object Repository
You then have three choices as indicated by the radio buttons at the bottom of the dialog. You can either add a literal duplicate of the form to your project, or a form inherited from it, or you can add the original form itself. For VFI you should inherit from the form in the Object Repository. This produces a new form class defined like this:
More importantly, the form already looks just like the base form, with all the same components, property settings and behaviour. You can modify any of the components already on the form, or add/change event handlers as you like. When you make an event handler on an inherited form, say for the OnCloseQuery event of the form, the IDE generates code like this:
This allows the inherited functionality to be invoked as well as the new code you need to add. If you want to replace the inherited behaviour, simply remove the call to inherited.
Using this VFI approach allows you to create forms with common behaviour and a common look and feel in any application with ease.
By default the contents of the Object Repository are identified through a configuration file (delphi60rc in Kylix 1 and delphi65rc in Kylix 2) that lives in the ~/.borland directory. A team of developers can share an Object Repository by asking Kylix to use a configuration file in a shared directory. This is done in the Tools | Environment Options... dialog (the Directory field in the Shared repository group on the Preferences page).
You can customise what's in the Object Repository by choosing Tools | Repository... (see Figure 9).
Figure 9: Customising the Object Repository
If you don't need to make a form available across multiple projects you can still visually inherit from a form in the current project without explicitly adding it to the Object Repository. As you can see in the various screenshots of the Object Repository dialog, the page immediately after New is a page dedicated to the current project (Project1 in all case so far). This page contains icons for each form and data module in the project and allows you to inherit from them (see Figure 10).
Figure 10: Inheriting from a form in the current project
Note that this technique of VFI is worth experimenting with. A really nice effect occurs if you have the base class and the descendant class open in the IDE at the same time. If you make a modification to something in the base form (for example moving a component) the descendant form immediately reflects the change; the IDE ensures that the descendant form reflects any alterations in its ancestor.
You can also explicitly override anything from the base form in the descendant. If you move a component on the descendant form, then a similar move on the ancestor will have no effect on the descendant. The form file for the descendant form keeps track of what properties have been changed from what was inherited to make sure things work appropriately.
One last point - if you make a change in the descendant form that stops changes in the ancestor from rippling through and you decide it was a mistake, you need to know how to undo the overridden property or properties.
To reset a component to inherit all its properties from the ancestor and override none of them, right-click on the component on the Form Designer and choose Revert to Inherited. To reset a single property the value in the ancestor form, right-click on the property in the Object Inspector and choose Revert to inherited.
This instalment has looked in more details at the OOP support in Kylix's language and also in the IDE. Next month we'll turn our attention to something else. In the meantime you can find a number of tips and techniques regarding Kylix on the Web; navigate to http://www.blong.com, click on the Articles link in the left hand frame, then click on the Borland Kylix articles link in the right hand frame and choose Kylix Tips From Under The Hood.
Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware.
If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.
Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi 2000 award.
Back to top