
Ausgehend von einer einfachen Beispielimplementierung zweier Aspekte hat der erste Teil dieses C++-Kurses [1] die verschiedenen Codearten der aspektorientierten Programmierung sowie deren Beziehungen zueinander erläutert. Illustriert wurden außerdem die Vorteile von Techniken wie Vererbung und Namensräume im Zusammenhang mit AOP. Die Listings der Beispielprogramme sind über den iX-FTP-Server verfügbar.
Nachdem bereits besprochen wurde, wie man Klassen und Klassen-Templates um beliebige Aspekte ergänzt, zeigt Listing 1 eine Implementierung des Aspekts Tracing, die sich mit mehreren Klassen verbinden lässt. Vor und nach jeder Member-Funktion fügt das Beispiel Aspektcode einschließlich der Konstruktoren, Destruktoren und verschiedener Operatoren dem fachlichen Code hinzu. Als Fachcode liegen zwei Adapter für numerische Werte vor: Int und Double. Neben den Elementen der orthodox-kanonischen Form, nämlich Standard- und Kopierkonstruktor, Zuweisungsoperator sowie Destruktor, verfügen Int und Double jeweils über einen Typumwandlungskonstruktor und -operator sowie Zugriffsmethoden für das Lesen und Schreiben des gekapselten Wertes.
Tracing wird hier als Klassen-Template implementiert, das von seinem Template-Parameter erbt (Listing 1, Zeile 85 bis 86). Es handelt sich also um ein klassisches Mixin. Dadurch ist der Aspekt nicht länger an eine konkrete Basisklasse gebunden, sondern kann mit allen Klassen verbunden werden, die die gleiche Schnittstelle wie Int aufweisen.
Bei der Implementierung des Kopierkonstruktors (Zeile 94) und des Zuweisungsoperators (Zeile 102) gilt es zu bedenken, dass als rechtes Argument ein Exemplar des mit dem betreffenden Aspekt verbundenen Typs verwendet wird. Diesen Zweck erfüllt ComposedType als Aliasname für Tracing<Base>.
Im Typumwandlungskonstruktor (Zeile 98) der Member-Funktion value() zum Lesen des gekapselten Wertes (Zeile 112) und dem Typumwandlungsoperator (Zeile 122) kommt eine spezielle Technik zum Einsatz: Traits Templates zum Auslesen von Meta-Informationen (siehe Kasten). Die Basisklasse wird dem Template Tracing als Parameter übergeben. Damit ist zwar bekannt, um welche Klasse es sich handelt. Da aber im Falle überladener Funktionen die Typen von Übergabe- und Rückgabeparametern variieren, ist diese Information zunächst nicht zugänglich, sondern muss aus den verfügbaren Daten rekonstruiert werden. Diesem Zweck dienen Traits Templates, wie sie in der C++-Standardbibliothek zu finden sind, beispielsweise in <numeric_limits>.
Die Technik zum Einfügen von Aspektcode vor und nach dem Aufruf einer normalen Member-Funktion hat bereits der erste Teil dieses Kurses demonstriert. Für die Verwendung mit Konstruktoren und dem Destruktor im Zusammenhang mit Vererbung muss sie speziell angepasst werden. Um Code vor einem Konstruktoraufruf auszuführen, machen wir von der C++-Regel Gebrauch, dass der Konstruktor eines geerbten Subobjekts vor dem Konstruktorkörper der abgeleiteten Klasse ausgeführt wird. Deshalb erbt Tracing erst von der Hilfsklasse BeforeConstructor. Der Code, der unmittelbar nach dem Destruktor einer Basisklasse ausgeführt wird, steht im Destruktor einer unmittelbar vor der Basisklasse abgeleiteten geschwisterlichen Klasse. Aus diesem Grund erbt Tracing an zweiter Stelle von der Hilfsklasse AfterDestructor. Erst danach erfolgt die öffentliche Ableitung von der als Template-Parameter beigesteuerten Basisklasse Base. Code, der nach einem Basisklassenkonstruktor ausgeführt wird, steht im Konstruktorkörper der abgeleiteten Klasse, und Code, der vor dem Destruktor der Basisklasse ausgeführt wird, befindet sich im Destruktor der abgeleiteten Klasse.
Ein Beispiel für die Inspektion und Modifikation von Übergabeparametern und Objektzustand zeigt Listing 2. Der Aspekt Positive sorgt dafür, dass Exemplare von Int oder Double nur Werte größer oder gleich Null annehmen können. Wären Hilfsklassen nötig - etwa um Aspektcode vor Konstruktoraufrufen oder nach Destruktoraufrufen einzufügen -, würden deren Konstruktoren die Referenzen auf die Funktionsparameter oder das Exemplar selbst übergeben. Es bestehen also keinerlei Einschränkungen hinsichtlich Art und Umfang der Parameterinspektion oder -manipulation. Ein (uns) unerklärbarer Rest bleibt insofern, als die Member-Funktion value() zum Auslesen des Wertes im Template Positive überschrieben werden muss, da ansonsten VC++ und GNU/Cygnus C++ die Funktion beim Übersetzen nicht eindeutig identifizieren können.
Bei der Programmierung in Java gehört der Umgang mit Ausnahmen sozusagen zum täglich Brot des Entwicklers. C++-Anwendungen verwenden Ausnahmen eher selten, vielleicht deshalb, weil der Compiler keine Verstöße gegen die im Funktionskopf vorgegebene Ausnahmenspezifikation meldet. Entsprechende Verstöße werden erst zur Laufzeit behandelt. Aus diesem Grund verzichteten die Programmbeispiele auf die Spezifikation von Ausnahmen; die Funktionen können daher beliebige Ausnahmen werfen. Dennoch ist es sinnvoll, auch in C++ folgende Möglichkeiten zur Einfügung von Aspektcode zu unterscheiden:
Listing 3 illustriert, wie die in Listing 1 eingeführte Technik angepasst werden kann, um Aspektcode nach fehlerfreier oder fehlerhafter Beendigung einer Funktion auszuführen.
Leicht nachvollziehbar ist die Vorgehensweise zur Entdeckung einer Ausnahme in einer einfachen Member-Funktion wie Int::value(const int&) (Listing 3, Zeile 35). Zu Anfang der Funktion Tracing::value(const int&) wird in bewährter Weise eine Instanz einer Hilfsklasse angelegt (hier AnyOtherMethod, Zeile 112), deren Konstruktor den Aspektcode enthält (Zeile 130), der vor dem eigentlichen Funktionsaufruf einfügt wird. Neu in der Hilfsklasse ist das Flag exceptionInAnyOtherMethod, das im Konstruktor der Hilfsklasse zunächst auf false steht. Innerhalb der Member-Funktion Tracing::value(const valueType&) (Zeile 110 bis 112) wird die überschriebene Member-Funktion Int::value(const int&) im Rahmen eines try-Blocks aufgerufen. Da es in diesem Beispiel genügt, zu wissen, ob während des Aufrufs dieser Member-Funktion eine Ausnahme aufgetreten ist, fängt die Ellipse ... die Exception anonym. Im catch-Block wird anschließend das Flag exceptionInAnyOtherMethod in der Hilfsklasse AnyOtherMethod auf true gesetzt. Nach Abarbeitung der Member-Funktion Tracing::value(const valueType&) wird der Destruktor des Exemplars der Hilfsklasse aufgerufen. In Abhängigkeit des Werts von exceptionInAnyOtherMethod wird nun Aspektcode nach einem erfolgreichen (Zeile 139) oder einem fehlerbehafteten (Zeile 137) Abschluss von Int::value(const int&) ausgeführt. Anschließend läuft der Aspektcode, der immer eingefügt wird (Zeile 140).
Aufwendiger ist es, diese Technik so umzusetzen, dass sie Ausnahmen in Konstruktoren entdeckt. Dies liegt an der Art der Objekterzeugung und -initialisierung in C++ und des damit verbundenen automatischen Aufrufs von Konstruktoren.
Ein Objekt gilt als konstruiert und initialisiert, sobald der Körper eines seiner Konstruktoren vollständig abgearbeitet wurde. Vor seiner Ausführung müssen jedoch zuerst alle geerbten und dann alle aggregierten Subobjekte konstruiert und initialisiert sein. Die Reihenfolge, in der ein Programm geerbte Subobjekte erzeugt, bestimmt sich aus ihrer Anordnung in der Vererbungsbeziehung. Sobald während des Programmablaufs eine Ausnahme auftritt, sieht C++ vor, dass alle seit Beginn des relevanten try-Blocks bis zum Auftreten der Ausnahme erzeugten und initialisierten Objekte wieder zerstört werden. (Aus diesem Grund ist die Verwendung des Operators new in einer Initialisiererliste oder im Körper eines Konstruktors keine gute Idee.)
Die beiden Hilfsklassen BeforeConstructor und AfterDestructor bilden geerbte Subobjekte einer Instanz des Klassen-Templates Tracing (Zeilen 92 bis 93). In der Anordnung der Vererbungsbeziehungen stehen BeforeConstructor und AfterDestructor vorn. Das bedeutet, erst an dritter Stelle soll das Programm ein Subobjekt des Template-Parameters Base, hier also Int, erzeugen. Tritt im Base-Konstruktor ein Fehler auf, gilt das Subobjekt Base als unvollständig erzeugt und wird verworfen, aber nicht zerstört. Das heißt, der Destruktor von Base wird nicht ausgeführt. Ein Aufruf des jeweiligen Destruktors zerstört die bis dahin vollständig konstruierten Subobjekte BeforeConstructor und AfterDestructor, und der Körper des entsprechenden Tracing-Konstruktors wird nicht mehr ausgeführt. Das Tracing-Objekt gilt somit als unvollständig konstruiert und wird verworfen, weswegen sein Destruktor nicht mehr aufgerufen wird. Daher sind keine weiteren Maßnahmen erforderlich, um den Aufruf der Member-Funktion beforeDestructor (Zeile 103) zu verhindern.
BeforeConstructor (Zeilen 67 und 92) unterstellt zunächst eine unvollständige Erzeugung des Subobjekts Base (Int). Deshalb setzt das Programm das Flag exceptionInCtor im Konstruktor von BeforeConstructor auf true und führt dann den vom Konstruktor von Base (Int) eingefügten Aspektcode aus.
Tritt während der Konstruktion von Base (hier Int) ein Fehler auf, soll der Aspekt lediglich Code für den fehlerbehafteten Aufruf eines Konstruktors einfügen, nicht jedoch für den Destruktor, da der Destruktor von Base (hier Int) nicht mehr gerufen wird. Dem Subobjekt AfterDestructor muss daher mitgeteilt werden, dass sein Destruktor keinen Aspektcode ausführen darf. Zu diesem Zweck erhält er bei der Konstruktion eine Referenz auf das von BeforeConstructor geerbte Flag exceptionInCtor (Zeile 97). Das AfterDestructor-Subobjekt hat also Zugriff auf einen Teil des Zustands des geschwisterlichen BeforeConstructor-Subobjekts. Der Destruktor von AfterDestructor führt den Aspectcode nur dann aus, wenn exceptionInCtor den Wert false hat. Leicht zu implementieren ist der Fall, dass während der Ausführung des Konstruktors kein Fehler auftritt. Im Körper eines Tracing-Konstruktors wird exceptionInCtor einfach auf false gesetzt (Zeile 99).
Abschließend bleibt noch zu erklären, welchem Zweck das Flag done in BeforeConstructor dient (Zeile 67). Der Aufruf der Funktion afterConstructor() (Zeile 53 bis 63) erfolgt grundsätzlich im Destruktor von BeforeConstructor (Zeile 73). Wurde ein Konstruktor von Base (hier Int) ohne Erzeugung einer Ausnahme abgeschlossen, wird afterConstructor() bereits zuvor im Konstruktorkörper von Tracing aufgerufen. Das done-Flag stellt deshalb sicher, dass beim zweiten Aufruf durch BeforeConstructor::~BeforeConstructor() nicht erneut der Aspektcode ausgeführt wird. Diese Technik stellt zwar fest, ob eine Ausnahme auftrat. Die Ausnahme kann aber nicht gefangen und inspiziert werden.
In C++ verläuft die Zerstörung eines Objekts in der umgekehrten Reihenfolge wie seine Erzeugung und Initialisierung. Dank des Flags exceptionInCtor der Klasse BeforeConstructor lässt sich feststellen, ob im Konstruktor der Basisklasse eine Ausnahme aufgetreten ist. Ist dies nicht der Fall, setzt der Konstruktor der Aspektklasse das Flag auf false (Zeile 99). Ein vergleichbarer, zuverlässig arbeitender Mechanismus existiert für den Destruktor offenbar nicht. Der Destruktor der abgeleiteten Klasse wird grundsätzlich vor dem der Basisklasse ausgeführt. Der Destruktor eines per multipler Vererbung hinzugefügten und vollständig erzeugten Subobjekts wird zudem völlig unabhängig von dem der Basisklasse Base aufgerufen.
Wir fanden keine Möglichkeit, Code hinzuzufügen, der entweder nur dann läuft oder nur dann nicht, wenn eine Ausnahme im Destruktor der Basisklasse auftritt. Ruft man den Destruktor innerhalb eines try-Blocks explizit auf, lässt sich zwar feststellen, ob eine Ausnahme erzeugt wird. Es ist jedoch nicht möglich, den anschließend folgenden automatischen Destruktoraufruf zu unterdrücken. Daher können wir nur Aspektcode hinzufügen, der vor und nach dem Destruktoraufruf ausgeführt wird.
Ansonsten wurde die Klasse Int so geändert, dass ein Wert kleiner Null beim Aufruf des Typumwandlungskonstruktors zu einer Ausnahme führt (Zeile 21 bis 25). Gleiches gilt, wenn mittels value() ein Wert kleiner Null gesetzt werden soll (Zeile 35 bis 40) oder wenn die Member-Variable v_ beim Aufruf des Destruktors den Wert Null hat (Zeile 26 bis 30).
Zu erwähnen ist der Vollständigkeit halber, dass das Einfügen von Code in virtuellen Member-Funktionen einschließlich eines virtuellen Destruktors auf die gleiche Art und Weise verläuft wie für Member-Funktionen mit früher Bindung.
Die bisherigen Beispielen haben einer Klasse stets nur einen Aspekt hinzugefügt. Im nächsten Schritt gilt es, eine Klasse mit mehreren Aspekten zu verbinden.
Der Verdeutlichung dient das eingangs eingeführte Beispiel. Angenommen, es liegen eine Klasse IntStack und zwei Aspekte Tracing und Checking vor. Folgende fünf Kombinationen sind dann möglich, wobei - wie bereits erwähnt - die beiden letzten eine unterschiedliche Semantik aufweisen:
Werden die beiden Aspekte in der in Listing 1 beschriebenen Form als Klassen-Templates einschließlich eines Traits Template implementiert, um den Typ der Stapelelemente zu ermitteln, wäre es nahe liegend, beispielsweise für die letztgenannte Komposition im Namensraum composed einen Ausdruck der Art typedef aspects::Checking<aspects::Tracing<original::IntStack> > Stack; zu schreiben. Allerdings würde dies zur Fehlermeldung des Compilers führen, dass in der Ausprägung des Template ValueType für aspects::Checking<original::IntStack> kein Member-Typ namens RET existiert. Das ist nicht verwunderlich, denn das Traits Template würde man üblicherweise nur für IntStack, DoubleStack und weitere Basistypen spezialisieren, nicht aber für alle möglichen Kombinationen von Aspekten.
Der Compiler kann nicht wissen, dass aspects::Checking<original::IntStack> eine von IntStack abgeleitete Klasse ist und er deshalb den für IntStack definierten Elementtyp verwenden kann. Es besteht jedoch die Möglichkeit, zur Übersetzungszeit herauszufinden, ob eine Klasse von einer anderen direkt oder indirekt abgeleitet ist. Diesen Vererbungsdetektor hat Andrei Alexandrescu in einer Mitteilung auf comp.lang.c++ veröffentlicht. Er besteht aus den beiden Klassen-Templates InheritDetector und Inherits, deren angepasste Implementierung in Listing 4 auch VC++ übersetzt.
Für eine Template-Meta-Funktion GetType fehlt im Grunde nur noch ein IF, das abhängig von einem booleschen Wert einen von zwei Typen zurückgibt. Listing 4 zeigt eine Implementierung dieser Meta-Verzweigung, die die Klassen SelectThen und SelectElse sowie die Klassen-Templates Selector und IF umfasst (Listing 4, Zeile 32 bis 68). Mit Blick auf VC++ wurden ausschließlich Member-Templates verwendet. IF und weitere Meta-Kontrollstrukturen wie SWITCH erklärt ausführlich der Artikel Aspect-Oriented-Programming im Überblick [[#literatur 3]].
In den Klassen-Templates Tracing und Checking steht nun statt des Traits Template ValueType die Template-Meta-Funktion GetType (Zeile 116 und 130). Die übrigen Teile des Aspektcodes ändern sich nicht. Jetzt können alle oben aufgeführten Kompositionen übersetzt werden. Auch das Hinzufügen eines weiteren Aspektes, was 16 Varianten ergibt, ist ohne weiteres möglich. Es gilt lediglich zu beachten, dass das Konstruktorproblem auftritt, wenn Nicht-Standardkonstruktoren vorkommen.
Wie generative Programmierung die Verbindung von Aspekten mit abgeleiteten Klassen erleichtern kann, demonstriert ein weiteres Listing, das aus Platzgründen hier nicht abgedruckt werden kann, aber (inklusive eines RefID138888_10:erläuternden Textes) bei den Beispielen auf dem iX-Listingsserver zu finden ist.
Grundsätzlich ist es möglich, einen Aspekt als Klassen-Template zu implementieren, das mit beliebigen anderen kombinierbar ist, die die geforderte Schnittstelle aufweisen. Dies schließt auch die Komposition mehrerer Aspekte ein, wobei das Konstruktorproblem auftreten kann. Die Verwendung von Templates als Template-Parameter gestattet eine nachvollziehbare Implementierung, die VC++ allerdings nicht mehr übersetzt. Interessanterweise muss die Komposition der Aspekte dann mehrstufig erfolgen, was an Einschränkungen von GNU/'Cygnus C++ liegen dürfte. Aus Platzgründen wird hier auf den Abdruck des Listings verzichtet. Es ist jedoch im Archiv der Programmquellen enthalten.
Im letzten Teil dieser dreiteiligen Programmierserie wird es darum gehen, wie sich Aspekte mit freien Funktionen verbinden lassen. Die Beispiele gehen dabei sowohl auf nicht rekursive als auch rekursive Aufrufe ein.
Dr.-Ing. Krzysztof Czarnecki
ist im Bereich Softwaretechnik der DaimlerChrysler-Forschung tätig.
Lutz Dominick
arbeitet in der Zentralabteilung Technik der Siemens AG auf dem Gebiet innovativer Softwareentwicklungstechniken.
Dr. Ulrich W. Eisenecker
ist Professor für Informatik an der Fachhochschule Kaiserslautern, Standort Zweibrücken.
Literatur
[1] Krzysztof Czarnecki, Lutz Dominick, Ulrich W. Eisenecker; Erste Aussichten; Aspektorientierte Programmierung in C++: Teil 1; iX 8/2001, S. 143
[2] Krzysztof Czarnecki, Ulrich W. Eisenecker; Generative Programming. Methods, Tools, and Applications; Addison-Wesley, Reading 2000
[3] Lutz Dominick; Aspect-Oriented Programming im Überblick; JavaSPEKTRUM 4/2000, S. 43 ff.
| iX-TRACT |
|
Dieser Text ist der Zeitschriften-Ausgabe 09/2001 von iX entnommen.
Parallelprogrammierung - die Kunst der Multi-Core-Nutzung
Agile ALM - agile Praktiken im Application Lifecycle Management
Webentwicklung - Applikationen für mobile Clients