Ist Dir auch schonmal ein C++ Anhaenger damit auf den Keks gegangen,
dass er Object Pascal heruntermachen wollte,
weil ihm "wichtige Merkmale einer moderenen objektorientierten
Programmiersprache fehlen"? Wenn man dann nachfragt, welche das
denn seien, kommen dann Beispiele, bei denen sich meistens herausstellt,
dass Object Pascal genau das schon seit geraumer
Zeit kann. Meist behauptet der Freak dann einfach, es sei ja
"allgemein bekannt, dass C++ Compiler performanteren Code erzeugen
als die Compiler anderer Programmiersprachen" (was Bloedsinn
ist, wie diverse Compilervergleiche zeigen). Oder aber er zieht sich
auf die etwas obskureren Features von C++ zurueck.
Eines dieser obskureren Features, welches noch nicht einmal 50% aller
C++ Programmierer beherrschen, sind Templates. Und die kann Delphi
tatsaechlich nicht, oder doch?
Ok, stelle ma uns mal wieder ganz dumm und fragen: "Watt is'nen Template?"
Ein Template - zu deutsch "Schablone" - ist eine Abstraktion eines Algorithmus
von den Daten, mit denen er arbeitet. In C++ sind Templates tatsaechlich
Bestandteil der Sprache. Wer schonmal ueber sowas wie das folgende gestolpert
ist, der hat tatsaechlich ein Template in freier Wildbahn gesehen:
//--- 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
(Quelle: http://leepoint.net/notes/cpp/oop-templates/template-ex1.html)
Dies ist eine Klasse mit einem Array, die beim Zugriff ueberprueft, ob
der Index innerhalb der Arraygrenzen liegt. (Ja, in C++ ist das kein
eingebautes Compilerfeature, man muss es selbst machen.)
Die Syntax
template<class T>
class CheckedArray {
}
bedeutet, dass es sich um ein Template handelt, in welchem T eine
parametrisierbare Klasse repraesentiert. Diese Klasse wird dann bei
der Verwendung des Templates angegeben wie folgt:
CheckedArray<double> test1(100);
Das bedeutet, dass das Template mit T = double verwendet werden soll.
Templates werden vom C++ Compiler (eigentlich vom Preprozessor, aber wen
interessiert der Unterschied?) umgesetzt in Kopien des Template-Codes.
D.h. bei jeder Verwendung eines Templates wird der komplette Code fuer
dieses Template erneut in das Programm eingebunden. Das koennte erklaeren,
weshalb C++ Programme so gross sind. ;-) Dieser und andere aehnlich
zeitintensive Vorgaenge sind aber definitiv der Grund, weshalb C++ Compiler
so langsam sind. (Typische Compilezeit fuer ein einige zig-tausend Zeilen
Programmcode in Delphi: 5-10 Sekunden, in C++ mehrere Minuten.)
Es gibt fuer C++ eine sogenannte Standard Template Library (STL), welche eine
ganze Reihe Templates, insbesondere fuer Container deklariert. Die STL
ist in der Regel im Lieferumfang des Compilers enthalten. (Die von Microsoft
enthielt urspruenglich ein paar heftige Bugs, fuer dies es inzwischen
Patches gibt.) Diese Container sind angeblich auf Performance bzw.
Speicherverbrauch optimiert, so dass der Programmierer eigentlich nur noch
den passenden Container zu seinem Problem verwenden muss, um performanten
Code zu schreiben.
Zunaechst die Enttaeuschung: Es gibt keinen geheimen Compilerschalter, mit
dem man Delphi (oder Kylix, denn das hier funktioniert auch mit dem
"Delphi fuer Linux") ploetzlich beibringen kann, obige Konstrukte zu
verstehen.
Aber das ist auch gar nicht noetig, denn Delphi kann bereits alles, was
dazu notwendig ist, auch wenn die Syntax etwas kryptischer ist (naja, das
Beispiel oben war auch nicht gerade einfach lesbar, oder?). Alles, was
man braucht, sind ein paar Conditional Defines und Include-Dateien.
(Wenn man es genau nimmt, konnte schon Turbo-Pascal diese Art von Templates,
nur ist damals noch niemand auf diese Idee gekommen.)
Hier ein Beispiel fuer ein einfaches 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}
Wenn man sich die ganzen IFDEFs, DEFINEs etc. wegdenkt, ist dies eine ganz
normale Klassendeklaration in einer ganz normalen Unit. Deklariert wird
eine Klasse _TYPED_OBJECT_LIST_ die eine TObjectList verwendet um Objekte
vom Typ _TYPED_OBJECT_LIST_ITEM_ zu speichern. Um es nicht zu kompliziert
zu machen, habe ich mich auf die beiden wesentlichen Methoden und Properties
(Add und Items) beschraenkt. Die komplette Unit steht zum Download bereit.
Ok, und nun zur Verwendung, die ist noch 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.
Dieses Beispiel deklariert einen Container TMemoList zum typsicheren
(?, gemeint ist type safe) Speichern von TMemo Objekten.
Man kann diese Unit einfach in ein Programm einbinden, eine Instanz von
TMemoList erzeugen und Memos darin speichern. Zugriff auf die
gespeicherten Memos erfolgt ganz bequem ohne Typ-Konvertierung:
SomeMemoList[0].Lines.Text := 'new memo text';
Wie schon gesagt, es geht alles mit rechten Dingen zu. Es werden nur
Funktionen des Delphi-Compilers benutzt, die dieser schon seit Jahrzehnten
in seiner Inkarnation als Turbo-Pascal beherrscht.
Der Trick besteht darin, die Unit zweimal als Include-Datei einzubinden
und dabei mittels Conditional Defines und IF(N)DEFs die jeweils nicht
gewuenschten Teile zu ueberspringen.
Ein {$IFNDEF TYPED_OBJECT_LIST_TEMPLATE} schliesst alle
Teile ein, die der Compiler nie zu Gesicht bekommen soll.
Ein {$IFNDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS} ... {$ENDIF}
schliesst den Teil ein, der nur im ersten Durchgang (Deklaration)
benutzt werden soll, und ein
{$IFDEF TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS} ... {$ENDIF}
schliesslich den Teil, der nur im zweiten Durchgang (Implementation)
benutzt werden soll.
Man haette sich diese Defines auch komplett schenken koennen, indem
man einfach zwei getrennte Include-Dateien verwendet und die Teile, die
komplett unerwuenscht sind, weglaesst. Das hat allerdings zwei Nachteile
-
verteilt man das Template auf zwei Dateien, das ist nicht nur unschoen sondern auch fehlertraechtig.
-
kann man die Template-Unit waehrend der Entwicklung des Templates nicht als normale Unit einbinden, man verschenkt somit die wertvolle Hilfe des Compilers.
Zunaechst nochmal die Template-Unit, in kleine Teilchen zerstueckelt:
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
unit t_TypedObjectList;
interface
uses
Sysutils,
Classes,
Contnrs;
type
_TYPED_OBJECT_LIST_ITEM_ = TObject;
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
Da es nicht wirklich als Unit verwendet wird, wird der Unit-Header inklusive
der Uses-Clause per IFNDEF auskommentiert. Man kann diese Unit waehrend der
Entwicklung ganz normal compilieren um so leicht evtl. Fehler zu finden.
Verwendet man sie aber als Template, so definiert man den Conditional
Define TYPED_OBJECT_LIST_TEMPLATE und veranlasst damit den Compiler
den Unit-Header zu ueberspringen. Ebenso ueberspringt man die Deklaration
von _TYPED_OBJECT_LIST_ITEM_, denn diesen Typ will man ja parametrisieren.
Weiter geht's mit der Typdeklaration des Containers _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}
Hier gibt es einen weiteren Conditional Define
TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS. Dieser sorgt dafuer, dass
dieser Unit-Teil nur beim ersten Include eingebunden wird. Am Ende der
Unit wird dieser Conditional Define definiert, so dass beim zweiten
Include dieser Teil uebersprungen wird. Der Rest ist eine stinklangweilige
Klassendeklaration, wie Du sie sicherlich schon tausendfach gesehen hast.
Lediglich die Typnamen sind etwas ungewohnt. Diese koennen genauso wie die
Namen der Conditional Defines eigentlich frei gewaehlt werden. Ich habe diese
spezielle Form gewaehlt, um die Tatsache zu unterstreichen, dass es sich
nicht um normale Typen handelt.
{$IFNDEF TYPED_OBJECT_LIST_TEMPLATE}
implementation
{$DEFINE TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
{$ENDIF TYPED_OBJECT_LIST_TEMPLATE}
Wieder bewirkt ein Conditional Define, dass dieser Teil der Unit vom
Compiler uebersprungen wird, wenn sie als Template verwendet wird. Dann
wird auch das Define fuer TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS
uebersprungen, denn dieses dient nur dem Test der Unit waehrend der
Entwicklung des Templates.
Die darauf folgende Implementation Section ist wieder recht
langweilig.
{$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}
Auch sie ist wieder von einem IFDEF eingeschlossen,
dem bereits bekannten TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS.
Dies bewirkt, dass der Compiler die Implementation Section beim
ersten Durchlauf komplett ueberspringt und nur die darauffolgende Zeile
{$DEFINE TYPED_OBJECT_LIST_TEMPLATE_SECOND_PASS}
ausfuehrt. Diese sorgt dafuer, dass dieses Define beim naechsten Durchlauf
definiert ist.
Durch die ganzen INCLUDEs und IFDEFs gaukelt man dem Compiler eigentlich
einen ganz anderen Sourcecode vor als der, den man auf den ersten Blick
sieht. Im Folgenden nun das, was der Compiler in Code umwandelt. Es beginnt
mit der Unit u_MemoList, schliesst zweimal die Template-Unit t_TypedObjectList
ein und enthaelt natuerlich immer nur die fuer den Compiler sichtbaren Teile.
Ich hoffe, dass dies fuer alle, die nach meinen Ausfuehrungen immernoch
nicht sicher sind, ob sie verstanden haben, wie es funktioniert, die
Erleuchtung bringt. ;-)
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.
Na, ueberzeugt? Ganz einfach, zumindest aus Compilersicht. ;-)
Ja, das ist eine gute Frage. Ich muss sagen, dass es eine ganze Weile
gedauert hat, bis ich angefangen habe solche Templates zu verwenden.
Ich war so sehr an TList und Konsorten mit
Typ-Konvertierungen (bzw. den 'as' Operator) gewoehnt, dass mir gar nicht
mehr auffiel, wie fehlertraechtig das sein kann. Seit mir das aber
klargeworden ist, haben sich meine Templates vermehrt wie die Karnickel,
insbesondere die, welche verschiedene Container implementieren
((Sorted)Collections, Vectoren, Queues, Stacks).
Das schoene daran ist, dass man den Code nur ein einziges Mal schreiben muss,
die Anwendung ist so einfach, dass man sie auch weniger versierten
Programmierern zumuten kann. Alles, was sie tun muessen ist zwei Bloecke
in ihre Units einzufuegen. In der Interface Section:
{$define TYPED_OBJECT_LIST_TEMPLATE}
type
_TYPED_OBJECT_LIST_ITEM_ = <zu speichernder Typ;
{$INCLUDE 't_TypedObjectList.tpl'}
und in der Implementation Section:
{$INCLUDE 't_TypedObjectList.tpl'}
Diese Bloecke kann man z.B. wunderschoen als Code-Insight Code-Template
speichern. Der reine Anwender der Templates braucht gar nicht zu wissen,
mit welchen Tricks intern gearbeitet wird. Er kann sogar mit dem Debugger
durch den Code durch-steppen.
Dies ist also mitnichten ein unnuetzer Beweis an die C++ Fans, dass es geht,
die praktische Verwendung ist durchaus sinnvoll. Ich hoffe im Laufe der
Zeit eine Template-Bibliothek aufzubauen, die sich halbwegs mit der
STL des C++ Lagers messen kann.
Du wirst vermutlich schon den ein oder anderen Code-generator gsehen oder
sogar benutzt haben. Es gibt welche, die von TList abgeleitete typed List
Klassen erzeugen. Ich mag sie nicht besonders, denn sie erweitern TList zwar
um weitere Methoden und Properties, aber die originalen sind weiterhin
verfuegbar. Dies verstoesst gegen das Objekt orientierte Paradigma, dass
alle nicht noetigen Informationen nicht zugaenglich sein sollten.
Mein erster Versuch in der Richtung war ein eigener Code-Generator, der
auf Vorlagen mit Platzhaltern aufbaute, so dass er sehr flexibel war und
dazu benutzt werden konnte eine breite Palette verschiedener Strukturen
zu generieren.
Leider hat diese Herangehensweise einen grossen Haken: Sie dupliziert den
Code statt ihn wiederzubenutzen. Wenn in der Vorlage ein Fehler war, so
musste man ihn entweder in allen daraus generierten Sourcen fixen oder
man musste alle generierten Sourcen nochmal erzeugen. Beides ist mir zu
fehleranfaellig.
Mit den Pseudo-Templates, die ich hier vorgestellt habe, gibt es diese
Problem nicht, denn ein einfaches Rebuild des Executables uebernimmt den
Bugfix in alle Implementationen eines Templates.
Ich waere niemals alleine auf diese Idee gekommen.
Eines Tages fand ich ein Posting in
borland.public.delphi.objectpascal von Radek Jedrasiak, in welchem er
von C++ aehnlichen Templates fuer Object Pascal schrieb, fuer die er
sich kleine Verbesserung hatte einfallen lassen. Ich fand
das interessant und las den Originalartikel von Rossen Assenov
(http://community.borland.com/article/0,1410,27603,00.html
Anmeldung erforderlich)
sowie die Kommentare dazu. Damit war die Idee geboren.
(c) Copyright 2002 by Thomas Mueller, alle Rechte vorbehalten
This document was generated using AFT v5.095
|