Finalisierung hinzufügen

Christoph Karl Walter Grein

Löse niemals Rätsel mit direktem Zugang zur Hölle...

Mit seinen etikettierten (tagged) Typen und der dazugehörigen Dynamik der Bindung ist Ada95 eine sehr mächtige Sprache geworden. Fast nach Belieben können neue Eigenschaften zu vorgegebenen Typen einer Klasse hinzugefügt werden. Damit scheint eine gewisse Beliebigkeit mit dem Ausgangspunkt einer Typhierarchie (der Wurzel einer Klasse) verbunden zu sein. Diese Beliebigkeit ist jedoch nur scheinbar gegeben, und man muss äußerst sorgfältig den Ausgangspunkt wählen, will man am Ende nicht in Schwierigkeiten geraten. Diese Tatsache wird schlaglichtartig durch folgendes Problem über kontrollierte Typen aufgezeigt, das ich vor längerer Zeit im Ada-Internet-Forum Chat@Gnat.com fand und von dem ich denke, dass es von allgemeinerem Interesse sein könnte.

Kurz gesagt:

1. Einführung

Ursprünglich stammt das Problem von Heath White (die Adresse <white@titian.jeol.com> ist mittlerweile ungültig), der schrieb: "One of the main headaches this causes occurs when you already have a tagged hierarchy of types. You want to derive off from it, but you want the new type to have controlled behavior. There is no good way to do this."

Robert I. Eachus hielt dagegen: "Of course there is, but in part it is non-intuitive ... If you are adding a component that needs to be controlled, the controlledness is a property of the component, and the language supports that correctly. It has to, since you might not be aware that the component you are adding to a non-controlled class is controlled.

The more 'troubling' case is when the addition of a component creates a need to finalize the containing structure. Access discriminants allow the component to see the containing structure.

I keep meaning to create an abstraction which deals with the most common sort of such extensions separate from other 'building block' tagged components. It is less than twenty lines long but choosing some of those lines correctly takes thought. Hmmm ...

This version has the side effect of making the extended type limited. The body for a non-limited version is much more complex because you have to use a record with an access value as a non-discriminant, then track assignment and copying using Initialize and Adjust. (Left as an exercise for the reader.)

There is however one 'can't be avoided' problem. The order in which the finalization of two extensions will be called is determined in part by the subclassing order (see RM 7.6.1(9)) and in part by whether or not there is an access discriminant. If you have too many components trying to finalize things, the ordering can get to be a problem."

2. Lösungen

2.1 Limitierte Typen

Eachus' Lösung für die Klasse der limitierten Typen findet sich hier. Die einzige Änderung gegenüber dem Original ist, daß ich sie zu einem Unterpaket von

gemacht habe, so dass Geschwisterpakete andere Typklassen behandeln können.

Der Kode sieht recht harmlos aus und man möchte kaum glauben, dass die Lösung für den unlimitierten Fall so "höllisch" ist, wie der Problemsteller Heath White es ausdrückte. Es erscheint mir sinnvoll, hier auf eine Regel in RM 3.9.1(3) hinzuweisen, die erzwingt, dass der aktuelle Typ für den formellen limitierten Typen Uncontrolled ebenfalls limitiert sein muss [AARM 3.9.1(3.a) erklärt warum], so dass der Wunsch nach einer unlimitierten Lösung durchaus berechtigt erscheint.

Dem aufmerksamen Leser wird aufgefallen sein, dass dem Eachus'schen Paket etwas fehlt: die Operation Initialize. Fügen wir also noch den generischen Parameter

hinzu. Der Leser möge sich vor dem Weiterlesen an der Implementierung versuchen - sie ist nicht ganz trivial.1

2.2 Unlimitierte Typen, sequentielle Umgebung

Meine Versuche, eine Lösung zu finden, führten zu einer längeren Diskussion im Netz. Wer sich Gedanken darüber macht, stößt bald auf das erste der zwei großen Probleme: Wie bringt man den Zugriffswert des gerade finalisierten Objekts in Adjust? Man schlage hierzu in den Bemerkungen des Annotated Ada RM 7.6 (17a..e) nach: Adjust weiß absolut nichts über das Ziel der Zuweisung. Auch die Ada Rationale (4.6.3) weist auf das Problem hin mit der Bemerkung, Access-Diskriminanten seien genau deshalb nur für limitierte Typen verfügbar, um Probleme beim Kopieren selbstreferenzierender Objekte zu vermeiden.

Die einzige Lösung scheint mir ein globaler Speicher zu sein, in dem Finalize vorübergehend den Zugriffswert ablegt und von wo ihn danach Adjust abholt. Das ist ein wenig unschön, widerspricht es doch der Vorstellung, alles innerhalb des Objekts zu verbergen - und was schlimmer ist: In einer Umgebung mit mehreren Prozessen kann das nicht sicher funktionieren!

Aber bleiben wir vorerst in einer rein sequentiellen Umgebung. Es bietet sich also an, etwas wie das hier Gezeigte zu versuchen. Abgesehen von dem unschönen Speicher2 ist der Kode dem vorigen sehr ähnlich und geradlinig. Die ganze Sache hat nur einen Haken, und das ist das zweite große Problem: Was nehmen wir für ein Access-Attribut her?

Die Zugänglichkeitsregeln verbieten, dass ein Objekt einer tieferen Zugänglichkeitsebene angehört als der [anonyme] Zugriffstyp, der durch das einfache 'Access-Attribut bezeichnet wird. Damit wird verhindert, dass ein Zeiger das Objekt, worauf er verweist, überleben kann, er sozusagen frei in der Luft hängen bleibt.

'Unchecked_Access umgeht diese Prüfung der Zugänglichkeit. Innerhalb des Objekts ist das absolut sicher, denn der Zeiger kann niemals das, worauf er verweist, überleben. Ein früher Ada95-Compiler ließ mich auch prompt in diese Falle tappen, indem er das Paket brav übersetzte und das zugehörige Testprogramm ausführte. Erst eine spätere Version eines anderen Compilers wies das Paket zu Recht zurück mit dem Verweis auf RM 13.10 (3) (Definition in 3.10 (9)): Der Präfix des Attributs 'Unchecked_Access muss eine Alias-Sicht sein.

Damit wären wir wieder am Anfang angelangt. Ich habe keine Lösung gefunden, die im Rahmen des Ada95-Standards bleibt. Vielleicht findet ein Leser einen Weg - Verbesserungen sind willkommen.

So bleibt nur die Hoffnung auf Compiler-abhängige Lösungen (wenn es solche gibt). Gnat bietet das Attribut 'Unrestricted_Access an, das in meinem Fall zufriedenstelled arbeitete. Eachus drückte seine Hoffnungen folgendermaßen aus: "If we can come up with an implementation independent package spec, then we can expect the various vendors to publish and maintain the package bodies. Yes, we might have to write the original versions, but the vendors are usually willing to maintain (i.e. not break) working code from real customers."

2.3 Unlimitierte Typen, Prozessumgebung

Wie schon oben erwähnt wurde, kann dieses Paket nur verwendet werden, wenn die Typhierarchie, die damit konstruiert wird, auf einen Prozess beschränkt bleibt. In einer Mehrprozessumgebung müssten wir aus Finalize und Adjust ein unzertrennliches Zwillingspaar machen, um zu verhindern, dass sich ein anderer Prozess dazwischenklemmt und den Inhalt des globalen Speichers überschreibt. Aber genau wie Adjust nichts vom Ziel einer Zuweisung weiß, so weiß auch Finalize nichts davon, ob ein solcher Aufruf von Adjust folgt oder nicht (das heißt, es weiß nicht, ob die Finalisierung innerhalb einer Zuweisung für das Objekt auf der linken Seite erfolgt oder ob das zu finalisierende Objekt gerade seinen Gültigkeitsbereich verlässt). Daher würde es auch nichts helfen, den globalen Speicher in ein geschütztes (protected) Objekt einzuschließen. Es sieht ganz danach aus, als brauchten wir einen separaten Speicher für jeden Prozess in der Partition. Jeder Prozess würde dann seinen eigenen Speicher in rein sequentieller Weise verwenden, und das ist wieder sicher.

Wir haben das Problem jetzt darauf reduziert, einen Weg zu finden, jeden Prozess zu identifizieren und seine Identität als Schlüssel zum Speicher zu verwenden. Das Problem wird allerdings durch die Tatsache erschwert, dass im allgemeinen Fall in einer Partition Prozesse dynamisch ins Leben gerufen werden können und auch wieder terminieren. Meinem Gefühl nach entpuppt sich dies als reichlich unhandliches Problem, für das ich keine Lösung gefunden habe - es sei denn, man ist so glücklich, über einen Compiler zu verfügen, der RM Annex C erfüllt (Gnat erfüllt alle Annexe). [Man beachte, dass dieser Annex nicht zum Sprachkern gehört.]

Annex C stellt uns genau das zur Verfügung, was wir benötigen: Das Bibliothekspaket Ada.Task_Identification [RM C.7.1] erlaubt es uns, die Identität jeden Prozesses zu erfragen. Wir könnten eine Liste mit Prozess-IDs konstruieren, in die sich jeder Prozess bei Geburt ein- und beim Tod wieder austragen müsste. Finalize müsste dann diese Liste nach der ID des aktuellen Prozesses durchsuchen und den Zugriffswert abspeichern, worauf Adjust erneut die Liste zu durchsuchen hätte, um den Wert wiederzugewinnen. Das Ganze wird wieder sehr viel komplizierter, als man anfangs ahnen konnte. Allerdings kann man dieses Vorgehen auch schwerlich als Lösung des Problems bezeichnen, denn es erfordert einen Eingriff in das Prozesssystem des bestehenden Programms, der mindestens so groß ist wie die Neukonstruktion des Typhierarchie mit Kontrolliertheit von Anbeginn an - und genau solche Eingriffe gilt es ja zu vermeiden.

Glücklicherweise stellt sich all das als unnötig heraus. Wieder stellt RM C.7.2 genau das Gesuchte zur Verfügung: Das Paket Task_Attributes ermöglicht uns, jedem Prozess in der Partition ein Attribut zuzuweisen, das den Zugriffswert des gerade finalisierten Objekts enthält, von wo Adjust ihn holen kann. [Mit der Implementierungserlaubnis gemäß RM C.7.2(28) brauchen Prozesse, die die Typhierarchie nicht verwenden, das Attribut nicht einmal zu speichern.]

Hier ist der endgültige Kode zu finden.

3. Weitervererbung

An Hand eines einfachen Beispiels wollen wir das Verfahren überprüfen: Es sei eine abstrakte Basisklasse Root gegeben, von der eine ganze Hierarchie von unkontrollierten Typen abgeleitet sei, die hier vom Typ Parent vertreten werde. Hiervon soll eine kontrollierte Hierarchie abgeleitet werden. Dazu wird zuerst der noch unkontrollierte Typ Derived definiert mit allen seinen Komponenten, der dann mit Hilfe einer Ausprägung Add_Control unseres Pakets kontrolliert gemacht wird: Controlled kann nun als Basis einer kontrollierten Hierarchie Controlled'Class dienen, die durch den Typ Final repräsentiert werde.

Für Objekte vom Typ Controlled wird beim Verlassen des Gültigkeitsbereichs die Operation Finalize aufgerufen:

So weit verläuft alles wie gewünscht.

Zu unserem Leidwesen müssen wir jedoch feststellen, dass das Verfahren nicht für Objekte vom Typ Final funktioniert. Zwar werden auch sie beim Verlassen des Gültigkeitsbereichs finalisiert, doch ist es nicht die erwartete neue Operation Initialize (Final'(F)), sondern das alte Initialize (Derived (F)), das verwendet wird. (Dem gewitzten Leser wird allerdings aufgefallen sein, dass eben das auch schon für Objekte vom Typ Controlled gilt.)

Der Grund hierfür ist die verflixte aber unumgängliche Typumwandlung im Rumpf der Prozedur Add_Finalization.To_Limited_Uncontrolled.Finalize.

Die Lösung dieses Problems ist vergleichsweise einfach, wenn man sich vor Augen hält, dass Typumwandlungen für etikettierte Typen stets nur Konvertierungen zwischen unterschiedlichen Sichten (view conversion) des Objekts sind, das Etikett (tag) des Objekts dagegen stets unverändert bleibt. Wenn wir also die generische Parameterprozedur

durch folgende ersetzen:

können wir im Rumpf der aktuellen Prozedur den Aufruf weiterverteilen:

Endlich haben wir unser Ziel erreicht:

Man beachte daß Finalize weder für P (natürlich nicht) noch D aufgerufen wird. (Objekte des abstrakten Typs Root können nicht deklariert werden.)

4. Schlussfolgerung

Das hier vorgestellte Problem stellt sich naturgemäß nur für sehr große Programme, wo es nicht so einfach ist, einen Typen neu zu definieren (möglicherweise hängen Programmteile von ihm ab, die von unterschiedlichen Teams in verschiedenen Ländern entwickelt werden) - und eine Neuübersetzung von mehreren hundertausend Programmzeilen ist auch nicht so ohne weiteres möglich.

Wie wir jedoch gesehen haben, ist eine allgemeine Lösung nur für limitierte Typen möglich; für unlimitierte Typen müssen wir auf ein compiler-abhängiges Attribut (Gnat) zurückgreifen, und auch damit ist die Lösung äußerst verzwickt und nicht ohne Probleme.

Lassen wir Tucker Taft, den Vater von Ada95, sprechen: "We made no attempt to solve this problem in Ada 95, so it would not surprise me if it were impossible. My contention was that each component should, in so far as possible, clean up after 'itself.' Being able to 'add' controlled-ness later in a hierarchy did not seem to be that important."

Es gibt somit nur einen gangbaren Weg aus dem Dilemma (und das ist auch der, den White gewählt hat): "In our code, we have just structured it so this problem doesn't arise - we've just made entire tagged hierarchies controlled, and that's that."

Und auch während der Entwicklung sehr großer Programme gibt es immer wieder Phasen, wo Umstrukturierungen leichter möglich sind. In Tucker Tafts Worten: "That doesn't seem like an unreasonable solution. In general, there are times when you do have to reorganize things to deal with certain kinds of enhancements. This may be one of them ... In any case, I don't really recommend these tricks. I would suggest you just go back and make the root type controlled if you don't find using controlled components sufficiently flexible."


Als Post Scriptum möchte ich hinzufügen, dass noch weitere Probleme verborgen sind. Vielleicht hat ein Leser das Bedürfnis nach einem Konstruktor verspürt, der im obigen Kode fehlt. Man könnte ihn zum sichtbaren Teil des Pakets Add_Finalization.To_Uncontrolled hinzufügen (als primitive Operation oder als nicht-primitive - man bedenke allerdings, dass primitive Functionen unter Vererbung abstrakt werden) mit folgendem Rumpf:

aber dieser Versuch muss fehlschlagen, wenn die Funktion als Initialwert in einer Deklaration verwendet wird. Der Grund dafür liegt in der Unterscheidung von Zuweisungsanweisungen (assignment statements) und Zuweisungsoperationen (assignment operations) im RM. Im Kodefragment

erfolgt die Initialisierung von C durch eine Zuweisungsoperation ohne vorangehende Finalisierung, so dass C nicht korrekt auf sich selbst verweist, d.h. anstatt auf C zeigt C.Controller.Parent in die Irre.

Noch eine weitere Überraschung musste ich erleben. Mit der oben gezeigten Typhierarchie stellte ich mit Verblüffung fest, dass ein Objekt vom Typ Controlled direkt bei der Deklaration finalisiert wurde:

wobei alle voreingestellten Anfangswerte, die wir für die Komponenten des Verbundes Derived definiert haben könnten, zerstört werden. Dies hat mit AI95-147 zu tun, der sich mit erlaubten Optimierungen beschäftigt, wenn Aggregate von kontrollierten Typen als Initialwerte verwendet werden. Nun bin ich kein Ada-Sprachadvokat, um zu entscheiden, ob Gnat 3.10p1 sich im Irrtum befindet, wenn er ein Objekt direkt bei der Deklaration finalisiert, aber ich konnte den Grund dieser unerwarteten Finalisierung zurückverfolgen bis zu der Deklaration

Hier haben wir ein Aggregat, das einem (vorübergehenden) anonymen Objekt zugewiesen werden darf, welches danach finalisiert werden muss. Ach o je - Finalize für ein Objekt vom Typ Component ruft Finalize für das durch den Zugriffswert referenzierte Objekt, und das ist genau mein Objekt X!

Ich sehe allerdings nicht die direkte Verbindung zum oben zitierten AI, denn auch das RM erlaubt schon, das anonyme Objekt für das Aggregat wegzuoptimieren. Wenn also Gnat es wegoptimierte (zusammen mit dem zugehörigen Adjust/Finalize-Paar), sollte (wenn meine Analyse korrekt ist) auch die unerwartete Finalisierung verschwinden.


Korrekturen

1 Das ist ein Irrtum in der Originalveröffentlichung. Für limitierte Typen ist das natürlich trivial. Schwierig ist es nur für unlimitierte Typen.

2 Robert A Duff hat mich am 13. Juni 2002 darauf hingewiesen, dass es selbst im sequentiellen Fall ein Problem gibt. Nehmen wir an, es gebe Verbunde X und Y, die zwei dieser Dinge enthalten. Wenn wir dann sagen:

    X := Y;
werden beide Komponenten von X finalisiert, so dass nachher eine Komponente auf die andere zeigt.


Der gesamte Kode zusammen mit einigen Testprogrammen und auch dieser Text (als MS-Word-Dokument) können im zip-Format runtergeladen werden, so daß niemand, der selbst experimentieren möchte, ihn einzuhacken braucht.
Die Datei Test_Add_Finalization_To_Limited_Uncontrolled.Result.txt für die brauchbare Variante ist am 20. September 2004 mit Ergebnissen für neuere Kompiler aktualisiert worden. Wenn Ihr bevorzugter Kompiler nicht diese Ergebnisse erzeugt, hat er ernsthafte Probleme mit der Finalisierung. (Ich habe mir allerdings nicht die Mühe gemacht, die unlimitierte Variante zu wiederholen, da sie sowieso nicht funktionieren kann.)


Ada 2005 RM 3.9.1(3/2): Im Unterschied zu Ada 95 kann jetzt eine Typerweiterung auch in inneren Geltungsbereichen stattfinden. Ein Beispiel hierfür ist neu hinzugefügt.

Das Schlüsselwort overriding wurde, wo passend, zu Kode hinzugefügt. Für nicht-Ada-2005-Übersetzer muss es wieder entfernt werden.


Erschienen (auf Englisch) in Ada Letters, Volume XIX, Number 4, December 1999.

Text auf Englisch.


Inhaltsverzeichnis
Contents