Multiple Inheritance for Delphi

Although complex, multiple inheritance can be handy from time to time. Roland implements multiple inheritance for Delphi 2.0 using the TLead class. Al Williams then adds a note on aggregation, containment, and derivation.


October 01, 1996
URL:http://www.drdobbs.com/database/multiple-inheritance-for-delphi/184409973

October 1996: Multiple Inheritance for Delphi

Multiple Inheritance for Delphi

Deriving from more than one class

Roland Kaufmann

Roland is a student at the Norwegian School of Economics and Business Administration in Bergen, Norway. He can be reached at [email protected].


For a variety of reasons, the designers of Object Pascal chose not to implement object-oriented concepts such as class methods, class variables, metaclasses, and multiple inheritance. To its credit, Borland has continued to extend the object-oriented capabilities of Delphi, its Object Pascal implementation. Delphi 2.0, for instance, supports class methods, class variables, and metaclasses-but not multiple inheritance. Even though multiple inheritance can add complexity, it is still useful if you want a class to extend more than one superclass, without excluding any superclass as a parent. It is also handy if you want to add some of the capabilities of another class. (This is called a "mix-in" and is often used to make classes comply to an interface.)

In this article, I'll present a technique for implementing multiple inheritance for Delphi. My approach does not rely on any compiler internals or Delphi-specific features, and therefore should be portable to other object-oriented languages with minimal effort. To illustrate this technique, I'll use the class TLead as an example that represents the leader of a software-development project. The leader must have the capabilities not only of a manager but also a programmer. Thus, TLead inherits from TManager and TProgrammer. The source code and related files for this implementation are available electronically.

Aggregating the Superclasses

Of course, if I were going to derive TLead from TProgrammer alone, I could have used Delphi's regular inheritance mechanism. All the members and methods in TProgrammer would then have been included in TLead in front of the added methods and members. I could pass a TLead whenever a TProgrammer was required, because the necessary superset would be found at the beginning of the instance.

The case is somewhat different when I want to derive from both TProgrammer and TManager, however. Only one of the superclasses may be located at the very beginning of TLead and benefit from having a TLead reference point to its members. (A reference is a pointer that follows instance syntax.) Given a reference to a TLead instance, the compiler does not provide a way to extract a reference to the other superclass. Languages that support multiple inheritance do so by providing the pointer conversion needed to access the other superclasses. One way to solve the problem is to emulate this behavior. However, I chose not to do this (see the accompanying text box entitled "Pointer Hacking Dismissed"). Instead, I aggregate the TProgrammer and TManager superclasses in TLead as if they were members of the same class, calling these members m_ProgrammerSuperClass and m_ManagerSuperClass, respectively. For now, assume that they are declared as references to TProgrammer and TManager. All objects in Delphi's new object model (instances of classes declared with the class keyword) are allocated on the heap, and object variables are merely references to the instance. Consequently, I construct the members representing the superclasses dynamically in TLead's constructor by explicitly initializing the members to the instances returned by their classes' constructors.

Since an object depends on its inherited parts being there first, a TLead cannot be a TLead without also being a TProgrammer. Therefore, I take care to initialize m_ProgrammerSuperClass and m_ManagerSuperClass before I initialize the rest of TLead.

For the same reason, I have declared the members representing the superclass as private. I want a TLead to look like an indivisible object, so users of the object can't alter parts of it. (In the worst case, a part could then be replaced by an object that was owned and later destroyed by some other code. The TLead would definitely be confused if it suddenly found that a part of it had unexpectedly disappeared!)

However, I have to make it possible to access the TProgrammer part of a TLead, so I provide a method in TLead called AsProgrammer. The only thing this function does is return m_ProgrammerSuperClass. That way, I can access the TProgrammer part but cannot replace it. I call such a method for a cast function, because it gets a reference to one class type from another. Whenever I want a TLead to assume the role of a TProgrammer, I get a TProgrammer reference to it through this function; see Example 1. I will have to use this function explicitly every time I want to do this (unless I choose to cache the reference in a temporary variable) because, from the compiler's point of view, there is no connection between TProgrammer and TLead.

Virtual Methods

If I had derived TLead from TProgrammer using regular inheritance, the TProgrammer part would have used the same virtual-method table (VMT) as the rest of the TLead object; see Figure 1(a). If I had called some of the virtual methods in the TProgrammer part, the overriding implementation of TLead would have been invoked, because TLead's VMT is used.

However, when TProgrammer and TManager are aggregated in TLead, the compiler regards them as separate objects with their own VMTs; see Figure 1(b). Hence, if I call the virtual method Work in the TProgrammer part of TLead (through a reference returned by the cast function), it is not the overriding implementation in TLead that will be called, but rather the implementation in TProgrammer, since the superclass member m_ProgrammerSuperClass is of that class.

Because I want the virtual methods in m_ProgrammerSuperClass to behave like their nonaggregated counterparts, I make them call the corresponding method in TLead whenever they are invoked. I don't want to alter the superclass behavior to call methods in one particular subclass, however. Doing that would not only prevent the superclass from functioning as a stand-alone class but also from being used as a superclass for other classes, since it only could call one subclass. Besides, in some cases I might not have any sourcemodifiable code for the superclass (if it is part of a compiled library, for instance). Instead, I extend the behavior of the superclass through a helper class that derives from it. I derive the class using the regular inheritance mechanism, so calls to the virtual methods of the superclass will appear in the helper class. I name the helper class TLeadProgrammer to reflect the fact that I'll find instances of this class only as parts of a TLead object. I put the entire declaration and implementation of TLeadProgrammer in the implementation section of the same unit where TLead is defined, since I will not be creating instances of this class outside TLead. (They would be incomplete parts of a TLead.) I create the superclass member m_ProgrammerSuperClass as an instance of this class type, but I don't change its declaration type, as I don't need to access it as a TLeadProgrammer in TLead.

In the helper class, I override all the virtual methods from the superclass TProgrammer, and provide new implementations that call the corresponding method in the subclass TLead. The helper class does not know which TLead to call, though. Its Self pointer points to the memory allocated for it rather than the TLead instance that owns it. I solve this problem by adding an extra member in the helper class called a "back-pointer." The TLead that aggregates the helper class gives it a pointer to itself upon construction. The helper class can now call the TLead through this pointer. I call this technique "forwarding." Although I don't plan to modify their behavior in TLead, I will have to forward all of the virtual methods since I may want to subclass TLead at a later time. Virtual methods that are not forwarded by the helper class are hidden and cannot be overridden in subclasses. For the same reasons, the methods in TLead that TLeadProgrammer calls must be declared "virtual." I also choose to use the same access rights in TLead as the corresponding methods in TProgrammer.

The forwarding methods in TLeadProgrammer are good places to take care of a chore of multiple inheritance-name resolution. A problem arises if I use the same symbol in more than one superclass; it is unclear which superclass I am extending by overriding the symbol in the subclass. For instance, both TProgrammer and TManager define the virtual method Work. I have to rename them to distinct symbols in TLead, such as WorkAsProgrammer and WorkAsManager, and make the corresponding forwarding methods in the helper classes call the one that is proper for the superclass whose method is being forwarded. For an example of this, examine the procedures TLeadProgrammer.Work and TLeadManager.Work in the code (available electronically; see "Availability," page 3).

Calling Ancestors

Sometimes, I override a virtual method just to add code to it, still letting the superclass do its work. For example, TLead.Work calls TProgrammer.Work to increment a private member in TProgrammer (the number of lines written). Also, if I don't want to alter the behavior now but have forwarded the method in case I want to in the future, I'd also like to make provisions for calling the superclass's implementation later. However, I cannot use the inherited keyword, because the compiler does not know that the subclass derives from the aggregated superclasses. Neither can I call the virtual method of the superclass member, as the call will be forwarded to the same function I am calling from, effectively deadlocking the thread.

Once more, I solve the problem by deriving a new class from TProgrammer using regular inheritance. In this class, I define a nonvirtual method for every virtual method in the superclass. These nonvirtual methods call the implementation in TProgrammer using the inherited keyword. That way, the calls will not be forwarded. I name this class TProgrammerCaller since its only purpose is to call methods in TProgrammer. I commonly name such classes "caller classes." All the methods bear the name of the method they are calling, but with the prefix Inherited- to reflect their purpose. Figure 2 illustrates how I handle a call to a virtual function in a superclass part.

There might be more than one subclass that needs this behavior. Therefore, I define this class in its own unit, so I can reuse it. All the methods are public since the subclasses that will be calling them, among them TLead, are not formally derived from the caller class and do not have access to protected methods.

I make TLeadProgrammer a descendant of TProgrammerCaller instead of TProgrammer so I can invoke the caller functions through the superclass member m_ProgrammerSuperClass. To make them visible to TLead, I also change the declaration type of m_ProgrammerSuperClass to TProgrammerCaller. Since TLead is a public class, its members must be declared public as well. As TLead does not require knowledge of the forwarding methods, I won't declare the member as an instance of the helper class and I'll keep the declaration of TLeadProgrammer private.

Dispose with Care

Even when I look upon an object as an instance of one of its superclasses, I want the entire object to be destroyed when I call the destructor. That is why the destructor is a virtual method: It would be meaningless to destroy just parts of an object. Of course, I want objects that are using multiple inheritance to behave this way, too; so I forward all the destructors from the superclasses to TLead's destructor Destroy, which calls the destructors of all the superclasses in opposite order of creation after it has taken down the rest of the object.

Life is not as simple as it may look, though. The destructor consists not only of the code that I write to take down the object but also of the code the compiler adds to free the memory of the instance. After the destructor is forwarded and it has taken down the rest of the object, TLead.Destroy will call the destructor of the caller class, TProgrammerCaller.InheritedDestroy, to destroy the memory of the superclass member. TLead.Destroy destroys the entire TLead object, just as if I had called it directly. But control now returns to the forwarding destructor, TLeadProgrammer.Destroy, which will move on to free memory of m_ProgrammerSuperClass.

Since memory cannot be released twice, I prevent one of the two destructors from freeing the memory. I would like TLead.Destroy to release the memory, so it could both be forwarded to and used alone. However, I override TProgrammer.Destroy with a destructor. Thus, if I call TLeadProgrammer.Destroy, it will always free the memory of the TProgrammer part. That implies that TLead.Destroy cannot release the memory if I call it from TLeadProgrammer.Destroy, but it will have to otherwise. Nonetheless, the destructors of the superclasses must be called in the correct order and not until after TLead has destroyed itself.

Although the order of destruction is important, the order in which the memory is released is not. I therefore redeclare TProgrammerCaller.InheritedDestroy to be a regular method instead of a destructor. Now I can use it to destroy the TProgrammer part without releasing its memory. TLeadProgrammer.Destroy tells the TLead object that it is going to free the memory of m_ProgrammerSuperClass by setting a flag. TLead.Destroy then checks this flag after it has called TProgrammerCaller.Destroy to take down the superclass part. If the flag is set, it knows that TLeadProgrammer.Destroy is going to free the memory when it regains control. If the flag is cleared, it releases the memory of m_ProgrammerSuperClass by calling the method FreeInstance, which is provided by Delphi.

Don't Repeat Yourself

When you inherit from multiple superclasses, it is possible that the same base class may be included more than once in the subclass object, a phenomenon known as "repeated inheritance" (see Bertrand Meyer's Object-oriented Software Construction, Prentice-Hall, 1988). Currently, this is the case with the TLead class; both TProgrammer and TManager inherit from TEmployee, and when they are aggregated in TLead, each of them brings a TEmployee part into the object; see Figure 3(a). Since these two TEmployees are separate parts of the objects, it is possible for a TLead to have two different names and to be of two different ages. The solution is to make TEmployee a virtual base class, which means that it is shared among the superclasses that inherit from it, so only one instance of it is included in the subclass; see Figure 3(b). I make base classes virtual when I know that subclasses may be used to represent a more-specific group of the objects. Leads are a specific group of employees, for example. (Another example is comboboxes, which are both editboxes and listboxes, but also a more-specific group of windows.)

The TProgrammer and TManager parts may have different views on how the TEmployee part should be initialized. However, as I am creating a TLead object, its constructor, TLead.Create, ultimately decides how the TEmployee part should be configured. Therefore, I aggregate it as an immediate member object in the subclass, and I initialize it with the correct parameters before I construct the superclasses. TProgrammer and TManager will then have to take the already-constructed TEmployee for granted. Similarly, the virtual base class will be destroyed in TLead's destructor after all of the superclasses have been taken down. None of the superclasses can do the destruction since they don't know whether other superclasses that depend on the virtual base class still remain in the object.

To work with a virtual base class, the superclasses have to make some special provisions. They have to provide a constructor CreateWithSuperClass that accepts an already-initialized base class and sets up only the rest of the superclass. For this to be possible, they must derive from the base class using aggregation (See Aggregation, Containment, and Derivation), so they can just assign their base class member to the object supplied from the subclass. I declare CreateWithSuperClass as protected since I will only use it in subclasses that aggregate the class. I want to create stand-alone objects of the superclass, too, so I have a regular constructor Create, which first initializes a base class and then passes it on to CreateWithSuperClass. It also sets a flag so that the destructor knows that the base class is not virtual and does not accidentally destroy it.

The virtual methods of the TEmployee part are forwarded to the ultimate subclass TLead. The reason is simple: If both TProgrammer and TManager override a method, which is the case with TEmployee.Work, then it is not obvious which of the two implementations I will have to call. TLead is the one that should make the decision, as is the case in TLead.WorkAsEmployee (available electronically).

Exception Handling

I have left out exception handling to simplify the code and focus on the inheritance from multiple superclasses. However, exceptions can occur almost anywhere when programming in the real world. Since an object that consists of aggregated parts is initialized in separate steps, it may only be partially constructed when the destructor is called to clean up after an exception. Therefore, I suggest you check if a superclass part is valid before destroying it, as in Example 2.

Conclusion

Although Delphi does not provide multiple inheritance, it can be emulated by using constructs already found in the language, such as aggregation, single inheritance, and virtual methods. The technique is general in that any class can be a used as a superclass. If virtual inheritance is required, then the superclasses must make some special provisions, but the base class can still be of any type.

Pointer Hacking Dismissed

An alternative to aggregating the superclasses as members is to override the NewInstance class methods for both the subclass and the superclasses, laying them out continuously in memory. Whenever I want access to the superclass part of an object, I calculate where it resides in memory from a pointer to the subclass. I could also make all the VMT pointers of the superclasses point to the VMT of the subclass, so I don't have to create a forwarding procedure for every virtual method. This would not only yield better performance, but would save the overhead of having pointers from both the superclass to the subclass and vice versa. While it seems attractive, there are also several downsides to this approach.

It requires knowledge of the internal workings of the compiler. I would have to know the size of every class to calculate the pointer, and I would have to know the layout of the virtual method table in order to fix it up. That may vary from compiler to compiler, and also from release to release.

Although efficient, the solution doesn't seem object oriented. I would have to perform a pointer calculation wherever a reference to the object as one of its superclasses was needed. If I add another superclass or another member at a later point, I would have to track down all those spots and change them, as the memory layout would be altered. The compiler is unlikely to help.

There is a problem with virtual inheritance. All parts of an object are supposed to be located continuously in memory. Since a virtual base class can only be put in front of one superclass, it is not clear which one it should precede. Other parts that depend on the base class will have to calculate a pointer to reach it, and that calculation will be specific for each subclass. Thus, I would have to create a version of the superclass part for every subclass I intended to derive from it-a serious obstacle for reuse.

The number of situations where it can be applied is limited. Not all languages allow pointer arithmetic and there are probably even fewer that let you decide where in memory an object will be created. If you change development tools, you may find you can't take your algorithms with you.

It seems like pointer hacking is not such a good idea after all.

-R.K.

Aggregation, Containment, and Derivation

Al Williams

Al, a consultant and author based in the Houston area, is a contributing editor for Dr. Dobb's Sourcebook. You can contact him at http://ourworld.compuserve.com/homepages/Al_Williams.


If you learned object-oriented programming using traditional languages like C++ or Object Pascal, you might think derivation is essential. However, if you think about it, you'll realize that derivation isn't an actual goal for an object system-it's a means to an end. Other systems achieve the same goals in different ways. In the accompanying article, Roland presents a method of forcing Object Pascal to support multiple inheritance without using derivation at all. Instead, the program uses a containment technique similar to that used by Microsoft's ActiveX objects (formerly OLE/COM objects).

An OOP language should enable encapsulation, code reuse, and polymorphism. Of course, Object Pascal allows all of these things using the class keyword. Within a class, you can protect members to achieve encapsulation. A class can derive from a different class to allow code reuse. When multiple classes derive from a common base class, you can treat pointers to the derived objects as pointers to the common base class without regard to their specific class. This is polymorphism. Notice that derivation is how Object Pascal supports code reuse and polymorphism.

ActiveX, on the other hand, doesn't really support derivation in the strict sense of the word. ActiveX is a protocol for constructing binary objects. A Pascal program might use an object created by a Visual Basic program, for example. Without source code, derivation becomes difficult. Does that mean that ActiveX doesn't support code reuse and polymorphism? Not at all.

An ActiveX object is nothing more than some code that provides at least one table of function pointers. Both the object provider (the server) and the object user (the client) must agree on what functions appear in the table, what arguments they support, and so on. This table is an interface. When you create an ActiveX object, you ask for a particular interface from an object. Objects can supply multiple interfaces.

ActiveX defines the first three entries in each interface table. These functions manage the object's reference count and allow you to learn about the other interfaces an object supports. When the reference count is zero, the object can destroy itself.

So, how does ActiveX support the three key OOP features?

Encapsulation is easy. Since clients only see a table of function pointers, all variables are private. Also, any code you don't refer to in the table is private.

ActiveX supports code reuse via containment and aggregation. In your object, you create instances of the object (or objects) you want to reuse. Then, you arrange for your interface functions to call the contained object. This is similar to the forwarding in Roland's technique. If you don't forward (or delegate) a function, then you override that function. You can also execute your own code before or after forwarding. Aggregation is similar, except you present the contained class's interfaces as if they were your own. That means you can't override or alter the base functions if you use aggregation.

In ActiveX, any two objects that have a common interface are polymorphic with respect to that interface. For example, a Car object would have two interfaces: IVehicle and ICar. Truck objects then have IVehicle and ITruck interfaces. That means you can work with both objects via their identical IVehicle interfaces. Remember, one of the predefined functions in each interface allows you to ask for other interfaces. Using that function, you could easily determine if the object was a truck or a car, and obtain the specific interface if you needed it.

Although derivation is the most common way to support OOP practices, it isn't the only way. Keep your sights on the goals of OOP, not the specific implementation of a particular language.

Figure 1: (a) TProgrammer part and rest of TLead share the VMT; (b) TProgrammer part and rest of TLead have separate VMTs.

Example 1: Getting a superclass through a cast function.

procedure TProject.Assign( AProgrammer : TProgrammer );
begin
  ...
end;
  ...
var
  ALead :  TLead;
  AProject  :  TProject;
begin
  ...
  AProject.Assign( ALead.AsProgrammer );
  ...
end.

Example 2: Only valid members are destroyed.

destructor TLead.Destroy;
begin
  ...
  if not ( m_ProgrammerSuperClass = nil ) then
    m_ProgrammerSuperClass.InheritedDestroy;
  if not ( ProgrammerSuperClass in m_Destructed ) then
    m_ProgrammerSuperClass.FreeInstance;
  ...
end;

Figure 2: Calling sequence when a virtual function is called.

Figure 3: (a) TEmployee is repeatedly inherited; (b) TEmployee is a virtual base class.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.