Multiple Aussichten

Zurzeit ist die aspektorientierte Programmierung noch etwas für Experten der Objektorientierung. Reine Theorie ist AOP jedoch nicht mehr, wie dieser dreiteilige Programmierkurs zeigt.

vorlesen Druckansicht
Lesezeit: 22 Min.
Von
  • Krzysztof Czarnecki
  • Lutz Dominick
  • Ulrich W. Eisenecker
Inhaltsverzeichnis

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.

Mehr Infos

Listing 1

Aspektcode vor und nach Aufrufen von Member-Funktionen

  1     namespace original
2 {
3 class Int
4 {
5 public:
6 Int():v_(0) // standard constructor
7 {
8 cout << "Int::Int()" << endl;
9 }
10 Int(const int& v):v_(v) // type conversion constructor
11 {
12 cout << "Int::Int(int)" << endl;
13 }
14 Int(const Int& vh):v_(vh.v_) // copy constructor
15 {
16 cout << "Int::Int(const Int&)" << endl;
17 }
18 Int& operator=(const Int& vh) // assignment operator
19 {
20 cout << "Int& Int::operator=(const Int&)" << endl;
21 v_ = vh.v_;
22 return *this;
23 }
24 ~Int() // destructor
25 {
26 cout << "Int::~Int()" << endl;
27 }
28 const int& value() const
29 {
30 cout << "const int& Int::value() const" << endl;
31 return v_;
32 }
33 void value(const int& v)
34 {
35 cout << "void Int::value(const int&)" << endl;
36 v_ = v;
37 }
38 operator const int&() const // type conversion operator
39 {
40 cout << "Int::operator const int&() const" << endl;
41 return v_;
42 }
43 protected:
44 int v_;
45 };
46
47 class Double
48 // gleicher Aufbau wie Int
49 }
50
51 namespace aspects
52 {
53 template <class T>
54 struct ValueType
55 {};
56
57 template<>
58 struct ValueType<original::Int>
59 {
60 typedef int RET;
61 };
62
63 template<>
64 struct ValueType<original::Double>
65 {
66 typedef double RET;
67 };
68
69 struct BeforeConstructor
70 {
71 BeforeConstructor()
72 {
73 cout << "before any constructor" << endl;
74 }
75 };
76
77 struct AfterDestructor
78 {
79 ~AfterDestructor()
80 {
81 cout << "after destructor" << endl;
82 }
83 };
84
85 template <class Base>
86 class Tracing: private BeforeConstructor, private AfterDestructor, public Base
87 {
88 typedef Tracing<Base> ComposedType;
89 public:
90 Tracing():Base() // standard constructor
91 {
92 afterConstructor();
93 }
94 Tracing(const ComposedType& ct):Base(ct)
95 {
96 afterConstructor();
97 }
98 Tracing(const typename ValueType<Base>::RET& t):Base(t)
99 {
100 afterConstructor();
101 }
102 ComposedType& operator=(const ComposedType& ct)
103 {
104 Assignment a;
105 Base::operator=(ct);
106 return *this;
107 }
108 ~Tracing()
109 {
110 beforeDestructor();
111 }
112 const typename ValueType<Base>::RET& value() const
113 {
114 AnyOtherMethod aom;
115 return Base::value();
116 }
117 void value (const typename ValueType<Base>::RET& v)
118 {
119 AnyOtherMethod aom;
120 Base::value(v);
121 }
122 operator const typename ValueType<Base>::RET&() const
123 {
124 AnyOtherMethod aom;
125 return Base::operator const typename ValueType<Base>::RET&();
126 }
127 private:
128 void afterConstructor() const
129 {
130 cout << "after any constructor" << endl;
131 }
132 void beforeDestructor() const
133 {
134 cout << "before destructor" << endl;
135 }
136 struct Assignment
137 {
138 Assignment()
139 {
140 cout << "before assignment" << endl;
141 }
142 ~Assignment()
143 {
144 cout << "after assignment" << endl;
145 }
146 };
147 struct AnyOtherMethod
148 {
149 AnyOtherMethod()
150 {
151 cout << "before any other method" << endl;
152 }
153 ~AnyOtherMethod()
154 {
155 cout << "before any other method" << endl;
156 }
157 };
158 };

159 }
160
161 namespace composed // computes composed code
162 {
163 // typedef original::Int Int; // Int without any aspect
164 // typedef original::Double Double; // Double without any aspect
165 typedef aspects::Tracing<original::Int> Int; // Int with Tracing
166 typedef aspects::Tracing<original::Double> Double; // Double with Tracing
167 }

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.

Mehr Infos

Listing 2

Inspektion und Manipulation von Funktionsparametern und Objektzustand

 1     // Klassen Int und Double wie in Listing 1
2
3 namespace aspects
4 {
5 // ValueType und Spezialisierungen wie in Listing 1
6
7 template <class Base>
8 class Positive: public Base
9 {
10 typedef Positive<Base> ComposedType;
11 typedef typename ValueType<Base>::RET valueType;
12 valueType correct(const valueType& v)
13 {
14 return (v &lt; 0)? 0 : v;
15 }
16 public:
17 Positive():Base()
18 {}
19 Positive(const ComposedType& ct):Base(ct)
20 {
21 v_ = correct(v_);
22 }
23 Positive(const valueType& v):Base(correct(v))
24 {}
25 Positive& operator=(const ComposedType& ct)
26 {
27 Base::operator=(ct);
28 v_ = correct(v_);
29 return *this;
30 }
31 void value(const valueType& v)
32 {
33 valueType t = correct(v);
34 Base::value(t);
35 }
36 const valueType& value() const
37 {
38 return Base::value();
39 }
40 };

41 }

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:

  • vor dem Funktionsaufruf (gezeigt in Listing 1),
  • nach Ausführung der Funktion ohne Auftreten einer Ausnahme,
  • nach Beendigung der Funktion mit Auftreten einer Ausnahme sowie
  • grundsätzlich nach Beendigung der Funktion (gezeigt in Listing 1).

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.

Mehr Infos

Listing 3

Einfügen von Aspektcode bei erfolgreichem und fehlerhaftem Funktionsaufruf

  1     namespace original
2 {
3
4 class CheckingException: public exception
5 {
6 public:
7 CheckingException (const char* msg):msg_(msg)
8 {}
9 const char* what() const throw()
10 {
11 return msg_;
12 }
13 private:
14 const char* msg_;
15 };
16
17
18 class Int
19 {
20 public:
21 Int(int v):v_(v)
22 {
23 if (value() < 0)
24 throw CheckingException("Exception in Int::Int(int)");
25 }
26 ~Int()
27 {
28 if (value() == 0)
29 throw CheckingException("Exception in Int::~Int()");
30 }
31 const int& value() const
32 {
33 return v_;
34 }
35 void value(const int& v)
36 {
37 if (value() < 0)
38 throw CheckingException("Exception in void Int::value(const int&)");
39 v_ = v;
40 }
41 protected:
42 int v_;
43 };
44 }
45
46 namespace aspects
47 {
48
49 using original:: CheckingException;
50
51 // ValueType und Spezialisierungen wie in Listing 1
52
53 void afterConstructor(const bool& exceptionInCtor,bool& done)
54 {
55 if (!done)
56 { if (exceptionInCtor)
57 cout << "after exceptional return of any constructor" << endl;
58 else
59 cout << "after normal return of any constructor" << endl;
60 cout << "always after any constructor" << endl;
61 done = true;
62 }
63 }
64
65 struct BeforeConstructor
66 {
67 BeforeConstructor():exceptionInCtor(true),done(false)
68 {
69 cout << "before any constructor" << endl;
70 }
71 ~BeforeConstructor()
72 {
73 afterConstructor(exceptionInCtor,done);
74 }
75 bool exceptionInCtor;
76 bool done;
77 };
78
79 struct AfterDestructor
80 {
81 AfterDestructor(const bool& refToCtorException):ctorException(refToCtorException)
82 {}
83 ~AfterDestructor()
84 {
85 if (!ctorException)
86 cout << "always after destructor" << endl;
87 }
88 const bool& ctorException;
89 };
90
91 template <class Base>
92 class Tracing: private BeforeConstructor, private AfterDestructor, public Base
93 {
94 typedef Tracing<Base> ComposedType;
95 typedef typename ValueType<Base>::RET valueType;
96 public:
97 Tracing(const valueType& t):AfterDestructor(exceptionInCtor),Base(t)
98 {
99 afterConstructor(exceptionInCtor = false,done); // no exception -> clear flag
100 }
101 ~Tracing()
102 {
103 beforeDestructor();
104 }
105 const valueType& value() const
106 {
107 AnyOtherMethod aom;
108 return Base::value();
109 }
110 void value (const valueType& v)
111 {
112 AnyOtherMethod aom;
113 try
114 {
115 Base::value(v);
116 }
117 catch(...)
118 {
119 aom.exceptionInAnyOtherMethod = true;
120 throw; // rethrow exception
121 }
122 }
123 private:
124 void beforeDestructor() const
125 {
126 cout << "before destructor" << endl;
127 }
128 struct AnyOtherMethod
129 {
130 AnyOtherMethod():execptionInAnyOtherMethod(false)
131 {
132 cout << "before any other method" << endl;
133 }
134 ~AnyOtherMethod()
135 {
136 if (exceptionInAnyOtherMethod)
137 cout << "after exceptional return of any other method" << endl;
138 else
139 cout << "after normal return of any other method" << endl;
140 cout << "always after any other method" << endl;
141 }
142 bool exceptionInAnyOtherMethod;
143 };
144 };

145 }
146
147 namespace composed
148 {
149 using aspects:: CheckingException; // exporting exception class
150
151 // typedef original::Int Int; // Int without any aspect
152 typedef aspects::Tracing<original::Int> Int; // Int with Tracing
153 }
154

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:

  • IntStack
  • IntStackTracing
  • IntStackChecking
  • IntStackTracingChecking
  • IntStackCheckingTracing

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.

Mehr Infos

Listing 4

Komposition mehrerer Aspekte

  1     namespace original
2 {
3 // Klasse IntStack wie in Listing 2 in iX 8/2001
4 class DoubleStack
5 // gleicher Aufbau wie IntStack
6 }
7
8 namespace aspects
9 {
10
11 // inheritance detector
12 template<class Base>
13 struct InheritDetector
14 {
15 typedef char (&no)[1];
16 typedef char (&yes)[2];
17 static yes test( Base* );
18 static no test( ... );
19 };
20
21 template<class Derived, class Base>
22 struct Inherits
23 {
24 typedef Derived* DP;
25 enum
26 {
27 RET =
28 sizeof( InheritDetector<Base>::test(DP()) ) ==
29 sizeof( InheritDetector<Base>::yes ) };
30 };
31
32 // meta-IF implementation using member-templates
33 struct SelectThen
34 {
35 template<class Then, class Else>
36 struct Result
37 {
38 typedef Then RET;
39 };
40 };
41
42 struct SelectElse
43 {
44 template<class Then, class Else>
45 struct Result
46 {
47 typedef Else RET;
48 };
49 };
50
51 template<bool condition>
52 struct Selector
53 {
54 typedef SelectThen RET;
55 };
56
57 template<>
58 struct Selector<false>
59 {
60 typedef SelectElse RET;
61 };
62
63 template<bool condition, class Then, class Else>
64 struct IF
65 {
66 typedef typename Selector<condition>::RET Selector_;
67 typedef typename Selector_::Result<Then,Else>::RET RET;
68 };
69
70 template <class T>
71 struct ValueType
72 {};
73
74 template<>
75 struct ValueType<original::IntStack>
76 {
77 typedef int RET;
78 };
79
80 template<>
81 struct ValueType<original::DoubleStack>
82 {
83 typedef double RET;
84 };
85
86 template <class T>
87 struct GetType
88 {
89 typedef typename
90 IF<(Inherits<T,original::IntStack>::RET),
91 ValueType<original::IntStack>::RET,
92 ypename IF<(Inherits<T,original::DoubleStack>::RET),
93 ValueType<original::DoubleStack>::RET,
94 void
95 >::RET
96 >::RET RET;
97 };
98
99 // Klassen CheckingException und ReportTracing wie in Listing 2
100
101 class TraceBeforeConstructor
102 {
103 public:
104 TraceBeforeConstructor()
105 {
106 cout << "Before constructor" << endl;
107 }
108 };
109
110 // we do not trace destructor of Base
111
112 template <class Base>
113 class Tracing: private TraceBeforeConstructor, public Base
114 {
115 public:
116 typedef typename GetType<Base>::RET valueType;
117 //
118 void push(const valueType& i)
119 { ReportTracing t("push");
120 Base::push(i);
121 }
122 //
123 };

124
125
126 template <class Base>
127 class Checking: public Base
128 {
129 public:
130 typedef typename GetType<Base>::RET valueType;
131 void push(const valueType& i)
132 {
133 if (size() == capacity())
134 throw CheckingException("stack overflow");
135 Base::push(i);
136 }
137 //

138 };
139
140 }
141
142 namespace composed
143 {
144 // typedef original::IntStack IntStack;
145 // typedef aspects::Tracing<original::IntStack> IntStack;
146 // typedef aspects::Checking<original::IntStack> IntStack;
147 // typedef aspects::Checking<aspects::Tracing<original::IntStack> > IntStack;
148 typedef aspects::Tracing<aspects::Checking<original::IntStack> > IntStack;
149
150 // typedef original::DoubleStack DoubleStack;
151 // typedef aspects::Tracing<original::DoubleStack> DoubleStack;
152 // typedef aspects::Checking<original::DoubleStack> DoubleStack;
153 // typedef aspects::Checking<aspects::Tracing<original::DoubleStack> > DoubleStack;
154 typedef aspects::Tracing<aspects::Checking<original::DoubleStack> > DoubleStack;
155 }

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’ [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.

[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.

Mehr Infos

iX-TRACT

  • Basis der aspektorientierten Programmierung ist die Modularisierung von Aspektcode und seine spätere Verbindung mit fachlichem Code; dabei lassen sich einzelne Aspekte mit mehreren Klassen verbinden.
  • Prinzipiell ist eine Fehlerbehandlung in aspektorientierten Programmen möglich, allerdings ist es aufwendig, das Auftreten von Ausnahmen in Konstruktoren zu entdecken.
  • Da sich bei einer Ausnahme im Destruktor einer Basisklasse der anschließende Destruktoraufruf nicht unterdrücken lässt, kann Aspektcode nur vor und nach dem Destruktoraufruf ausgeführt werden.