Zugriff auf private-Element aus anderer Klasse
-
Es sei eine Klasse "Class_1" mit einer privaten Struktur.
Wie greife ich aus einer anderen Klasse "Class_2" am geschicktesten auf die Struktur von Class_1 zu?Was mir so durch den Kopf gegangen ist:
- in Class_2 ein friend Class_1 definieren
- in Class_1 eine neue public-Methode erstellen, die als Rückgabewert die Struktur liefert
- die Struktur public machen (ungern)
Kommt etwas davon in Frage oder ist alles Mist? Welche Varianten gibt es noch?
MfG die Fledermaus
(die während ihr antwortet gerade ihr Leben aufs Spiel setzt, um uns vor dem Bösen zu bewahren)PS
Funktioniert es überhaut, über eine public-Methode auf ein private-Elemet zuzugreifen?
-
Hallo,
Kann man nicht allgemein gültig sagen. Rein aus dem Bauch heraus würde ich eine public-Methode implementieren, die eine Kopie der relevanten privaten Daten zurückgibt.
Allerdings kann es durchaus auch empfehlenswert sein, Class_2 als friend von Class_1 zu deklarieren, wenn die beiden Klassen sehr eng zusammenarbeiten und häufig auf private-Datenelemente der anderen Klasse zugreifen müssen, allerdings keine HAT oder IST-Beziehung zwischen ihnen besteht.Funktioniert es überhaut, über eine public-Methode auf ein private-Elemet zuzugreifen?
Was meinst du damit?
Soetwas:
class foo { int val; ... public: int get_value() const { return val; } // gibt kopie von val zurück. };
Zeiger/Referenzen auf interne Klassen-Daten kannst du auch zurückgeben. Dies zeugt tlw. allerdings von schlechtem Design, da so die Datenkapselung übergangen wird, was nicht immer gewünscht ist.
Gruß Caipi
-
wenn du die struktur private machen willst, dann hat das doch sicher einen grund. und dieser grund sollte idr auch dagegen sprechen, die struktur unverändert an den nutzer der klasse weiterzugeben.
wenn du willst, daß der nutzer die struktur nur lesen aber nicht schreiben kann, dann ist es vielleicht das beste, du schreibst eine methode, die eine kopie zurückgibt.
eine gute antwort auf so eine designfrage kann man aber nur geben, wenn man das problem kennt. wenn du die klasse genauer beschreiben würdest, dann könnte man dir hier brauchbarere tips geben.
-
Wie Konfusius schon sagte, eine allgemeingültige Antwort auf solche Fagen gibt es nicht. Immerhin gibt es eine Hitparade der Scheußlichkeiten, die dir bei der Entscheidung helfen kann. Meine persönliche Proritätenliste (von total scheußlich nach weniger scheußlich) wäre in diesem Punkt:
- Public member Variablen
- Rückgabe von nicht-const Referenzen oder Pointern (das ist nichts anderes als public Variablen)
- Friends mit dem einzigen Ziel, auf Variablen zugreifen zu können
- Friends, die auch auf Variablen zugreifen
- const Referenzen oder Pointer
Die Liste ist möglicherweise nicht vollständig - es ist ja schon spät
Für mich persönlich schließe ich alle diese Scheußlichkeiten bis auf die letzte kategorisch aus. Ich würde mich immer fragen, was am Design nicht stimmt, wenn ich in Versuchung käme, sie doch zu verwenden. Auch const Referenzen/Pointer setze ich äußerst sparsam ein. Es gibt meist eine bessere Möglichkeit.
Wenn eine andere Klasse tatsächlich die gesamte struct braucht, würde ich mich als erstes fragen, was mit der Aufgabenverteilung der beiden Klassen nicht stimmt. Meist führt diese Frage zu einem anderen Design. Falls nicht, tja, dann muss man halt in den sauren Apfel beißen....
Stefan.
-
DStefan schrieb:
Wenn eine andere Klasse tatsächlich die gesamte struct braucht, würde ich mich als erstes fragen, was mit der Aufgabenverteilung der beiden Klassen nicht stimmt. Meist führt diese Frage zu einem anderen Design. Falls nicht, tja, dann muss man halt in den sauren Apfel beißen....
Randfrage: Wie würdest du ohne friend das Zusammenspiel zwischen Container und Iterator implementieren? *interessiert ist*
-
CStoll (off) schrieb:
Randfrage: Wie würdest du ohne friend das Zusammenspiel zwischen Container und Iterator implementieren? *interessiert ist*
Auf die Schnelle:
class ContainerNode { // ... }; class ContainerIterator { public: ContainerIterator(ContainerNode *start); // ... }; class Container { public: // ... typedef ContainerIterator iterator; // ... iterator begin() { return iterator(_start); } iterator end() { return iterator(_stop); } private: ContainerNode *_start; ContainerNode *_stop; };
Natürlich müsste man mit Templates arbeiten, aber das ist hier ja nicht von Interesse. Außerdem ist dieser Container wohl eine Liste oder ein Vector, Sets und Maps müssten anders aussehen. Aber friends sind hier wirklich nicht notwendig.
Stefan.
-
sieht nett aus - nur stört daran, daß jeder Nutzer mit ContainerNode-Elementen hantieren könnte. (aus Sicherheitsgründen sollte diese Klasse privat in Container verpackt werden - und schon braucht der Iterator friend-Zugriff, um etwas damit anfangen zu können)
-
ContainerNode braucht nicht private zu sein.
Wiederverwendbarkeit:
zB kann man Listen als Ringe oder durch End und Start Nodes Implementieren.
Beide nutzen die gleichen Nodes.Der Iterator ist nur ein Adapter, welcher das Interface einer speziellen Speichermanagment Implementierung in das einheitliche Iterator Inerface wandelt.
Das bedeutet, dass er nicht die Implementierung kennen mus, sonder nur das Interface.Das Iterator eine nicht const Ref auf den gehaltenen Type anbietet, wiederspricht nicht OO Prinzipien, da man ja nicht die Implementation (Wie der Container den Speicher verwaltet) veröffentlicht.
Man stellt nur ein Interface zur Klasse zur verfügung, welches sagt "du kannst immer schreibenden Zugriff auf das gekapselte Element haben".
das gekapselte Element ist aber dem nutzer bekannt, er hat den Container damit instanziert.
Der Iterator Braucht kein Wissen über das Verhalten der Elemente, über welche er iteriert, sein Wissen beschränkt sich auf das Storage Interface.
-
CStoll (off) schrieb:
sieht nett aus - nur stört daran, daß jeder Nutzer mit ContainerNode-Elementen hantieren könnte. (aus Sicherheitsgründen sollte diese Klasse privat in Container verpackt werden - und schon braucht der Iterator friend-Zugriff, um etwas damit anfangen zu können)
@CStoll (off)
b7f7 hat das wichtigste schon gesagt, ich möchte aber noch etwas zu deinen Sicherheitsgründen anmerken:Wie b7f7 richtig dargestellt hat, öffnen weder ContainerNode noch ContainerIterator die Implementierung von Container für Aussenstehende. Das leuchtet ein, wenn man sich vorstellt, was genau "jeder Nutzer" eigentlich mit den beiden Klassen anfang könnte. Sie wiederverwenden - und das ist ja durchaus erstrebenswert.
Außerdem gebe ich zu bedenken, dass auch die friend-Deklaration (einer Klasse) ein Sicherheitsloch ist. Und was für eins!!! Selbst wenn also die oben skizzierte Implementierung einen Unsicherheitsfaktor darstellen würde, müsste man immer noch abwägen, welches Übel schlimmer ist. Fast immer würde man, meine ich, zu dem Ergebnis kommen, dass friends das größere Übel sind!
Stefan.
-
DStefan schrieb:
Wie b7f7 richtig dargestellt hat, öffnen weder ContainerNode noch ContainerIterator die Implementierung von Container für Aussenstehende. Das leuchtet ein, wenn man sich vorstellt, was genau "jeder Nutzer" eigentlich mit den beiden Klassen anfang könnte. Sie wiederverwenden - und das ist ja durchaus erstrebenswert.
Das Klassenpaket als Ganzes soll auch wiederverwendet werden - aber ich möchte nach Möglichkeit verhindern, daß irgendwer mir etwas selbst-erstelltes als ContainerNode unterschiebt (schließlich stehen in der Klasse Informationen drin, die nur mich etwas angehen). Und außerdem, was machst du, wenn du keinen ContainerNode hast, sondern die Elemente intern in der Container-Klasse speicherst?
Ein Beispiel: Wie würdest du einen Iterator für diese Klasse schreiben?
class intVec { private: int* data; size_t len; public: intVec(size_t l) : data(new int[l]),len(l) {} ... }
Außerdem gebe ich zu bedenken, dass auch die friend-Deklaration (einer Klasse) ein Sicherheitsloch ist. Und was für eins!!!
friend ist, richtig angewendet, überhaupt kein Sicherheitsloch - natürlich darfst du nicht mit Freundschaften um dich werfen
Aber im Regelfall schreibst du selber die Klassen/Funktionen, die friend-Zugriff bekommen sollen, also kannst du sehr gut kontrollieren, wo und wie diese deine privaten Daten anfassen.
-
CStoll schrieb:
Das Klassenpaket als Ganzes soll auch wiederverwendet werden - aber ich möchte nach Möglichkeit verhindern, daß irgendwer mir etwas selbst-erstelltes als ContainerNode unterschiebt (schließlich stehen in der Klasse Informationen drin, die nur mich etwas angehen).
Das verstehe ich nicht: Wie soll dir jemand etwas unterschieben? Du implementierst deinen Container mit ContainerNodes, alles private. Über die public Schnittstelle erhält man nur einen ContainerIterator, der mit einem (privaten!) ContainerNode initialisiert wird. Der Iterator bietet einen von dir kontrollierten Zugriff auf Elemente des Containers. Das ist doch alles sicher. Oder habe ich da etwas missverstanden? Falls ja, kannst du mal ein bischen Code posten, einen tatsächlichen Missbrauch?
CStoll schrieb:
Und außerdem, was machst du, wenn du keinen ContainerNode hast, sondern die Elemente intern in der Container-Klasse speicherst?
Ein Beispiel: Wie würdest du einen Iterator für diese Klasse schreiben?class intVec { private: int* data; size_t len; public: intVec(size_t l) : data(new int[l]),len(l) {} ... }
In diesem Fall erhält der ContainerIterator natürlich einen int*:
class intVec { private: int* data; size_t len; public: intVec(size_t l) : data(new int[l]),len(l) {} ... iterator begin() { return iterator(data); } iterator end() { return iterator(data + len); } }
CStoll schrieb:
friend ist, richtig angewendet, überhaupt kein Sicherheitsloch - natürlich darfst du nicht mit Freundschaften um dich werfen
Aber im Regelfall schreibst du selber die Klassen/Funktionen, die friend-Zugriff bekommen sollen, also kannst du sehr gut kontrollieren, wo und wie diese deine privaten Daten anfassen.Nun, dann möchte ich gern wissen, was du unter einem Sicherheitsloch verstehst. Eine friend Klasse ist nicht die Klasse selbst. Trotzdem kann sie handeln, wie die Klasse selbst. Beispielsweise können Variablen manipuliert werden, ohne dass man das in einer Funktion der Klasse sieht.
Und dass man selbst die Klassen schreibt, die friend zu der anderen Klasse sind, ist nicht gesagt, oder? Und wenn man sie ursprünglich mal selbst geschrieben hat, pflegt sie vielleicht jemand anders - zwei Jahre später (oder so). Und selbst wenn nur ich selbst mit dem ganzen System umgehe, meinst du nicht, dass ich durchaus vergessen haben könnte, was ich mir zu den beteiligten Klassen gedacht hatte? Irgendwann in der Zukunft bei der Pflege des Programms.
friend heißt immer, ich muss noch woanders nachsehen um festzustellen, was mit der Klasse los ist. Beispielsweise ob eine Änderung unerwünschte Nebeneffekte hat. Das nenne ich unsicher!
Stefan.
-
DStefan schrieb:
Das verstehe ich nicht: Wie soll dir jemand etwas unterschieben? Du implementierst deinen Container mit ContainerNodes, alles private. Über die public Schnittstelle erhält man nur einen ContainerIterator, der mit einem (privaten!) ContainerNode initialisiert wird. Der Iterator bietet einen von dir kontrollierten Zugriff auf Elemente des Containers. Das ist doch alles sicher. Oder habe ich da etwas missverstanden? Falls ja, kannst du mal ein bischen Code posten, einen tatsächlichen Missbrauch?
Der ContainerNode ist privat (zumindest der vom Container erstellte), aber wenn die Klasse als solche öffentlich ist, hindert niemand den Nutzer daran, so etwas zu schreiben:
ContainerNode n0; ... //initialisiere n0 mit irgendwelchen Pseudowerten ContainerIterator i0(n0);
Und da der Iterator davon ausgeht, daß der übergebene Node aus einem ordentlichen Container stammt, wird er im besten Fall nicht richtig damit zusammenarbeiten können.
CStoll schrieb:
Und außerdem, was machst du, wenn du keinen ContainerNode hast, sondern die Elemente intern in der Container-Klasse speicherst?
Ein Beispiel: Wie würdest du einen Iterator für diese Klasse schreiben?class intVec { private: int* data; size_t len; public: intVec(size_t l) : data(new int[l]),len(l) {} ... }
In diesem Fall erhält der ContainerIterator natürlich einen int*:
Und wie schützt du den iterator davor, über das Vektorende (oder den Anfang) hinauszurennen?
CStoll schrieb:
friend ist, richtig angewendet, überhaupt kein Sicherheitsloch - natürlich darfst du nicht mit Freundschaften um dich werfen
Aber im Regelfall schreibst du selber die Klassen/Funktionen, die friend-Zugriff bekommen sollen, also kannst du sehr gut kontrollieren, wo und wie diese deine privaten Daten anfassen.Nun, dann möchte ich gern wissen, was du unter einem Sicherheitsloch verstehst. Eine friend Klasse ist nicht die Klasse selbst. Trotzdem kann sie handeln, wie die Klasse selbst. Beispielsweise können Variablen manipuliert werden, ohne dass man das in einer Funktion der Klasse sieht.
Deswegen sagte ich ja "richtig angewendet". Wenn irgendwer auf die internen Daten meiner Klasse zugreifen muß, dann ist mir ein selektiver friend-Zugriff lieber als die Daten public in die Welt hinauszuschreien (wie realisierst du z.B. iterator::op++() oder container::insert(), ohne daß jemand außerhalb des Container-Pakets die Vorgänger/Nachfolger-Pointer das Node beeinflussen kann?)
friend heißt immer, ich muss noch woanders nachsehen um festzustellen, was mit der Klasse los ist. Beispielsweise ob eine Änderung unerwünschte Nebeneffekte hat. Das nenne ich unsicher!
Aber an einer Stelle nachzusehen, wer da was ändern könnte, ist mir deutlich lieber als jeden möglichen Zugriff auf meine Variablen zu überprüfen.
-
CStoll schrieb:
Der ContainerNode ist privat (zumindest der vom Container erstellte), aber wenn die Klasse als solche öffentlich ist, hindert niemand den Nutzer daran, so etwas zu schreiben:
ContainerNode n0; ... //initialisiere n0 mit irgendwelchen Pseudowerten ContainerIterator i0(n0);
Und da der Iterator davon ausgeht, daß der übergebene Node aus einem ordentlichen Container stammt, wird er im besten Fall nicht richtig damit zusammenarbeiten können.
Na und! Das ist doch nicht mein Problem. Und vor allem betrifft das meine Container-Implementierung überhaupt nicht. Bedenke: Es ist doch vollkommen unmöglich, zu verhindern, dass jemand Klassen, die ich geschrieben habe zweckentfremdet. Ich kann nur dafür sorgen, dass die Klassen funktionieren, wie sie sollen, wenn man sie verwendet wie vorgesehen. Und natürlich sollte ich den Input verifizieren, soweit das möglich und vertretbar ist. Somit sollte eine ContainerNode-Implementierung z.B. prüfen, ob der übergebene Pointer nicht 0 ist; ansonsten darf er aber davon ausgehen, dass der Parameter wie erwartet funktioniert. Vergleiche:
std::vector<int> vec1; std::vector<int> vec2; // Fülle die Vectoren (oder auch nicht) std::sort(vec1.begin(), vec2.end()); // ERROR
Kann ich hier erwarten, dass std::sort() sicherstellt, dass beide Iteratoren von demselben Vector kommen? Wohl kaum! Und wie sollte das überhaupt gemacht werden? Es ist Sache des Kunden, die Preconditions zu erfüllen. Tut er das nicht, wasche ich meine Hände in Unschuld.
CStoll schrieb:
Und wie schützt du den iterator davor, über das Vektorende (oder den Anfang) hinauszurennen?
Gar nicht! Das ist nicht meine Sache (s.o.). Natürlich könnte man Container bauen, die sowas verhindern, aber wir diskutieren ja (nehme ich mal an) über STL-artige Klassen, und für die ist sowas explizit nicht vorgesehen. Garbage in ==> Garbage out. Und das ist OK so.
CStoll schrieb:
Wenn irgendwer auf die internen Daten meiner Klasse zugreifen muß...
Nun, ich meine, dass man sich wirklich bemühen sollte einen solchen Zugriff überflüssig zu machen - damit private Daten auch wirklich privat bleiben. Falls es friends gibt (oder auch Unterklassen bei protected Variablen, was ja auch viele OK finden), sind die Daten nicht mehr privat und ich kann ihre Handhabung nicht mehr kontrollieren. Und bitte: Alle meine hier geposteten Lösungsvorschläge funktionieren ohne dass private Daten offengelegt werden.
CStoll schrieb:
dann ist mir ein selektiver friend-Zugriff lieber als die Daten public in die Welt hinauszuschreien
Da bin ich mit dir einer Meinung: public (oder protected) Variablen sind schlimmer als friends (sieh dir meine Liste der Scheußlichkeiten an
).
CStoll schrieb:
(wie realisierst du z.B. iterator::op++() oder container::insert(), ohne daß jemand außerhalb des Container-Pakets die Vorgänger/Nachfolger-Pointer das Node beeinflussen kann?)
Sehr sorgfältig und vorsichtig
wobei der op++() kein Problem sein dürfte. Das macht entweder der iter allein (z.B. beim intVec) oder iter und Node zusammen. Zugriffe mit einem Iterator auf den Container können sehr viel schwieriger sein, sind aber ebenfalls ohne friend lösbar. Natürlich je nach Container-Typ verschieden.
CStoll schrieb:
Aber an einer Stelle nachzusehen, wer da was ändern könnte, ist mir deutlich lieber als jeden möglichen Zugriff auf meine Variablen zu überprüfen.
Entschuldige, aber das ist nicht die Alternative! Es geht doch hier gar nicht um friends contra public Variablen, sondern darum, ob man friends zulassen oder das Prinzip der Kapselung möglichst ernst nehmen sollte. So hab ich die Diskussion jedenfalls verstanden.
Ich möchte betonen, dass ich keineswegs dafür plädiere, das Schlüsselwort friend aus dem C++-Standard zu streichen! Das habe ich schon in meinem ersten Posting in diesem Tread angedeutet. friend kann notwendig sein, um einen Aufwand zu vermeiden, der einfach nicht vertretbar ist. Ich finde lediglich, dass es oft zu leichtsinning eingesetzt wird. Mir scheint, manche Leute (anwesende natürlich ausgenommen
) sehen es als nicht vertretbaren Aufwand an, sich mal ne Stunde lang ein Design zu überlegen, bei dem friend nicht notwendig ist.
Oder anders: Ich finde, dass das Prinzip der Kapselung oft sehr leichtfertig außer Acht gelassen wird - womit wir protected oder gar public Variablen gleich mit im Visier hätten. Das gilt übrigens auch für so manche professionelle/kommerzielle Bibliothek, einschließlich einiger STL-Implementierungen.
Stefan.
-
DStefan schrieb:
CStoll schrieb:
(wie realisierst du z.B. iterator::op++() oder container::insert(), ohne daß jemand außerhalb des Container-Pakets die Vorgänger/Nachfolger-Pointer das Node beeinflussen kann?)
Sehr sorgfältig und vorsichtig
wobei der op++() kein Problem sein dürfte. Das macht entweder der iter allein (z.B. beim intVec) oder iter und Node zusammen.
Und wie wollen sie zusammenarbeiten, ohne auf die gegenseitigen internen Daten zugreifen zu können?
Zugriffe mit einem Iterator auf den Container können sehr viel schwieriger sein, sind aber ebenfalls ohne friend lösbar. Natürlich je nach Container-Typ verschieden.
Nächste Frage: Wie?
Entschuldige, aber das ist nicht die Alternative! Es geht doch hier gar nicht um friends contra public Variablen, sondern darum, ob man friends zulassen oder das Prinzip der Kapselung möglichst ernst nehmen sollte. So hab ich die Diskussion jedenfalls verstanden.
Es geht wohl eher um die Frage, ob sich friends mit der Kapselung vertragen.
(btw, es hindert übrigens niemand deine Kollegen daran, die Containerklasse aufzubohren und nachträglich die internen Daten offenzulegen - Kapselung hin oder her)
Oder anders: Ich finde, dass das Prinzip der Kapselung oft sehr leichtfertig außer Acht gelassen wird - womit wir protected oder gar public Variablen gleich mit im Visier hätten. Das gilt übrigens auch für so manche professionelle/kommerzielle Bibliothek, einschließlich einiger STL-Implementierungen.
Klar, public zerstört die Kapselung, protected ist auch noch gefährlich, aber friend's stellen imho eine Ergänzung meiner Klasse dar (wenn du Angst hast, daß jemand deine Freund-Klassen manipuliert, dann solltest du mit Programmieren aufhören - die Klasse selber kann genauso leicht manipuliert werden ;))
-
Hör mal, du bringst mich aber ganz schön ans Ackern
Diesmal fange ich mit dem Ende an:Es geht wohl eher um die Frage, ob sich friends mit der Kapselung vertragen.
Find ich nicht. Kapselung heißt, dass eine Klasse ihre Implementierungs-Details verbirgt und daraus folgt, dass nur die Klasse selbst mit ihrer Implementierung hantiert. In diesem Fall: auf Membervariablen zugreift. Das ist bei Verwendung von friend nicht sicher gestellt. Also vertragen sich friends nicht mit Kapselung.
(btw, es hindert übrigens niemand deine Kollegen daran, die Containerklasse aufzubohren und nachträglich die internen Daten offenzulegen - Kapselung hin oder her)
und:
(wenn du Angst hast, daß jemand deine Freund-Klassen manipuliert, dann solltest du mit Programmieren aufhören - die Klasse selber kann genauso leicht manipuliert werden)
Klar, jeder kann im Header jedes "private" durch "public" ersetzen, ein const weg casten und was der schlimmen Dinge mehr sind. Bloß trifft das den Punkt? Ich finde nicht! Derartige Konstrukte erfüllen zwei Aufgaben: Der Compiler wird in die Lage versetzt, beim Übersetzen auf Regelverstöße zu prüfen. Er kann mich darauf aufmerksam machen, wenn ich im Irrtum bin. Und ein menschlicher Leser sieht, dass etwas ein Implementierungsdetail ist, nicht verändert werden darf/wird usw. Beides dient dazu, Fehler und Versehen zu vermeiden. Was du oben andeutest, ist aber kein (menschlicher) Fehler und erstrecht kein Versehen, das ist Mutwilligkeit.
Jetzt nimm friend: Die Kapselung wird aufgebrochen und damit eben jenen Fehlern und Versehen (im Gegensatz zu Mutwilligkeit) Tür und Tor geöffnet. Und sei es auch nur etwas wie:
if(myFriend->_privaterPointer = 0) // "=" statt "==" // ...
Das ist menschlich, ein Versehen. Aber wenn ich die Klasse, der _privaterPointer gehört (und die diese Variable private deklariert hat!) debugge, werde ich den Fehler nicht finden. Ich muss mir den Friend ankucken, um den Fehler zu finden. Bitte: Um sowas zu vermeiden, ist doch das Prinzip der Kapselung erst erfunden worden!
Zu der Frage, wie man Container und Iteratoren ohne friend implementieren könnte, folgt ein bischen Code. Ich bitte um Nachsicht, der Code ist nicht getestet, nicht mal compiliert - und innerhalb einer halben Stunde entstanden. Es geht ja nur darum, eine Idee zu skizzieren.
class ContainerNode { public: ContainerNode *prev() { return _prev; } ContainerNode *next() { return _next; } protected: ContainerNode(int data) : _data(data) { } void setNext(ContainerNode *next) { _next = next; } void setPrev(ContainerNode *prev) { _prev = prev; } private: int _data; ContainerNode *_prev; ContainerNode *_next; }; class ContainerIter { public: ContainerIter(ContainerNode *node) : _node(node) { } ContainerIter &operator++() { _node = _node->next(); return *this; } ContainerNode *node() { return _node; }; private: ContainerNode *_node; }; class Container { public: ContainerIter begin() { return ContainerIter(_start); } ContainerIter end() { return ContainerIter(_stop); } void erase(ContainerIter iter) { PrivateNode *tmp = static_cast<PrivateNode *>(iter.node()); // Test auf tmp == _start und/oder tmp == _stop nicht vergessen! tmp->unlink(); delete tmp; } void insert(ContainerIter iter, int data = int()) { PrivateNode *tmp = static_cast<PrivateNode *>(iter.node()); // Auch hier ggf. Test auf tmp == _start und/oder tmp == _stop. tmp->link(new PrivateNode(data)); } private: class PrivateNode: public ContainerNode { public: PrivateNode(int data) : ContainerNode(data) { } void link(PrivateNode *node) { // Der übliche Krempel beim Einfügen eines Knotens (hinter *this). } void unlink() { // Der übliche Krempel beim Entfernen eines Knotens. } }; PrivateNode *_start; PrivateNode *_stop; };
Ich hoffe, das ist verständlich. Die Klasse ContainerNode erlaubt öffentlich den Zugriff auf den nächsten und vorherigen Knoten. Das braucht ContainerIter für die Navigation. Natürlich müsste eigentlich auch Zugriff auf die Daten gewährt werden, aber das ignorieren wir mal. Nur die protected Schnittstelle der Klasse erlaubt auch Änderungen. Dadurch dass Container nur ContainerIter liefert und dieser wiederum nur einen Zugriff auf ContainerNode erlaubt, kann ein Kunde (auf legalem Wege, s.o.) nur für den Container ungefährliche Dinge mit Objekten dieser Klasse anstellen. Es ist nicht möglich, den Container zu korrumpieren.
Intern arbeitet der Container mit PrivateNode, einer Unterklasse von ContainerNode. Die ganze Liste ist also aus PrivateNodes aufgebaut, exportiert wird aber nur ContainerNode. Das erzwing in insert(), erase() usw. einen Cast von ContainerNode auf PrivateNode. Aaaber: Da Container ausschließlich Iteratoren liefert, die mit PrivateNodes erzeugt wurden und da außerdem ausschließlich der Container selbst die Liste verändert, ist dieser Cast sicher. Ein ContainerIter muss zwangsläufig auf einen PrivateNode zeigen, falls er vom Container selbst stammt.
Diese Einschränkung mag dich stören: Was ist, wenn z.B. Container::insert() mit einem ContainerIter aufgerufen wird, der nicht vom Container selbst stammt? Es knallt, klar. Bloß muss mich das nicht stören. Wer sowas tut, hat den Container außerhalb seiner Spezifizierung verwendet und ist an dem Desaster selbst Schuld. Dazu habe ich mich ja schon geäußert. Nimm irgend einen STL-Container und füttere ihn mit einem Iterator, der nicht von ihm selbst stammt. Das knallt auch! Kein Wunder, oder?
Und da es ja hier um friends geht: Was wäre in Bezug auf diese falsche Art der Verwendung gewonnen, wenn ich den ganzen Kram mit friends implementiert hätte? Ich meine gar nichts! Nur dass mit friends jeder falsche Zugriff innerhalb von ContainerNode oder ContainerIter auf Variablen von Container zu Fehlern im Container führen könnte. Und das ist mit dieser Implementierung ausgeschlossen. Versehen/Fehler wirken sich da aus, wo sie gemacht werden. Nicht in einer anderen Klasse.
Stefan.
-
Ich glaube, wir gehen unterschiedlich an die Sache heran: Für dich sind Container, Node und Iterator unabhängig voneinander und sollten möglichst wenig miteinander zu tun haben. Deshalb breuchst du noch irgendwelche privaten Zwischenklassen, die dort eine Verbindung herstellen. Für mich bilden diese Klassen eine Einheit, die nur zusammen einen Sinn ergeben - und auch nur zusammen verwendet werden.
Und dieses static_cast (das du übrigens auch innerhalb der PrivateNode-Methoden brauchst) sieht scheußlicher aus als die Möglichkeit, über friend auf die Daten zuzugreifen.
Und ein menschlicher Leser sieht, dass etwas ein Implementierungsdetail ist, nicht verändert werden darf/wird usw. Beides dient dazu, Fehler und Versehen zu vermeiden.
Ein menschlicher Leser sieht auch den Zusammenhang, wenn irgendwo ein 'friend' steht.