"Ausleihen" von Objekten



  • hustbaer schrieb:

    Grundsätzlich, ja, mit dem Callback kann man das schon machen. Ohne rum-gemove hindert den User aber auch niemand daran den Stream trotzdem zu verwenden.

    Bei der Callback-Variante nehme ich natürlich an, daß der ZipFileReader -Konstruktor einen Stream&& erwartet, sonst macht die ja gar keinen Sinn. Sorry für die Verwirrung, ich dachte, das wäre aus dem Kontext klar gewesen.

    hustbaer schrieb:

    Bleibt also IMO nur noch die "move" Variante. Und davon bin ich kein grosser Fan. Weil ich "initialized" als Invariante eigentlich sehr cool finde. Und Move-Semantik nimmt mir das - zumindest bei Klassen die nicht sowieso schon einen sinnvollen/natürlichen "empty" Zustang haben.

    Das stimmt. Ich benutze Movesemantik recht häufig (ist halt schon praktisch), aber meistens mit Bauchschmerzen, weil meine Objekte dadurch einen Zombiezustand erhalten.

    Idealerweise sollte der Compiler sehen können, ob ein Objekt noch "lebt", und ggf. den Zugriff darauf verweigern. (Wird das nicht bei Rust so gemacht? Wobei ich mich frage, wie die dann mit nicht zur Compilierzeit entscheidbaren Fällen umgehen.) Solange er das nicht kann, muß ich mit Zombieobjekten leben. Immerhin haben die den Vorteil, daß ihre Benutzung i.d.R. fehlschlägt (entweder implizit, d.h. ein Zombiestream würde bei tryRead() / tryWrite() immer "0 Elemente" zurückgeben, oder explizit à la ObjectDisposedException ). Und lieber ein Laufzeitfehler mit Callstack als falsche Ergebnisse.

    Wie aber würde man das Übertragen von Besitztum abbilden, wenn man nicht die Move-Semantik dazu gebrauchen will? unique_ptr<> würde gehen, das ersetzt das Zombieproblem der Move-Semantik durch die Nullzeigerproblem. Sonst fällt mir aber nichts ein.



  • 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() einen std::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. den GizmoReader geben const 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 wie Stream nicht moveable und hätten eine Factoryfunktion, die einen std::unique_ptr<Stream> erwartet und einen std::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.



  • @Audacia,

    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 in GizmoReader<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. Wenn R ein Reader ist (Ein Reader ist ein Typ, der das Read 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() einen std::unique_ptr<Stream> zurückgeben

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

    Bei einem generischen Ansatz -- also GizmoReader<DS> mit parameterisierter "DataSource" -- kannst Du dem Ding immer noch einen unique_ptr unterjubeln, zum Beispiel mit DS=IndirectlyOwnedDataSource<T> oder DS=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.



  • 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 und GizmoReader oder ZipFileReader skizzieren?



  • audacia|off schrieb:

    Das ist mir zu abstrakt. Magst du mir mal ein Beispiel anhand von Stream und GizmoReader oder ZipFileReader 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 😕


Anmelden zum Antworten