"Ausleihen" von Objekten



  • Angenommen, ich habe ein veränderliches Objekt, das ich zeitweise an andere Objekte "ausleihen" möchte. Naheliegendes Beispiel wäre ein Streamobjekt:

    Stream stream = FileStream::open(..., "rb");
    Gizmo gizmo = GizmoReader::fromStream(stream) // erwartet Stream&
        .readGizmo(1337);
    

    Hier gibt es zwei Probleme. Einerseits könnte es ja sein, daß GizmoReader länger lebt als stream ; dann speichert GizmoReader eine Referenz auf ein nicht mehr existierendes Objekt:

    Stream stream = FileStream::open(..., "rb");
    return GizmoReader::fromStream(stream); // erwartet Stream&, verweist aber auf lokale Variable!
    

    Andererseits hat der Stream einen Zustand (die Position), auf den sich der GizmoReader verläßt; weil ich nach der Konstruktion des GizmoReader immer noch Zugriff auf das Streamobjekt habe, kann ich aber auch die Streamposition verändern und damit eine Invariante des GizmoReader verletzen, nämlich daß die Streamposition beim Eintritt in readGizmo() dieselbe ist wie nach dem Verlassen des GizmoReader -Konstruktors:

    GizmoReader gizmoReader = GizmoReader::fromStream(stream); // erwartet Stream&
    stream.seek(42, SEEK_CUR); // <-- verletzt Invariante von GizmoReader
    Gizmo gizmo = gizmoReader.readGizmo(1337); // <-- inkonsistenter Zustand
    

    Beides würde ich gerne verhindern. Eine naheliegende Lösung wäre, daß der GizmoReader -Konstruktor eine rvalue-Referenz erwartet und der Stream also in seinen Besitz übergeht:

    GizmoReader gizmoReader = GizmoReader::fromStream(std::move(stream)); // erwartet Stream&&
    

    Das ist wiederum unschön, weil der Stream ja eigentlich mir gehört und ich noch etwas damit vorhabe, nachdem ich das gizmo daraus gelesen habe. Ich könnte also eine Methode einführen, mit der ich mir den Stream zurücknehmen kann, wenn ich fertig bin:

    GizmoReader gizmoReader = GizmoReader::fromStream(std::move(stream)); // erwartet Stream&&
    Gizmo gizmo = gizmoReader.readGizmo(1337);
    stream = std::move(gizmoReader).retrieveStream(); // rvalue-Methode
    

    Jedoch ist das nicht nur unbequem, weil ich dem gizmoReader so immer einen Namen geben muß, sondern es verschafft mir auch ein Problem mit der Exceptionsicherheit; eigentlich müßte ich schreiben:

    GizmoReader gizmoReader = GizmoReader::fromStream(std::move(stream)); // erwartet Stream&&
    try
    {
        Gizmo gizmo = gizmoReader.readGizmo(1337);
    }
    __finally
    {
        stream = std::move(gizmoReader).retrieveStream(); // rvalue-Methode
    }
    

    (wobei wir uns jetzt das __finally durch einen entsprechenden RAII-Wrapper substituiert denken).

    Was ich eigentlich will, ist folgendes:
    - wenn ich eine rvalue-Referenz an GizmoReader::fromStream() übergebe, übernimmt der GizmoReader den Besitz und das Lifetime-Management des Streams. Ich kann den GizmoReader also nach Belieben herumreichen.
    - wenn ich aber eine lvalue-Referenz an GizmoReader::fromStream() übergebe, soll das resultierende Objekt nicht moveable sein, oder genauer gesagt: es soll keinen Namen haben können, und es soll das aktuelle Statement nicht überdauern können, so daß ich sicher sein kann, daß es vom Stream -Objekt überlebt wird. Dafür speichert es keinen Stream , sondern nur eine Stream& -Referenz.

    Das führt mich zu folgendem Entwurf:

    class GizmoReader
    {
    private:
        std::unique_ptr<Stream> streamGuard;
        Stream* stream;
    
        GizmoReader(void) = delete;
        GizmoReader(Stream& _stream) : stream(&_stream) { }
        GizmoReader(Stream&& _stream)
          : streamGuard(new Stream(std::move(_stream))),
            stream(streamGuard.get())
        {
        }
    
    public:
        GizmoReader(GizmoReader&&) = default;
        GizmoReader& operator =(GizmoReader&&) = default;
    
        static const GizmoReader fromStream(Stream& stream) { return GizmoReader(stream); } // Ergebnis ist nicht moveable
        static GizmoReader fromStream(Stream&& stream) { return GizmoReader(stream); } // Ergebnis ist moveable
    
        Gizmo readGizmo(int gizmoWidget) const;
    };
    

    Das Problem, daß ich den Stream wieder verändern könnte, bevor ich readGizmo() aufrufe, bleibt bestehen, wird aber dadurch erschwert, daß ich das in demselben Statement machen müßte, also z.B.

    Stream stream = ...;
    Gizmo theGizmo = GizmoReader::fromStream(stream).readGizmo((stream.seek(42, SEEK_CUR), 1337));
    

    Das ist aber bizarr genug, daß es hoffentlich niemand macht.

    Hat dazu jemand eine Meinung? Ist das eine idiomatische Lösung? Oder mache ich die Sache komplizierter, als sie ist?

    Edit: fehlende Klammer ergänzt, Zeiger statt Referenz verwendet



  • Bei meinem Taschenrechner gibt es einen TokenStream und der ist so aufgebaut (eigtl. von The C++ Programming Language)

    class TokenStream {
    public:
        explicit TokenStream(std::istream& is);
        explicit TokenStream(std::istream *is);
        // ...
    private:
        // ...
        std::istream* input {};
        bool ownsInput {false};
    };
    
    TokenStream::TokenStream(std::istream& is):
        input{&is}
    {
    }
    
    TokenStream::TokenStream(std::istream *is):
        input{is}, ownsInput{true}
    {
    }
    
    TokenStream::~TokenStream()
    {
        if (ownsInput) delete input;
    }
    

    Generell sind Pointer geeignet, wenn nullptr also "Kein Objekt" ein gültiger Wert ist. Weiß nicht, ob das in die Richtung geht, die du dir vorgestellt hast.



  • Ist es tatsächlich so, dass der reader länger als der stream lebt? Das sollt3 sicher das Problem der übergeordneten Instanz sein. Dass du den stream von außen noch verändern kannst, ist sicher nicht das Problem. Vielleicht kann es sogar gewollt sein, zB. wenn du ein paar bytrs überspringen möchtest.



  • HarteWare schrieb:

    Generell sind Pointer geeignet, wenn nullptr also "Kein Objekt" ein gültiger Wert ist.

    Ist es aber nicht. Eine Objektreferenz brauche ich ja in jedem Fall. Ansonsten ist der Ansatz mit dem ownsObject -Flag ja ungefähr dasselbe wie mein std::unique_ptr<> .

    Techel schrieb:

    Ist es tatsächlich so, dass der reader länger als der stream lebt?
    Das sollt3 sicher das Problem der übergeordneten Instanz sein.

    Schon, aber ich versuche ja, mithilfe des Typsystems den Benutzer meiner Klassen davon abzuhalten, eine Dummheit zu begehen. Wenn du mit meiner Klasse oben folgendes machst:

    Stream localStream = FileStream::open(..., "rb");
    return GizmoReader::fromStream(localStream);
    

    dann bekommst du einen Compilerfehler, außer du benutzt std::move() .

    Techel schrieb:

    Dass du den stream von außen noch verändern kannst, ist sicher nicht das Problem. Vielleicht kann es sogar gewollt sein, zB. wenn du ein paar bytrs überspringen möchtest.

    Das kann ich machen, bevor ich den Stream an den GizmoReader übergebe. Aber sobald der GizmoReader leihweise im Besitz des Streams ist, muß er annehmen können, daß der Stream sich nicht verändert. (Vielleicht liest der Konstruktor einen Header daraus, in dem steht, wie viele Gizmos drin sind und jeder folgende Aufruf von readGizmo() liest dann einen Gizmo; das geht offensichtlich schief, wenn zwischendurch jemand den Stream versetzt.) Wenn das egal wäre (z.B. wenn Stream immutable wäre), müßte ich mir um das Objektbesitztum keine Gedanken machen, sondern nur noch um die Lebenszeit.



  • Ich kann erstmal nicht erkennen wieso GizmoReader eine Klasse ist und keine Funktion. (Als Implementierungsdetail kann es ja ruhig Klassen geben, aber das Interface sollte ne Funktion sein.)
    Damit würde das Problem entfallen:

    Gizmo ReadGizmoFromStream(Stream&);
    ...
    auto gizmo = ReadGizmoFromStream(stream);
    

    Falls du aus anderen Gründen wirklich eine Klasse brauchst, und auch das Auslesen des/der Gizmo(s) nicht mit einem einzigen Call machen kannst ... pfuh. In dem Fall würde ich das dem Aufrufer/User der Klasse umbinden. Also dass der einfach keinen Mist bauen darf.

    Und was Exceptionsicherheit angeht...
    Was willst du mit dem Stream noch anfangen nachdem das Lesen eines Gizmos daraus fehlgeschlagen ist? Kommt der Fall wirklich so oft vor dass du den Stream danach noch sinnvoll weiterverwenden kannst? (Du weisst ja z.B. nicht wo er steht, also was schon gelesen wurde. Oder ob er überhaupt noch funktioniert.)



  • hustbaer schrieb:

    Ich kann erstmal nicht erkennen wieso GizmoReader eine Klasse ist und keine Funktion. (Als Implementierungsdetail kann es ja ruhig Klassen geben, aber das Interface sollte ne Funktion sein.)

    Ursprünglich war das auch eine freie Funktion, aber mir ist die Zahl der Varianten über den Kopf gewachsen. Der Ansatz mit der Klasse sollte zu einer fluent-Schreibweise führen, die einerseits die Zahl der Überladungen minimal hält, andererseits den Aufruf (IMO) leserlicher macht. Also

    GizmoReader::fromStream(stream).readGizmo(arg)
    

    und

    GizmoReader::fromFile(filename).readGizmoEx(arg, version)
    

    anstatt

    readGizmoFromStream(stream, arg)
    

    und

    readGizmoExFromFile(filename, arg, version)
    

    .
    Das könnte ich bereits lösen, indem die Factoryfunktionen immer einen const GizmoReader zurückgeben (dann geht nur fluent-style). Aber wenn auch zwei Gizmos in einem Stream stehen können, will man readGizmo() evtl. auch mehrfach aufrufen, so daß man in diesem Fall dem GizmoReader einen Namen geben muß, wodurch er zum lvalue wird. Das führte mich zu meinem Ansatz oben.

    hustbaer schrieb:

    Was willst du mit dem Stream noch anfangen nachdem das Lesen eines Gizmos daraus fehlgeschlagen ist? Kommt der Fall wirklich so oft vor dass du den Stream danach noch sinnvoll weiterverwenden kannst?

    Sagen wir, ich habe stdout und stderr auf diese Weise "verliehen" (dann natürlich an einen GizmoWriter ). Da will ich evtl. noch eine schöne Fehlermeldung drauf ausgeben, bevor mein Programm terminiert.



  • audacia|off schrieb:

    Das könnte ich bereits lösen, indem die Factoryfunktionen immer einen const GizmoReader zurückgeben (dann geht nur fluent-style).

    (Das stimmt freilich nicht ganz, man kann das Funktionsergebnis immer noch an eine const GizmoReader& -Referenz binden. Aber da kann ich dann auch nichts machen.)



  • OK.
    Ich verstehe aber das Problem nicht so ganz.
    So lange ein ReadGizmo call "atomar" ist, ist es doch egal wenn der Benutzer des Readers zwischen zwei ReadGizmo calls selbst 'was mit dem Stream macht.

    Blöd wäre nur wenn der Reader sich zwischen Aufrufen irgendwelchen State (z.B. Offsets in den Stream) merken müsste, die dann nicht mehr stimmen wenn der User den Stream dazwischen selbst verwendet.

    ----

    Kannst du ein Problemszenario skizzieren (Pseudocode) das du verhindern möchtest? Vielleicht verstehe ich es dann 🙂



  • Vielleicht ist das Gizmo-Dateiformat ja ein Containerformat, und GizmoReader::fromStream() muß erst einen Header lesen, in dem steht, wie viele Gizmos darauf folgen. Dann ist klar, daß du den Stream nicht versetzen darfst, bevor du GizmoReader::readNextGizmo() aufrufst. Die Streamposition ist dann Teil des internen Zustands von GizmoReader , und deshalb möchte man sie eigentlich kapseln.

    Oder ein anderes Beispiel. Hier geht es nicht um den forcierten fluent style, sondern nur um Probleme mit ausgeliehenen Objekten.

    Ich habe einen ZipFileReader , den ich aus einem Stream konstruiere. Bei ZIP-Dateien steht am Ende eine Liste von central directory headers, die man liest, um eine Liste der enthaltenen Dateien zu erhalten. Allerdings steht da nicht der Offset zu den einzelnen Dateien drin, sondern der Offset zum local file header, der den eigentlichen Daten unmittelbar vorangeht und redundante Metadaten speichert. Weil er Felder variabler Größe enthält, kann ich nicht a priori sagen, wie groß dieser local file header ist, sondern ich muß da hinspringen und ihn lesen.

    Weil die ZIP-Datei sehr groß sein kann, möchte ich die local file headers nicht alle lesen, sondern nur dann, wenn die Datei auch gebraucht wird. Angenommen, ich habe folgendes Interface:

    class ZipFileReader
    {
    private:
        Stream& stream; // nur ausgeliehen
        ...
    public:
        ZipFileReader(Stream& _stream);
        std::size_t numFiles(void) const;
        ZipFileAttributes fileAttributes(std::size_t index) const;
        std::size_t fileDataOffset(std::size_t index);
        ...
    }
    

    Das Lesen der local file headers soll lazy sein; also nimmt fileDataOffset() den Stream, fährt ihn an die entsprechende Position und liest den local file header, um an den tatsächlichen Datenoffset zu kommen.

    Dann könnte einem unbedarften Benutzer aber Folgendes passieren:

    Stream stream = ...;
    ZipFileReader reader(stream);
    stream.seek(reader.fileDataOffset(0), SEEK_SET); // <-- setzt den Stream an die richtige Position
    std::size_t secondFileOffset = reader.fileDataOffset(1); // <-- setzt den Stream wieder um
    readFile(stream, reader.fileAttributes(0).fileSize()); // <-- schlägt fehl, da falsche Position
    

    Das Grundproblem ist m. E., daß nicht korrekt im Typsystem abgebildet ist, ob eine Methode den Stream braucht und/oder verändert.

    Dieses Problem könnte man eben vermeiden, wenn man dem ZipFileReader den Stream übereignet. Nur wie kann ich dann denselben Stream wieder benutzen, um eine Datei zu lesen? Am ehesten wohl mit einem Callback, der mir den Stream vorübergehend zur Verfügung stellt:

    template <typename ReadCallbackT> // void(Stream&)
        void ZipFileReader::readFile(std::size_t index, ReadCallbackT&& callback)
    {
        std::size dataOffset = fileDataOffset(index);
        callback(stream);
    }
    ...
    zipFileReader.readFile(0, [&](Stream& stream) { readFile(stream, zipFileReader.fileAttributes(0).fileSize()); });
    


  • Ach so, dir geht's hier um die Theorie - ich dachte du hättest nen konkreten Anwendungsfall.
    (Und den sehe ich hier nicht, weil das Beispiel IMO kontruiert ist, es gibt IMO keinen Grund dass der Benutzer den Stream selbst verwenden kann während der ZipReader drauf hängt.)

    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. Und wenn er schlau genug ist brav den Callback zu verwenden, dann frage ich mich ob er nicht auch schlau genug wäre es auch ohne Callback richtig zu machen.

    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.



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


Anmelden zum Antworten