twm's homepage logo
Von einem, der auszog die Heimat schätzen zu lernen ...
Object Pascal (Delphi) Templates
Deutsch English
Google
Search dummzeuch.de
Search WWW

Einleitung

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?

Was ist ein Template?

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.

Templates in Object Pascal

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 funktioniert das?

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

  1. verteilt man das Template auf zwei Dateien, das ist nicht nur unschoen sondern auch fehlertraechtig.
  2. kann man die Template-Unit waehrend der Entwicklung des Templates nicht als normale Unit einbinden, man verschenkt somit die wertvolle Hilfe des Compilers.

Details

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.

Was der Compiler wirklich sieht

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. ;-)

Fazit

Wozu das Ganze?

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.

Warum nicht einen Code-Generator benutzen?

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.

Danksagung

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

letzte Änderung: 2012-10-14 twm
Post to del.icio.us Best Viewed With Open EyesValid XHTML 1.0!