You probably know this situation: A C++ enthusiast is trying to "convince"
you that C++ is better than Object Pascal, because OP is lacking some
"important features of a modern object oriented programming language".
If you ask him which he will usually give you examples of things that
are actually supported in OP and have been for years. At that point he
will then either start talking about that "everybody knows" that
C++ compilers generate better optimized code than compilers for other
programming languages and therefore you must use C++ for everything (which
is bollocks as any decent performance comparison will prove). Or he will
think up some rather obscure feature of C++.
One of these obscure features, which less than 50% of all C++ programmers
ever use, are templates or generics. And Delphi doesn't support them, or
does it?
OK, let's assume we are stupid and ask "What is a template?"
(In the German original this is an adapted quote from
"Die Feuerzangenbowle" a very good and funny movie with Heinz Ruehmann.
A template - or generic - is an abstraction of an algorithm from the data
it works on. In C++ templates are part of the language. If you ever
ran into something like the following, you have actually seen a template
in the wild:
//--- file CheckedArray.h
#ifndef CHECKEDARRAY_H
#define CHECKEDARRAY_H
#include <stdexcept>
/////////////////////////////////// class CheckedArray<T>
template<class T>
class CheckedArray {
private:
int size; // maximum size
T* a; // pointer to new space
public:
//================================= constructor
CheckedArray<T>(int max) {
size = max;
a = new T[size];
}//end constructor
//================================= operator[]
T& CheckedArray<T>::operator[](int index) {
if (index < 0 || index >= size)
throw out_of_range("CheckedArray");
}
return a[index];
}//end CheckedArray<T>
};//end class CheckedArray<T>
#endif
//--- file test.cpp
#include "CheckedArray.h"
//================================= main test program
void main() {
CheckedArray<double> test1(100);
test1[25] = 3.14;
CheckedArray<int> test2(200);
test2[0] = 55;
}//end main
(from: http://leepoint.net/notes/cpp/oop-templates/template-ex1.html)
This is a class with an array which does a bounds check on the index
on every access. (Yes, in C++ this not a built in compiler feature,
you must implement it yourself.)
The syntax
template<class T>
class CheckedArray {
}
means that it is a template, where T is a class that can be parameterized.
The actual class is provided when the template is used like this:
CheckedArray<double> test1(100);
This means that the template is to be used with T = double.
Templates are converted by the C++ compiler (actually it is done by the
preprocessor, but who cares about this difference?) into copies of the
template code. This means that with each usage of a template the whole
code for this template will be added to the program again. This could
explain why C++ programs are that large. ;-) But this and other similarly
complex processes are definitely the reason why C++ compilers are that
slow (Typical compile times for a few ten thousand lines of
code in Delphi: 5-10 seconds, in C++ several minutes).
There is the so called Standard Template Library (STL) which comes with
every decent C++ compiler (The one from Microsoft came with some serious
bugs but there is an inofficial patch available for it). It contains mostly templates
for different container classes. Those containers are supposed to be
optimized for performance or memory usage respectively so that in theory
the programmer only needs to select the correct container for his problem
to write efficient code.
First I must disappoint you: There is no secret compiler switch which
enables templates in Delphi (or Kylix, for that matter, because what
I describe here works with "Delphi for Linux" too).
But that isn't necessary, because Delphi already supports everything
we need for this, only the syntax is a bit cumbersome (otoh the example
above wasn't easy to read either, was it?). All it needs are a few
conditional defines and include files. (If you are as old as I, you
probably remember that even the good old Turbo Pascal had these features,
it's just that nobody had the idea to use them in this way.)
Here is a simple template in Object Pascal:
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
unit t_TypedObjectList;
interface
uses
Sysutils,
Classes,
Contnrs;
type
_TYPED_OBJECT_LIST_ITEM_ = TObject;
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
type
_TYPED_OBJECT_LIST_ = class
protected
fList: TObjectList;
function GetItems(_Idx: integer): _TYPED_OBJECT_LIST_ITEM_;
public
constructor Create;
destructor Destroy; override;
function Add(_Item: _TYPED_OBJECT_LIST_ITEM_): integer;
property Items[_Idx: integer]: _TYPED_OBJECT_LIST_ITEM_
read GetItems; default;
end;
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
implementation
{$DEFINE TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
{$IFDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
{ _TYPED_OBJECT_LIST_ }
constructor _TYPED_OBJECT_LIST_.Create;
begin
inherited Create;
fList := TObjectList.Create;
end;
destructor _TYPED_OBJECT_LIST_.Destroy;
begin
fList.Free;
inherited;
end;
function _TYPED_OBJECT_LIST_.Add(
_Item: _TYPED_OBJECT_LIST_ITEM_): integer;
begin
Result := fList.Add(_Item);
end;
function _TYPED_OBJECT_LIST_.GetItems(
_Idx: integer): _TYPED_OBJECT_LIST_ITEM_;
begin
Result := fList[_Idx] as _TYPED_OBJECT_LIST_ITEM_;
end;
{$WARNINGS off}
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
end.
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
{$DEFINE TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
If you ignore the IFDEFs, DEFINEs etc. for the moment this is an
ordinary class declaration in an ordinary unit. We declare a class
__TYPED_OBJECT_LIST__ which internally uses a TObjectList to store
objects of type __TYPED_OBJECT_LIST_ITEM__. To make it easier to
understand, I have omitted all but the essential methods and properties
(Add and Items). The complete unit is available for download.
OK, now how do I use this thing? It is even simpler:
unit u_MemoList;
interface
uses
Sysutils,
Classes,
Contnrs,
StdCtrls;
{$define TYPED_OBJECT_LIST_TEMPLATE}
type
_TYPED_OBJECT_LIST_ITEM_ = TMemo;
{$INCLUDE 't_TypedObjectList.tpl'}
type
TMemoList = class(_TYPED_OBJECT_LIST_)
end;
implementation
{$INCLUDE 't_TypedObjectList.tpl'}
end.
This example declares a container TMemoList to type safely store
TMemo objects.
You can simply use this unit in a program, create an instance of
TMemoList and store memos in it. Accessing those memos can be done
conveniently without any type conversion:
SomeMemoList[0].Lines.Text := 'new memo text';
As said before, there is no magic here. We use only features of the
Delphi compiler that it has had for years even in its early incarnations
as Turbo Pascal.
The trick is to include the unit twice as an include file and by using
conditional defines and IF(N)DEFs exclude the parts that aren't wanted.
A {$IFNDEF TYPED_OBJECT_LIST_TEMPLATE} ... {$ENDIF} brackets all
those parts the compiler should never see.
A {$IFNDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS} ... {$ENDIF}
brackets the part that should only be available in the first pass
(interface) and
a {$IFDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS} ... {$ENDIF}
brackets the part that should only be available in the second pass
(implementation).
It would be possible to omit those defines by having two separate
include files, one for the interface and one for the implementation.
But this has got two drawbacks:
-
Splitting the template into two files is not just unelegant but also error prone.
-
The template unit can during development of the template not be used as an ordinary unit, giving away the priceless help of the compiler.
Let's have a look at the template unit again, this time split into easily
palatable parts:
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
unit t_TypedObjectList;
interface
uses
Sysutils,
Classes,
Contnrs;
type
_TYPED_OBJECT_LIST_ITEM_ = TObject;
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
Since we don't realy use it as a unit the unit header including the
uses clause is commented out via IFNDEF. During develompent of the
template you can compile the unit like any ordinary unit to simplify
finding any bugs. But when later used as a template you define the
conditional
define TYPED_OBJECT_LIST_TEMPLATE and so make the compiler ignore
the unit header. Also we want to ignore the declaration of
_TYPED_OBJECT_LIST_ITEM_, because we want to parameterize this
type.
Now let's look at the type declaration of the container
_TYPED_OBJECT_LIST_:
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
type
_TYPED_OBJECT_LIST_ = class
protected
fList: TObjectList;
function GetItems(_Idx: integer): _TYPED_OBJECT_LIST_ITEM_;
public
constructor Create;
destructor Destroy; override;
function Add(_Item: _TYPED_OBJECT_LIST_ITEM_): integer;
property Items[_Idx: integer]: _TYPED_OBJECT_LIST_ITEM_
read GetItems; default;
end;
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
Here we have got another conditional define
TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS.
This one makes the compiler read this part only the first time it
is included. The rest is an ordinary class declaration like those
you have seen a thousand times. Only the type names probably look a
bit strange to you. You can choose them freely, actually, the same goes
for the names of the conditional defines. I have chosen this special
form to emphasize that they are no ordinary types.
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
implementation
{$DEFINE TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
Again a conditional define lets the compiler ignore this part if it
is used as a template. This also means that it will ignore the define
for TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS, because this is only
used during the development of the template.
The following implementation section is, again, rather boring:
{$IFDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
{ _TYPED_OBJECT_LIST_ }
constructor _TYPED_OBJECT_LIST_.Create;
begin
inherited Create;
fList := TObjectList.Create;
end;
destructor _TYPED_OBJECT_LIST_.Destroy;
begin
fList.Free;
inherited;
end;
function _TYPED_OBJECT_LIST_.Add(
_Item: _TYPED_OBJECT_LIST_ITEM_): integer;
begin
Result := fList.Add(_Item);
end;
function _TYPED_OBJECT_LIST_.GetItems(
_Idx: integer): _TYPED_OBJECT_LIST_ITEM_;
begin
Result := fList[_Idx] as _TYPED_OBJECT_LIST_ITEM_;
end;
{$WARNINGS off}
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
end.
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
Again we have got a bracketing IFDEF .. ENDIF with the conditional
define TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS already mentioned
above. This makes the compiler ignore this part in the first pass.
It will only see the line
{$DEFINE TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
following the ENDIF. This defines this symbol for the second pass.
With all these INCLUDEs and IFDEFs we make the compiler see a totally
different source than what we see. The following is what the compiler
actually compiles. It starts with the unit u_MemoList, includes
the template unit t_TypedObjectList twice and of course omits what
the compiler cannot see.
I hope this will help all those, who are not yet sure they understand, to
see the light. ;-)
unit u_MemoList;
interface
uses
Sysutils,
Classes,
Contnrs,
StdCtrls;
type
_TYPED_OBJECT_LIST_ITEM_ = TMemo;
type
_TYPED_OBJECT_LIST_ = class
protected
fList: TObjectList;
function GetItems(_Idx: integer): _TYPED_OBJECT_LIST_ITEM_;
public
constructor Create;
destructor Destroy; override;
function Add(_Item: _TYPED_OBJECT_LIST_ITEM_): integer;
property Items[_Idx: integer]: _TYPED_OBJECT_LIST_ITEM_
read GetItems; default;
end;
type
TMemoList = class(_TYPED_OBJECT_LIST_)
end;
implementation
{ _TYPED_OBJECT_LIST_ }
constructor _TYPED_OBJECT_LIST_.Create;
begin
inherited Create;
fList := TObjectList.Create;
end;
destructor _TYPED_OBJECT_LIST_.Destroy;
begin
fList.Free;
inherited;
end;
function _TYPED_OBJECT_LIST_.Add(
_Item: _TYPED_OBJECT_LIST_ITEM_): integer;
begin
Result := fList.Add(_Item);
end;
function _TYPED_OBJECT_LIST_.GetItems(
_Idx: integer): _TYPED_OBJECT_LIST_ITEM_;
begin
Result := fList[_Idx] as _TYPED_OBJECT_LIST_ITEM_;
end;
end.
Convinced? Easy, isn't it? At least for the compiler. ;-)
Yes, that's a good question. It took me quite a while before I started
using templates like this. I was so used to TList and friends and
typecasts (and the 'as' operator) that I didn't realize how error
prone that can be. After seeing the light ;-) my templates have started
multiplying like rabbits, especially those that implement several
different sorts of containers ((sorted) collections, vectors, queues,
stacks etc.)
The nice thing about it is, that you need to do do the hard bit
- writing the code - only once, using it is simple enough so that even
the non-wizard software developers can master it easily. All they
need to do is adding two blocks of code into their units.
In the interface section:
{$define TYPED_OBJECT_LIST_TEMPLATE}
type
_TYPED_OBJECT_LIST_ITEM_ = <type to save>;
{$INCLUDE 't_TypedObjectList.tpl'}
and in the implementation section:
{$INCLUDE 't_TypedObjectList.tpl'}
Those blocks can for example be saved as Code Insight code templates.
The template user doesn't need to understand the tricks that are used
internally. He can even use the debugger to single step through the
code.
This is not just a pointless proof for the C++ enthusiasts that it is
possible, it is also useful in real life programming. Given time I
hope to create a template library resembling to an extend the STL of the
C++ world.
Most of you will be familiar with some sort of code genrator. There are
some which will generate typed lists as a descendant class of TList.
I don't like those, because they will extend TList with additional
methods and properties but the original ones will still be there. This
violates the object oriented paradigma of hiding all information that
is not necessary.
My first approach was to write my own code generator which uses
a template source code with wildcards so it is very flexible and
can generate a wide variety of different structures.
The the main disadvantage of it was that I was duplicating the code
rather than reusing it. If there was a bug in my
template I had to either fix it in all the sources generated from it
or regenerate the sources again from the fixed template. Both approaches
were too error prone.
With the pseudo templates introduced here this problem does not exist.
Just rebuilding the executable will automatically include the fix for
all implementations of a template.
I would never have come up with this idea on my own. One day I came
across a posting from Radek Jedrasiak in
borland.public.delphi.objectpascal where he wrote about C++ like templates
in Object Pascal for which he had made a small improvement. I found
the principle quite interesting and read the original article by
Rossen Assenov
(http://community.borland.com/article/0,1410,27603,00.html
registration mandatory)
and the comments there. The idea was born.
(c) Copyright 2002-2003 by Thomas Mueller, all rights reserved
This document was generated using AFT v5.095
|