I always label myself as a C++ programmer, which is odd as I've mainly been using Borland's Delphi for the last two and a half years. This article relates to my experiences during that time.
So why am I writing about a proprietary version of Pascal in a C++ publication?
Two reasons. Firstly, because the editor seemed short of material, so I thought I could get away with it and secondly, because I'm really interested in how the differences between the languages lead to different solutions to similar problems.
Now, if you've survived so far and you're not familiar with Delphi, here's a quick overview. You never know when it may be useful, if only to say "I'm not doing it in Delphi because…"
The greatest strength and weakness of Delphi are the same thing. It is a proprietary language that is targeted at just one platform - Windows. Borland were free to do what they liked to extend the Pascal syntax on which it is based. At the same time, they created a class library called the VCL that provides components both for the user interface and for database access. The environment is a very effective RAD in that it allows you to build both the user interface and database access parts of your application with very little code being needed. In that respect, it is in competition with MS Visual Basic. For this reason, as well as the fact the Borland seemed to market Delphi as a database application builder, Delphi isn't always recognised as a general applications language - albeit in the context of Windows only.
Prior to Delphi, Borland extended their Pascal to include objects. This extension was rather like the class in C++ as opposed to the 'struct' in C. The 'object' type extends the 'record' type by allowing member functions, constructors etc. These types can be instantiated on the heap, the stack or statically.
Delphi turns its back on this extension! Instead, a new keyword 'class' was introduced. All classes are subclasses of 'TObject'. Such classes can only be instantiated on the heap. Source code normally uses references to these objects. These references are assignable; i.e. they're more like references in Java - in other words pointers. Class members are accessed using the dot operator.
One big downside to this scheme is that you must explicitly create and free your objects. You don't get the benefits of scope based lifetimes within your methods. In fact, Object Pascal is generally more verbose than C++.
The first version of Delphi was 16 bit only; all three subsequent versions are 32 bit only. Each new version has extended the language and the VCL substantially. A quick summary of the current feature set goes something like this:
Run time type information is always available for class instances.
Class references. That is, references to a class type rather than an instance. These may be used with virtual constructors, a feature completely missing from C++.
Exception handling with the 'try-catch' construct is broadly like that of C++ except that only class instances can be thrown and though the stack is unwound, it doesn't necessarily help. As compensation, a 'try-finally' construct allows you to roll your own recovery.
Classes may have properties that may be assigned to and from. The nice thing about this is that they can be implemented by direct access to a private data member or by 'set' and 'get' methods.
As well as procedural types, i.e. function pointers in C++, there are method pointers, In C++ terms; these are a package of pointer to a method plus a pointer to the object. These are widely used in the VCL for event handling, e.g. 'button clicked' or 'edit change' events.
Language level support for OLE Automation through variants of dispatch type.
COM interface declarations, usage and implementation. This means that you may implement any number of interfaces in your classes. That's a sort of multiple interface only inheritance in the same way as Java. Also, dual interface COM objects can be used efficiently. Interfaces when used as references to external COM objects or internal Delphi class instances do have scope based lifetimes, i.e. they behave like C++ smart COM pointers.
There are some significant missing features. Particularly:
No proper multiple inheritance. To Java enthusiasts I gather that this is a virtue!
No operator overloading.
No macros, though there is conditional compilation.
Because of the nature of Pascal and the fact that types aren't included in headers but instead are declared in an interface section, there can be problems with circular references. If two classes have a two-way association, they must be declared in the same unit or you must jump through hoops involving either inheritance or type casting.
This is the real meat of this article. Some of the differences between C++, Java and Delphi must be apparent by now. The differences in syntax are neither here nor there in the main. Types are declared in type sections and instances are created in their own peculiar way. If you follow the rule that everything is back to front, wherever you have a space put a colon, put semicolons wherever there's room except before an 'end', you'll probably have compilable code!
On the other hand, certain C++ idioms and techniques just aren't available. You have to live without the STL. You can't create a locking object and let its scope define the lock on a resource. Well, you can if you don't mind using an interface rather than a class reference and you can wait until the end of the method.
At the detailed level, Pascal programmers use language features such as sets, arrays with unusual indexes and so on that are less freely used in C++. Most such things are perfectly possible now, either with class libraries such as STL or by rolling your own with operator overloading etc. It's largely a matter of habit. For example, I still find myself doing low level bit masking to pack all kinds of stuff into an integer or whatever, just like we did years ago.
Finally, source files are usually longer with more classes declared in one file because of the circular reference problem, The first significant feature that I want to discuss is really a mundane one but it's nevertheless pervasive in its scope.
Generally speaking, Delphi programmers don't have long and bitter disputes over how to do accessors and mutators. While we're on the subject, I utterly detest the naming of methods like GetColour(). The prefix 'Get' should be reserved for the cases when it really does cost something, like GetThatPileOfMachineryRustingInTheCorner(). But I digress and lose my last two surviving readers on religious grounds.
The great thing with properties is that you can define your attributes in a simple way initially. If you want a property to be readable and writeable, there's nothing to stop you simply declaring the data member publicly with the name that the outside world will see if you want to! Not that I would of course, the point is that property access is semantically identical to data member access, so you can always leave your options open. The stages of complexity that you could go through are:
property Colour : TColor read fColor write fColor;
Colour : TColor;
The above are effectively equivalent.
property Colour : TColor read fColor write SetColor;
This would be appropriate if you wanted to do something when the colour changes e.g. invalidate the window.
property Colour : TColor read GetColor write SetColor;
This would allow you to delegate the colour storage to some other object when you completely re-implement the class.
Incidentally, properties may also take any number of indexes of any type and may also be declared as default indexed properties. This gives an effect similar to the overloaded  operator in C++.
So in the end, properties are perhaps a syntactic sugar in much the same way as operator overloading. They don't have an enormous impact on the grand design of a program but they make the details of class declaration and usage much cleaner.
I assume that they won't make inroads into the C++ language as such. Nevertheless, Borland has property extensions to C++ Builder as well as their being generally used in COM.
It's also worth contrasting this with the Data Attribute Notation style introduced in Overload 28. As that article is the first I've heard of it and it seems to serve a slightly different purpose, I'll leave it as an exercise for the reader. One interesting point is that DAN requires class types for each attribute, something that's not needed in our context. This is a difference that will crop up again, so file it away for now!
Having class references and virtual constructors allows you to achieve something like class factories with no programming overhead! I have used this feature many times and it is something that C++ really misses. Look at the horrors of CRuntimeClass and the macros that use it in the old MFC library and you'll know what I mean.
However, I'd like to sketch another case where it was useful.
I needed a red-black tree implementation that could be used as a basis for mapping classes. These trees are so useful that we needed to get a good design so that we could take full advantage. There are many possible ways that a tree could be made to work. For example, the key and the value could be stored in one object that is collected by the tree. The comparison function might then take a reference to two objects. This is all right for inserting objects into the tree but less useful for lookups, where a similarly keyed object would need to be created temporarily. In the case of a map, the key probably won't be part of the collected object at all. Also, who is to say that it's just objects that you want to collect?
The STL approach would be the ideal but of course is out of the question. The trick here is to write the tree code so that instead of creating nodes of a fixed type, the actual node type can be given when the tree is created. The node type must of course be derived from a class that has the left and right references and the node colour. In addition, it might carry a string key and an object reference, or just an object reference or string key and string value. Because a class reference is passed to the tree and a virtual constructor is available, nodes of the correct type are created. For this purpose, a factory class instance would need to be passed in C++, and although there is more coding involved, it may well be a useful technique to consider.
Of course, the down side in Delphi is that you need to write all these derived node classes yourself and that a certain amount of casting is required. The casting can largely be eliminated from client code however.
The upshot is that I was easily able to create the half dozen basic map classes that I needed as well as use the tree as an ordered collection.
Before I go on with the red-black tree story, there are other uses to which dynamic construction may be put. With a collection of objects derived from some common base type, it is often necessary to clone these objects. Because each instance has its class type available through the RTTI mechanism, most of the work can be done in the base class. A base clone method can create the correct type and then call a virtual assign method. Each derived class need only implement an assign method.
Another frequent use is in object persistence. When reading a stream, a class identifier of some kind is extracted and mapped to a class reference. This reference is then used to directly create the streamed object. A familiar mechanism in C++, except factory objects replace the class references.
What about the comparison methods that the red-black tree needs in order to operate? Clearly a plain function pointer isn't going to be of much use. Its parameters are hard to pin down. One parameter could reasonably be the red-black node but another parameter would need to be the key. Unfortunately, the key is of unknown type - perhaps not a problem if you have templates. One workable solution is to have a base class that has a virtual method that takes the node and whose state includes the key or at least provides access to the key. Although this kind of solution is valid, it does lead to additional class types being declared and probably leads to additional instances that you could well live without.
The Delphi solution is a variant on the latter approach, instead of providing an instance of a class, provide an 'event' in the form of a method pointer. Now we are placing no constraints on the type of the object that does the work, we know only that it has a method with the right signature and that it can obtain a key from somewhere.
In the case of a map class, the event handler may be a method of the map class itself. Indeed, I created a hierarchy of maps that were based on the key type, so that each could handle the key in the most appropriate way. For each of these I also derived a node class that had a suitable key. From these maps and nodes I then derived the specific classes that mapped the type I needed. This may sound like it was a lot of work but it wasn't so bad. By splitting the type problem into key and value stages, the amount of source code needed was very small. Not as small as it would have been with templates but at least the amount of compiled code was probably very similar, if not less!
The above example is one of many where this mechanism has been used in a context other than user interface programming. One significant way that these approaches differ from the generic approach is that the red-black tree code only needs to be compiled once, it isn't compiled repeatedly for each type that is needed.
The main difference with the traditional object oriented approach is that we don't have to introduce base classes that are likely to be inconvenient when we've moved on from this library. This inconvenience becomes apparent when you find yourself either creating over complex derived instances that need to reference other classes (such as a map class). These classes may need to access state in another class that you really didn't want to expose. In this case, the best solution would probably be to instantiate one of the classes as a short-lived instance with a copy of the key as part of its state.
What I'm trying to say is that the event mechanism as used by Delphi provides an alternative form of polymorphism. Rather than an instance presenting an interface defined in its base class, it is simply presenting a method of the required type. In many cases, this saves you needing to create class hierarchies when none would otherwise be needed. This is analogous to the advantage of generic programming using templates where for example, you're not forcing collected objects to be of a 'collectable' class. In practice of course, Delphi programs still rely heavily on conventional object oriented class hierarchies, just as templates can't replace them in C++.
It's worth looking a little more at the conventions for implementing events. Typically, event methods are procedures rather than functions, even when the handler may pass data back to the caller. That is because in most cases, the absence if a handler allows the default behaviour. Rather than relying on a return value, a non-const reference to a value that is already set to the default state is provided as a parameter. Often, you might want to achieve the specialisation either by providing an event handler or by derivation. In this case, the code that calls the event is provided as the body of a virtual method. For components this is very natural. For example, if you were sub-classing an edit control and you needed to extend the 'OnChange' event, you would simply override the 'Change' method, leaving the OnChange property available as it is with the base class.
The good news is that not only is this article nearing the end but that this mechanism is available to the C++ programmer as well. See my article in Overload 17/18 for an example that uses a template to package an object pointer and method pointer of the target type. This implementation is more involved simply because of the need to support multiple inheritance. With single inheritance and casting, the job can be done in the same way as in Delphi.
Bad news for Java programmers though. As the language doesn't support method pointers, you will have to use pure OO techniques only.
COM uses a different model for the same purpose - connection points. This entails providing an interface to handle a number of events rather than just the one method. This can be inefficient if you're only interested in a subset of events fired by an out of process server. One good feature of this mechanism is that multiple interfaces can be advised side by side, i.e. more than one object can be notified when an event occurs. This feature can be reproduced in Delphi but you have to do a bit more work. An event property has to be implemented as an event list! I have found this to be a very useful thing in terms of co-ordinating objects, particularly in user interfaces. I have also put the event lists into an 'event broker' collection that has been even more useful.
Overload Journal #29 - Dec 1998 + Programming Topics
|Browse in :||
All > Topics > Programming (772)
Any of these categories - All of these categories