"Ausleihen" von Objekten
-
Naja, da es C++ nicht AFAIK nicht möglich ist den Zugriff auf eine Variable durch das Übergeben der Variable (z.B. an einen Konstruktor) zu beschränken... wird es wohl oder übel auf "ein Problem durch ein anderes ersetzen" rauslaufen.
Die Zero-Cost Variante wäre wohl "der User soll halt keinen Unfug machen". Alles andere ist irgendwo mit Kosten verbunden. Also das Moven, die Zusätzliche Indirektion über nen Zeiger oder evtl. eine "in use" State Variable im Objekt selbst.
Was auch noch eine Möglichkeit wäre. Quasi
Stream s(...); // hat keine Seek/Read/... Funktionen { StreamAccess a(s); // OK a.Seek(...); a.Read(...); } ZipReader r(s); // Hat intern nen StreamAccess, auch OK, da "a" nicht mehr lebt r.Foo(...); r.Bar(...); StreamAccess a(s); // Boom -> Exception, da "s" noch "in use" ist
Alternativ könnte man der
Stream
Klasse auch die ganzen üblichen Memberfunktionen spendieren, nur dass die halt throwen wenn jmd. anderes exklusiven Zugriff hat.Finde ich aber auch nicht schön. Kann man machen, ist aber einigermassen gewöhnungsbedürftig.
Ich frage mich ob es nicht besser wäre zu sersuchen das Problem auszusitzen - in der Hoffnung dass destructive move, relocation oder etwas vergleichbares bald standardisiert wird
-
Ich hab nochmal über die Zombies nachgedacht und neige nun dazu, Besitztum allein mithilfe von
std::unique_ptr<>
abzubilden:- die Streamklasse sollte nicht moveable sein; also sollte
FileStream::open()
einenstd::unique_ptr<Stream>
zurückgeben - der
GizmoReader
erbt seine Factoryfunktionen von einer Basisklasse:
class GizmoReader : public ReaderBase<GizmoReader> { friend class ReaderBase<GizmoReader>; private: GizmoReader(DataSource&& _dataSource) : ReaderBase<GizmoReader>(std::move(_dataSource)) { } public: Gizmo readGizmo(int arg); };
Hier kapselt
DataSource
die Quelle der Daten (z.B. ein Stream oder Dateiname + Offset); die Factoryfunktionen für z.B. denGizmoReader
gebenconst GizmoReader
zurück, so daß ohne miese Tricks nur rvalue-Nutzung möglich ist:template <typename ReaderT> class ReaderBase { private: DataSource dataSource_; ReaderBase(const ReaderBase&) = delete; ReaderBase& operator =(const ReaderBase&) = delete; protected: ReaderBase(DataSource&& _dataSource) : dataSource_(std::move(_dataSource)) { } ReaderBase(ReaderBase&&) = default; ReaderBase& operator =(ReaderBase&&) = default; const DataSource& dataSource(void) const { return dataSource_; } public: static const ReaderT fromStream(std::unique_ptr<Stream> stream) // übernimmt den Stream { return ReaderT(DataSource::fromStream(std::move(stream))); } static const ReaderT fromStream(Stream& stream) // leiht den Stream nur aus { return ReaderT(DataSource::fromStream(stream)); } static const ReaderT fromFile(std::string filename, std::size_t offset = 0) { return ReaderT(DataSource::fromFile(filename, offset)); } };
- Langlebige Objekte wie ein
ZipFileReader
wären dann genau wieStream
nicht moveable und hätten eine Factoryfunktion, die einenstd::unique_ptr<Stream>
erwartet und einenstd::unique_ptr<ZipFileReader>
zurückgibt. Den Stream kann man sich dann vorübergehend ausleihen oder endgültig zurückholen:
class ZipArchiveReader { private: std::unique_ptr<Stream> stream; ... ZipArchiveReader(const ZipArchiveReader&) = delete; ZipArchiveReader& operator =(const ZipArchiveReader&) = delete; ZipArchiveReader(ZipArchiveReader&&) = delete; ZipArchiveReader& operator =(ZipArchiveReader&&) = delete; ZipArchiveReader(std::unique_ptr<Stream> _stream) : stream(std::move(_stream)) { } public: static std::unique_ptr<ZipArchiveReader> fromStream(std::unique_ptr<Stream> stream); static std::unique_ptr<ZipArchiveReader> fromFile(const std::string& filename); template <typename CallbackT> // auto(Stream& stream) auto withStreamDo(CallbackT&& callback) { return callback(*stream); } static StreamPtr retrieveStream(std::unique_ptr<ZipArchiveReader> archiveReader) { return std::move(archiveReader->stream); } ... };
Das scheint mir im Moment gut genug zu sein.
- die Streamklasse sollte nicht moveable sein; also sollte
-
du brauchst wohl sowas wie eine übergeordente Objektverwaltung. Bei der müssen sich sich die Clients die Objekte abholen und wieder zurückgeben. So ist der Besitzer eines Objekts immer eindeutig definiert.
Die Objekte selbst müssen natürlich so gestaltet sein, dass der temporäre Ausleiher sie nicht in einen inkonsisten Zustand versetzen kann, so dass der nächste Ausleiher nur noch Schrott bekommt.
-
Ich würde vielleicht versuchen, die Klasse (in deinem Beispiel GizmoReader), die ein anderes Objekt bzw eine Referenz (in deinem Fall ein Stream oder Stream&) wrappt, generisch ohne Referenzen zu gestalten, so dass man im Endeffekt alles mögliche reinstecken kann, was bestimmte Stream-Bedigungen erfüllt -- inklusive einer Referenz-Wrapper Klasse.
In der C++ Standardbibliothek gibt es
std::reference_wrapper
für ähnliche Zwecke. Direkt nutzbar wäre es u.a. so:template<class S> // S = Stream oder reference_wrapper<Stream> class GizmoReader { S stream; ... Gizmo read() ... }; template<class S> Gizmo GizmoReader<S>::read() { Stream& s = this->stream; // sollte in jedem Fall für S funktionieren // work with s }
Man muss sich auf
Stream&
natürlich auch nicht festlegen. Aber die erste Zeile inGizmoReader<S>::read
erlaubt es dir wenigestens, um die Definition einer eigenen Referenz-Wrapper-Klasse rumzukommen, in der man sämtliche Methodenaufrufe von Hand "weiterleiten" muss, da man den Dot-Operator noch nicht überladen kann.Die Fragestellung mit dem optionalen Ausleihen eines Streams erinnert mich auch stark an Rust. Dort gibt es in der Standardbibliothek unter
std::io
etwas sehr ähnliches. WennR
ein Reader ist (Ein Reader ist ein Typ, der dasRead
Trait implementiert), dann ist&mut R
(Referenz auf ein R) auch gleichzeitig ein Reader.
-
audacia schrieb:
Ich hab nochmal über die Zombies nachgedacht und neige nun dazu, Besitztum allein mithilfe von
std::unique_ptr<>
abzubilden:- die Streamklasse sollte nicht moveable sein; also sollte
FileStream::open()
einenstd::unique_ptr<Stream>
zurückgeben
Warum? Weil ein
unique_ptr
nullable ist? Weil Du erwartest, dasssizeof(Stream)
besonders groß ist? Weil Du erwartest, dass das Umziehen eines Stream-Objekts zu teuer wäre? Wenn Du alles mit "Nein" beantwortest, sehe ich keine Notwendigkeit vonunique_ptr
. Es ist doch dann nur ein unnötiger "layer of indirection".Bei einem generischen Ansatz -- also
GizmoReader<DS>
mit parameterisierter "DataSource" -- kannst Du dem Ding immer noch einenunique_ptr
unterjubeln, zum Beispiel mitDS=IndirectlyOwnedDataSource<T>
oderDS=BorrowedDataSource<T>
template<class T> class IndirectlyOwnedDataSource { unique_ptr<T> ptr; ... }; template<class T> class BorrowedDataSource { reference_wrapper<T> ref; ... };
T kann (muss aber nicht) in beiden Fällen sogar eine abstrakte Klasse sein, wenn Du noch Laufzeitpolymorphie haben willst.
Hauptsache Du hast ein einheitliche "DataSource/Stream"-Concept.
- die Streamklasse sollte nicht moveable sein; also sollte
-
Andromeda schrieb:
du brauchst wohl sowas wie eine übergeordente Objektverwaltung. Bei der müssen sich sich die Clients die Objekte abholen und wieder zurückgeben. So ist der Besitzer eines Objekts immer eindeutig definiert.
Also sowas wie
StreamAccess
von hustbaer?krümelkacker schrieb:
Ich würde vielleicht versuchen, die Klasse (in deinem Beispiel GizmoReader), die ein anderes Objekt bzw eine Referenz (in deinem Fall ein Stream oder Stream&) wrappt, generisch ohne Referenzen zu gestalten, so dass man im Endeffekt alles mögliche reinstecken kann, was bestimmte Stream-Bedigungen erfüllt -- inklusive einer Referenz-Wrapper Klasse.
Sicher, wenn man eh Template-Code schreibt, ist das eine Möglichkeit. Aber ich finde, header-only libraries werden überbewertet. Ich will eigentlich nicht den ganzen
GizmoReader
templatisieren.krümelkacker schrieb:
Warum? Weil ein unique_ptr nullable ist? Weil Du erwartest, dass sizeof(Stream) besonders groß ist? Weil Du erwartest, dass das Umziehen eines Stream-Objekts zu teuer wäre? Wenn Du alles mit "Nein" beantwortest, sehe ich keine Notwendigkeit von unique_ptr. Es ist doch dann nur ein unnötiger "layer of indirection".
Das dachte ich zuerst auch, aber ich bin mittlerweile zur Ansicht gelangt, daß ich mich lieber nicht mit Zombiestreams herumschlagen will. Wie hustbaer oben sagte: initialized ist schon eine sehr erstrebenswerte Invariante. Außerdem bräuchte ich in jedem Fall eine Indirektion, weil
Stream
typischerweise Laufzeitpolymorphie verwendet.
-
audacia|off schrieb:
Andromeda schrieb:
du brauchst wohl sowas wie eine übergeordente Objektverwaltung. Bei der müssen sich sich die Clients die Objekte abholen und wieder zurückgeben. So ist der Besitzer eines Objekts immer eindeutig definiert.
Also sowas wie
StreamAccess
von hustbaer?Eher so wie ein Betriebssystem seine Ressourcen vergibt, eine Speicherverwaltung seinen Speicher, oder ein Multitasking-Kernel Prozessorzeit, usw. Das Thema ist im Prinzip so alt, wie die elektronische Datenverarbeitung überhaupt.
-
Das ist mir zu abstrakt. Magst du mir mal ein Beispiel anhand von
Stream
undGizmoReader
oderZipFileReader
skizzieren?
-
audacia|off schrieb:
Das ist mir zu abstrakt. Magst du mir mal ein Beispiel anhand von
Stream
undGizmoReader
oderZipFileReader
skizzieren?Ich bringe mal ein Beispiel mit einer Systemkomponenten z.B. der RS232-Schnittstelle. Programm-1 das über RS232 kommunizieren will, ruft die entsprechende Open-Funktion auf. Ist alles okay, bekommt die Anwendung ein 'Handle' aka Objektreferenz, was Programm-1 berechtigt die Schnittstelle zu benutzen. Programm-1 wird sozusagen zum temporären Besitzer der RS232. Hat sie sich quasi ausgeliehen. Kommt nun Programm-2 daher und will auch die RS232 nutzen, hat es Pech. Die Open-Funktion wird scheitern und Programm-2 bekommt keine Erlaubnis. Erst wenn Programm-1 keinen Bedarf mehr hat, und die Close-Funktion aufgerufen hat, bekommen andere Programme wieder die Chance auf die Rs232 zuzugreifen. Da das OS 'weiß' dass Programm-1 die RS232 gerade benutzt, kann es ihm die Schnittstelle auch wieder entziehen, zum Beispiel wenn Programm-1 abgestürzt ist, oder beendet wurde, ohne die Close-Funktion aufzurufen.
Dieses simple Prinzip des "Ausleihens" von Systemressourcen kann man auch im Kleinen anwenden. Kern des ganzen ist eine Softwarekomponente als "Objektmanager", bei dem sich die Clients registrieren und dann bestimmte Objekte anfordern können. Das Ganze lässt sich beliebig ausbauen, z.B. durch eine periodische Abfrage an den Client, ob er das Objekt noch braucht. Antwortet er nicht, wird es ihm entzogen. Was gleichzeitig zu Folge hat, dass jeder Zugriff seinerseits auf die bereits erhaltene Objektreferenz ins Leere läuft.
-
Andromeda schrieb:
Ich bringe mal ein Beispiel mit einer Systemkomponenten z.B. der RS232-Schnittstelle. [...]
Ich glaube, wir reden aneinander vorbei. Mir geht es darum, wie ich Besitztumsverhältnisse in C++ semantisch abbilde. Und du erklärst mir, wie der Ressourcenmanager eines Betriebssystems funktionieren soll