![]() Acrobat file (195K) | ![]() ClarisWorks 4 file (45K) | ![]() QuickView file (473K) |
Technote 1009 | OCTOBER 1995 |
This Technote answers a common question about MPW C++: Why doesn't HandleObject
support multiple inheritance? To answer that question, this Note provides a brief overview of how multiple inheritance is implemented in MPW C++.
This Technote is addressed primarily to C++ developers who are concerned about memory fragmentation.
Contents
Accordingly, Apple recommends using malloc
or the standard operator new
for allocating small objects.
For large objects, handles have some significant advantages. For one thing, they minimize RAM usage by avoiding fragmentation. Also, some developers need to continue using handles in their existing code.
HandleObject
. Objects of any class descended from HandleObject
are allocated as handles in the heap. Your program may refer to one of these objects as if it were a simple pointer; the compiler takes care of the extra dereference required. A HandleObject
is useful in Macintosh programming for the same reason that a handle is useful. The use of handles helps prevent heap fragmentation, which is critical on Macintosh computers that use small amounts of memory. If you need to write an app that is small -- i.e., less than 100K -- you need to consider using HandleObjects
.
The nature of HandleObject
imposes some restrictions, however, on how you can use it in a program. These restrictions include:
class TSample: public HandleObject {The error message the compiler generates in this case is:
public:
...
long fData;
};
TSample *aSampleObject; // Legal
TSample anotherSample; // Results in a compile-time error
Can't declare a handle/pascal object: anotherSample
At first, this message might seem strange because the last two lines seem to both declare objects. Actually, the first declaration is of a pointer to an object, not of the object itself.HandleObject
data members, since the object might move if the heap is compacted. If you writelong *x = & (aSampleObject -> fData);then x becomes "stale," i.e., it has a valid address but doesn't point to where the programmer intends, if the object moves. The solution is to lock the object if there is a possibility that the heap may be compacted. Objects of
HandleObject
are allocated with a call to NewHandle
, so you can use HLock
and HUnlock
(along with an appropriate type cast) to lock and unlock the object.HandleObject
. The reason behind this restriction is not obvious. To understand the reason, you must look at the implementation of multiple inheritance.class TBaseA {If you were to look at objects of these classes (see Figure 1), you would find that in each case the object storage would contain four bytes for the C++ virtual table (
public:
virtual void SetVarA(long newValue);
long fVarA;
...
};
class TBaseB {
public:
virtual void SetVarB(long newValue);
long fVarB;
...
};
vtable
) and four bytes for the data member. Any code that accesses the data members (for example, TBaseB::SetVarB
) would do so using a fixed offset from the start of the object. (In the particular version of C++, this offset was 0; your offset may vary.) Figure 1 shows the layout of TBaseA
and TBaseB
objects.
![]() | ![]() |
class TDerived: public TBaseA, public TBaseB {In this case, an object of
public:
virtual void SetDerivedVar(long newValue);
long fDerivedVar;
...
};
TDerived
has the following layout, as shown in Figure 2:
This is what you would expect. TDerived
inherits from both TBaseA
and TBaseB
, and therefore objects of TDerived
contain a part that is a TBaseA
and a part that is a TBaseB
. In addition, the virtual table vtableDerived
includes the tables for both TBaseA
and TDerived
.
TDerived
also inherits the virtual member functions defined in TBaseA
and TBaseB
. Suppose you wanted to call SetVarB
, using a TDerived
object. The code for SetVarB
expects to be passed a pointer to a TBaseB
object (all member functions are passed a pointer to an appropriate object as an implicit parameter), and refers to fVarB
by a fixed offset from that pointer. Therefore, to call SetVarB
using a TDerived
object, C++ passes a pointer to the middle of the object; specifically it passes a pointer to the part of the object that represents a TBaseB
.
This gives you a very basic idea of how C++ implements multiple inheritance. For more details, read "Multiple Inheritance for C++" by Bjarne Stroustrup in Proceedings EUUG Spring 1987 Conference, Helsinki.
HandleObject
class expects to be passed a handle to the object, instead of a pointer; when multiple inheritance is used, the compiler sometimes has to pass a pointer to the middle of the object. Pointers into the middle of an object, even though (and especially because) they are implicit in this case, nevertheless present the same problem as pointers to object data members (as described earlier). The object's handle could be moved during heap compaction, rendering the pointer "stale."
Designing a new implementation of multiple inheritance that is compatible with a HandleObject
, as well as the rest of C++, is a big undertaking. For that reason, it is unlikely this restriction will disappear in the future. There are, however, two alternatives to consider:
HandleObject
is for purposes of compatibility with code that expects handle objects. However, another valid reason is to reduce the chance of fragmentation that would result from using non-relocatable blocks. But even in applications for which fragmentation would otherwise be a critical concern, memory allocation patterns may be very predictable; fragmentation is less of an issue when all allocated blocks are of similar sizes.
TDerived
as:class TSingleDerived: public TBaseA {In this case
public:
virtual void SetDerivedVar(long newValue);
void SetBaseB(long newValue);
long fDerivedVar;
TBaseB fBaseBPart;
...
};
TSingleDerived
inherits only from TBaseA
, but includes an object of TBaseB
as an data member. It also implements the virtual member function SetBaseB
to call the function by the same name in the TBaseB
class. (In effect, TSingleDerived
delegates part of its implementation to TBaseB
.) There are advantages and disadvantages to this approach. The advantage is that it requires only single inheritance, yet you can still reuse the implementation of
TBaseB
. The disadvantage is that TSingleDerived
is not a sub-class of TBaseB
, which means that an object of TSingleDerived
cannot be used in a situation that requires a TBaseB
. Also, TSingleDerived
has to define a member function that corresponds to each function in TBaseB
. (You can, however, define these functions as inline and non-virtual, which eliminates any run-time overhead.)
HandleObject
). Each vtable
is twice as large, and each virtual member function takes about 24 bytes, compared to 14. This is true even if you do not take advantage of multiple inheritance. For this reason, MPW C++ also contains a built-in class called SingleObject
, whose objects can be allocated in the same way as normal C++ objects, but which only supports single inheritance.
PascalObject
, uses Object Pascal's run-time implementation, which takes the least amount of space, but the most execution time.
WaitNextEvent
. Then, if some predetermined amount of time (perhaps one minute, for example) has elapsed since the user interacted with the application, unlock everything and compact the heap. EventAvail
to check if there is now an event that needs processing. If such an event has arrived, return to your main event loop to process it. CompactMem
routine really compacts the entire application heap even if you ask it to compact just a little bit of it, use NewPtr
instead. Ask for a moderate-sized block (e.g. "NewPtr(40000)
"), and check the time to see if you need to call EventAvail
. Then ask for another block. NewPtr
, dispose of the compaction pointers, and lock your handles. HNoPurge
on this list prior to a NewPtr
-based compaction, then call HPurge
on it after the compaction. Summary
You cannot use a HandleObject
with multiple inheritance because of the way multiple inheritance is implemented in MPW C++. Your alternatives are to give up one or the other: You can either use native C++ objects and let the objects fall where they may, or give up multiple inheritance and use a form of delegation.
Further Reference