Fewer pointers – less double thinking:

type object vs. type class in Delphi

 

1. Introduction

 

The article by Peter Roth “Nice and nicer: how nice are your Object Pascal classes?” (DI, August 97) introduced the criteria of good classes and the problems related to the type class. The classes in Delphi are hidden pointer variables, which requires special thinking and attention. Complementary to the article of Mr. Roth, I am going to discuss the objects that have nothing to do with pointers and behave like normal global or local variables in a high level programming language: I mean objects declared by the “old fashioned” Object Pascal type object. Variables of this type may be either global/local, or dynamic (reside on the heap). What is called  the old object calling convention is actually a mechanism of referring to an object 

 

SomeObject = object  

 

via a pointer

 

SomeObjectPtr = ^SomeObject;

 

Because of the convention that variables of type class in Delphi are hidden pointers, this way of calling the object really becomes obsolete – but not the type object itself. The linear memory model implemented since Windows 95 and NT together with the new features of Delphi 2-4, leave cases, when the type object provides a better solution.

 

2. Pitfalls of the type class

 

In general, pointers are somehow in conflict with such fundamental principles of high level languages as 1) The rule of scope, and 2) One-to-one correspondence between a variable and its instance (memory image). Classes and pointers do not work this way. Not only do we need to declare and initialize them like other variables, we also have to take two additional actions: to create and later destroy the instances (to allocate and de-allocate the memory). Thus, although a pointer var p : ^T is visible and exists only in its scope, its instance p^ (some structure of type T) does not necessarily exist within this scope and does not necessarily disappear outside it. Moreover, it may happen that several pointers var p1, p2, p3 : ^T point to the same instance of type T, or conversely, there may be instances of T, to which no pointer points at all. Pointers can also point to a wrong place.

 

Hidden pointers (type class) are even more peculiar. For objects

 

var  a, b : TSomeClass

 

neither the assignment  a := b, nor the condition a = b has its usual mathematical meaning. Instead, special methods -- Copy and Equal -- must be designed to perform these functions (as described in Roth’s article). Also,

 

procedure Any1(x : TSomeClass, …)   as well as

procedure Any2(const x : TSomeClass, …)

 

can actually change the instance of  x -- despite the fact that it looks like called by value.

 

One more example: imagine that you have to design a new type, say TMatrix, and operations on values of that type like Sum and Product. Normally, you need to declare

 

function Sum(const A, B : TMatrix) : TMatrix;

function Product(const A, B : TMatrix) : TMatrix;

 

This allows to use expressions like Product(Sum(A, B), C), but not if TMatrix is a class. Where do the functions and the implicit variable Result (all pointers) point to? Who is responsible for creating and destroying the instances of the Result of these functions? Here it is better not to bother with functions at all, but to declare procedures instead and give up the  opportunity to write the above mentioned expressions:

 

procedure Sum(const A, B : TMatrix; var C : TMatrix);

procedure Product(const A, B : TMatrix; var C : TMatrix);

 

Again, because TMatrix is a class, the const and var have no usual meaning and are only used to show the input parameters and the output in the procedures.

 

            (Note: an undocumented feature of Delphi 2-4 syntax even allows omitting the sign ^ in pointer expressions like p1^.SomeField  or  p2^[i, j], writing p1.SomeField  or  p2[i, j] instead. This means that pointers can be hidden not only for classes now, therefore the provocative confusions between instances and their pointers are very real).

 

This shows that explicit or implicit pointer variables are somehow unnatural for high level languages, because they require a sort of “double-thinking” in the design phase and can cause even more pain while debugging. The crucial issue here is to distinguish between logical and technical reasons to use pointer variables. Logically pointers are needed in Pascal for designing special data structures such as linked lists, graphs et cetera, which can not be done in any other way.

 

There are also technical reasons that have influenced programming style for a decade. Here are at least four such reasons:

 

1. Memory access limitations for IBM PCs required that no data structure exceed 64K. Thus, the stack size usually was much less, typically 16K (program size plus stack could not exceed 64K). This limited the space for global and local variables, but since Delphi-2 it is not true any more. Now the stack size is controlled by the compiler directives Minimal stack size ($MINSTACKSIZE, default 16K) and Maximal stack sizes ($MAXSTACKSIZE, default 1M), and the stack grows incrementally as needed.  Thus, memory allocation on the heap is not more efficient than allocation on the stack any more. 

 

2. The only way to utilize upper memory before the advent of 32-bit operating systems was using the heap and pointers. Again, this is not true any more either.

 

3. For arrays of largely variable size, it was reasonable in standard Pascal to allocate memory on the heap rather than to reserve the maximal possible array size (allocated on the stack). Since the introduction of variant arrays in Delphi-2 and dynamic arrays in Delphi-4, this reason has become less important. Variables and fields of these types require the memory according to their current length (plus overhead), and programmers may treat them similar to usual local or global variables. (Note that “non-locked” variant arrays are at least 10 times slower than static variables or dynamic arrays in Delphi-4).

 

The above mentioned technical reasons are not valid any more, but the following still is.

 

4. The Visual Component Library (VCL) is based on the Windows API that uses classes and pointers heavily, thus if the users want to build a hierarchy of objects inheriting from the VCL, these objects must also be of type class (i.e. pointers).

 

Nevertheless, designing new structures which are not descendants of the VCL,  we can find better solutions than using classes. In the next section we will describe programming with type object as an alternative to class.

 

3. Where type object is better than class

 

In the literature about Delphi we can not find much about type object except that it is still supported to be backward compatible with Object Pascal. Therefore, we use the syntax given in Object Pascal before introduction of Delphi, plus we can also use properties. The relationship of type object with type class is such that a hierarchy based on inheritance from type object can not mix with that of class (no mixtures of is-relationship type, according to Roth’s terminology.) Nevertheless, fields of object or class may be of any type, i.e., a mixture of the has-relationship is acceptable, although rarely meaningful.

 

We are going to use the type object for declaring global or local variables without using pointers. In practice we want them to be mostly global, so that they do not lose their values outside the scope. To avoid the clutter of many global variables concentrated in one place, we are free to design as many units as we logically need so that each unit declares its own global variables.

 

The biggest advantage of type object shows when we define structures with  fixed and known length at design time. Then there is no need to allocate dynamic memory to certain fields and structures (otherwise the type class would be more appropriate).

 

While designing a hierarchy of objects without virtual methods, we do not need to declare and use constructors and destructors at all. (Constructors are still needed for virtual object variables, not to allocate memory to them, but to initialize the virtual call mechanism -- late binding.) Here are examples.

 

TAlgCmplx = object   {defines complex numbers in form  a + ib}

     Re, Im : extended;

     procedure Conj;  {z.Conj returns the conjugation to z}

     procedure Init(const x, y : extended);  {z.Init(x,y) means  z = x + iy}

     end;

 

  TExpCmplx = object  {defines complex numbers in form r*exp(i*Arg) }

     r, Arg : extended;

     procedure Conj;  {z.Conj returns the conjugation to z}

     procedure Init(const rad, fi : extended); {z.Init(rad, fi) means rad*exp(i*fi)}

     end;

 

Given these types, the following functions perform operations on complex numbers

 

function AlgOper(const z1 : TAlgCmplx; const op : char; const z2 : TAlgCmplx) : TAlgCmplx;

function ExpOper(const z1 : TExpCmplx; const op : char; const z2 : TExpCmplx) : TExpCmplx;

 

and functions

 

function ExpToAlg(const z : TExpCmplx) : TAlgCmplx;

function AlgToExp(const z : TAlgCmplx) : TExpCmplx;

 

transform one format into another. With these functions it is easy to use arithmetic expressions with complex numbers. For example, the mathematical expression 

(z1 - z2)/(z3 + z4) may look like

 

AlgOper(AlgOper(z1, ‘-‘, z2), ‘/’, AlgOper(z3, ‘+’, z4));

 

Another example defines an object that provides bit-to-bit access within one byte.

 

TBit = [0..1];

T8Bits = object

      BitStore : byte;

      function  GetBit(  const i : byte) : TBit;

      procedure PutBit(const i : byte; const r : TBit);

      property Bit[const i : byte] : TBit read GetBit write PutBit;

      end;

 

With this definition and  var s : T8Bits  we can write for example  s.Bit[3] := 0;  or 

 

with s do Bit[2] := Bit[1];

 

(Note: it seems reasonable to specify Bit as default property in this example, but all versions of the Delphi compiler up to 4 do not support the property specifier default for object type.)

 

The advantages of using type object over type class in the examples above are obvious. Besides that the variables of the defined types may be treated simply as non-pointer variables, the type T8Bits  occupies exactly one byte, not 4, like it would if it were a class. Also, if complex numbers were defined as a class, it would be impossible to define the operations as functions and to use them in expressions.

 

So far, the examples above were rather simple, involving no hierarchy, inheritance or polymorphism. Let us consider some examples with all these features. First, a general note. Given the definitions:

 

TObj1 = object  {some object} ; TObj2= object(TObj1);  TObj3 = object(TObj2);

TCls1 = class  {some class} ;       TCls2= class(TCls1);     TCls3 = class(TCls2);

 

the respective variables

 

var Obj1 : TObj1;  Obj2 : TObj2; Obj3 : TObj3;

       Cls1 : TCls1;   Cls2 : TCls2;  Cls3 :  TCls3;

 

are assignment compatible from left to right, which means in particular that pointer Cls1 may point to instances of any of  the types TCls1, TCls2, TCls3. If all three class types have a virtual method procedure Same(…); virtual and constructor Create(…), then

 

if  Cls1:= TCls1.Create(…) ,   Cls1.Same(…)  calls the version of Same for TCls1;

if  Cls1:= TCls2.Create(…) ,   Cls1.Same(…)  calls the version of Same for TCls2;

if  Cls1:= TCls3.Create(…) ,   Cls1.Same(…)  calls the version of Same for TCls3.

 

Thus, the same variable Cls1 always calls the correct versions of the method according to one which created the instance of Cls1 at run time: this is how the polymorphism works for the type class.

 

In contrast, when we deal with variables of type object directly, it’s impossible to not know at compile time which of the objects in the hierarchy calls the method. For example Obj2.Same(…) calls exactly the version of the method for the type TObj2. Thus, for the type object the usual form of polymorphism never occurs at all, at least not with a direct method call, but it may happen when one method calls another. Suppose, there is a method belonging only to TObj1, say TObj1OnlyMethod(…), and in the body of it a virtual method Same is called (Same may belong to TObj1, TObj2 or TObj3). Then, at compile time, it can not be known what version of Same must be called. With that in mind, let us consider the following example of object types.

 

type T3D = array[1..3] of extended;

        T3x3 = array[1..3, 1..3] of extended;

 

     T3DPoint = object

     x : T3D; {3D vector}

     procedure Init(const a : array of extended);

     procedure Apply(var v : T3D); {just calls Move}

     procedure Move(var v : T3D); virtual; {here it is empty}

     end;

 

     TDisplacement = object(T3DPoint) {the inherited field x is intended for displacement}

     constructor Init(const d : array of extended);

     procedure Move(var v : T3D); virtual; {applies displacement x to vector v}

     end;

 

     TGenTransfrm = object(TDisplacement)

     A : T3x3;   {matrix of rotation}

     constructor Init(const d, B : array of extended);

     procedure Move(var v : T3D); virtual; {applies rotation A and displacement x to vector v}

     procedure Prod(const B : T3x3); {A := A*B}

     end;

 

var v : T3D;

      Shift : TDisplacement;

      GenTrans : TGenTransfrm;

 

(Note: for virtual methods of object type the directive virtual should be used instead of the override.) Given these definitions, the variable v.x  represents some point in 3D space, Shift.x is intended to keep a displacement, and GenTrans represents a general transformation with displacement GenTrans.x and rotation GenTrans.A . Then, the call Shift.Move(v) performs the displacement of v, the call GenTrans.Move(v)  performs the rotation and then displacement of v. In these cases the caller is known at compile time.

 

To demonstrate the case when the caller is unknown at compile time, the ancestor type T3DPoint introduces a dummy method Move (which does nothing) and Apply (which just calls Move). Contrary to the method Move, there is just one version of method Apply, inherited by all descendants. Thus, Shift.Apply(v) calls TDisplacement.Move(v) and GenTrans.Apply(v) calls TGenTransfrm.Move(v),  i.e. the appropriate version of the method Move is called in each case. So, the polymorphism can work also with type object, although its usage is more limited in this case.

 

Finally a few notes on using a “mixture” of a class type, say SomeClass, having certain fields of the object type. Such combination can be useful if no other field of SomeClass is of the class type. Then, the methods Create and Destroy inherited from the VCL’s  TObject, will automatically allocate and de-allocate the right amount of memory, so that the users need not bother to design their own methods Create and Destroy to perform these operations for every field of SomeClass of type class.

 

4. Conclusions

 

Since version 5.5 of Borland’s Turbo Pascal, objects are defined as type object, which may be referred both directly and indirectly via pointers (as static or dynamic in the more confusing terminology). Delphi adds new features to the objects and introduces the type class as an object called indirectly via a hidden pointer. One could expect that because a class is essentially just an object referred via the pointer, the syntax and semantics of both structures must be identical. Nevertheless, there are some differences, probably because Delphi is still evolving as a programming language. Hopefully, they will disappear in the later versions. After all, other data types apply to both direct and indirect variables. Why this exception for the class?

 

The practical advantages of type object increased because of the linear memory model and the potentially unlimited stack implemented in Delphi 2-4. The type object provides better and safer solutions in situations when developers need structures of a short length, or structures of a long fixed length, or when fields of the object are dynamic and variant arrays.

 

Acknowledgments: Finally, I wish to express my deep appreciation to Dr. Manfred Mackeben, who helped to essentially improve this text.

 

 

 

Alexander Gofen