[Design] Wrapper-Klasse um C Handle kopierbar?


  • Administrator

    Hallo zusammen,

    Eine relativ einfache Design-Frage zu einer Wrapper-Klasse. Ich nehme hier als Beispiel FILE , aber dies nur zur Veranschaulichung. Ich will einen Wrapper bauen um ein Handle, ähnlich wie z.B. FILE . Also man hat Funktionen, um ein FILE Objekt zu erhalten, damit etwas zu machen oder es zu zerstören.

    FILE* file = fopen(...);
    fwrite(file, ...);
    fread(file, ...);
    fclose(file);
    

    Ich möchte nun eine Wrapper-Klasse schreiben, welche diese Funktionalität kapselt.

    class File
      : public boost::noncopyable
    {
      FILE* m_handle;
    
    public:
      File(...)
        : m_handle(fopen(...))
      {
      }
    
      ~File(...)
      {
        if(m_handle) fclose(m_handle);
      }
    
      void read(...)
      {
        fread(m_handle, ...);
      }
    
      void write(...)
      {
        fwrite(m_handle, ...);
      }
    };
    

    Diese Klasse habe ich als nicht kopierbar markiert. Der Grund dafür ist, dass eine Kopie von einem Objekt des Typs File nicht zur wirklich Kopie der Datei führt und dies eigentlich auch nicht der Fall sein soll.

    Dadurch habe ich allerdings keine Möglichkeit die File -Klasse in einen std::vector zu packen (C++11 darf ich nicht verwenden). Genau diese Funktionalität wird allerdings des Öfteren benötigt. Da ganze Listen mit solchen Objekten rumgeschoben werden. Meine Frage an euch ist jetzt: Wie würdet ihr das machen?

    a) Die Klasse umbenennen? Z.B. FileHandle? Mich stört dann irgendwie, dass eine FileHandle Klasse Methoden für das Lesen und Schreiben aufweist. Bin ich da zu pingelig?
    b) Den Klassennamen so belassen aber kopierbar machen? Jeder wird verstehen, dass nicht wirklich die Datei kopiert wird sondern nur der Zugang? (Referenzzählen usw. ist kein Problem, wird bereits vom C System angeboten)
    c) Den Namen so belassen und nicht kopierbar machen? Der Benutzer muss halt Zeiger in den Container ablegen und z.B. Boost.PointerContainer verwenden.
    d) etwas ganz anderes?

    Ich wäre dankbar für konstruktives Feedback 🙂

    Grüssli



  • Macht es denn in deiner Anwendung Sinn, mehrere File -Instanzen auf die gleiche Datei zu haben? Oder sind STL-Container der einzige Grund, Kopien zu erlauben? Falls es nur um diese technische Beschränkung geht, würde ich eher zu Nicht-Kopierbarkeit tendieren. Der Benutzer kann dann allenfalls ptr_vector oder shared_ptr benutzen und macht im zweiten Fall geteiltes Ownership auch gleich deutlich.


  • Administrator

    Nexus schrieb:

    Macht es denn in deiner Anwendung Sinn, mehrere File -Instanzen auf die gleiche Datei zu haben?

    Selten, aber ist schon möglich und wurde über die Referenzzählung in der C Schnittstelle auch bereits vorgesehen.

    Wenn ich es nicht erlaube, habe ich allerdings das Problem, dass man so gut wie immer über die Indirektion arbeiten werden muss. Die Indirektion wird zur Pflicht und das stört mich. Man müsste schlussendlich fast immer mit shared_ptr<File> oder ptr_vector<File> arbeiten. Denn die Handles müssen im Programm herumgereicht werden. Man macht z.B. Abfragen und erhält eine Liste von Handles zurück und man kommt kaum anders an die Objekte ran. Solche Abfragen sind somit der ganz normale Arbeitsablauf. Es wird kaum je jemand selber ein Objekt erstellen, die Objekte werden meistens in der Bibliothek erstellt und dann herausgegeben.

    Grüssli



  • hola

    und was ist wenn du einfach den noncopy-teil auslagerst ?
    da wuerde sich z.b. eine handle-klasse anbieten.
    darin haelst du einen ptr auf den noncopy-teil und gibst es dann ueber die handle-klasse nach aussen weiter

    Meep Meep

    edit: mist, ich glaub ich hab dich mistverstanden



  • wegen dem vector:

    was ist wenn du den copy-ctor und assign-op privat machst und den vector als friend angibst ? waere dann deine handle-klasse nicht noncopy ausser fuer den vector ?

    Meep Meep



  • @Dravere:
    Du könntest den Ball auch ganz flach halten, und gleich mit shared_ptr<FILE> arbeiten...

    typedef boost::shared_ptr<FILE> FilePtr;
    
    FilePtr SomeFunction()
    {
    	FilePtr f(fopen("foo.txt", "rw"), &fclose);
    	if (!f)
    		throw std::runtime_error("blah");
    	return f;
    }
    
    void Write(FilePtr const& f, std::string const& str)
    {
    	size_t const written = fwrite(str.data(), 1, str.size(), f.get());
    	if (written != str.size())
    		throw std::runtime_error("blubb");
    }
    
    void Flush(FilePtr const& f)
    {
    	if (fflush(f.get()) != 0)
    		throw std::runtime_error("meh");
    }
    
    // ...
    
    int main()
    {
    	FilePtr f = SomeFunction();
    	Write(f, "foo");
    	Flush(f);
    }
    


  • Dravere schrieb:

    a) Die Klasse umbenennen? Z.B. FileHandle? Mich stört dann irgendwie, dass eine FileHandle Klasse Methoden für das Lesen und Schreiben aufweist. Bin ich da zu pingelig?

    Find ich zu pingelig, wo du schon so fragst 😉
    Ein Handle ist halt nichts als ein besserer Pointer, Proxy oder wie auch immer - warum solltest du nicht Funktionalität bieten, um den Zugiff über das handle auf die dahinterliegende Ressource zu vereinfachen?



  • Falls Du C++11 Mittel benutzen darfst, könntest Du Deinen Wrapper "move-only" machen. Ich würde mir dann aber wahrscheinlich nicht mal die Mühe machen und stattdessen einfach unique_ptr oder shared_ptr mit entsprechendem deleter nehmen.


  • Administrator

    Meep Meep schrieb:

    was ist wenn du den copy-ctor und assign-op privat machst und den vector als friend angibst ? waere dann deine handle-klasse nicht noncopy ausser fuer den vector ?

    Ich muss sagen, dass ich die Idee noch witzig finde. Aber auf der anderen Seite finde ich sie inkonsistent. Und wenn ich etwas gern habe und anstrebe, dann ist es Konsistenz! Von daher eher nicht...

    hustbaer schrieb:

    @Dravere:
    Du könntest den Ball auch ganz flach halten, und gleich mit shared_ptr<FILE> arbeiten...

    An sowas habe ich zwischenzeitlich auch gedacht und finde den Gedanken noch recht interessant. Darauf gekommen bin ich über einen Zwischenschritt. Habe mir überlegt die Funktionalitäten aufzutrennen, dass ich eine FileHandle Klasse einführe, welche dann an eine File Klasse übergeben werden kann. Danach habe ich mir überlegt, ob ich die File Klasse überhaupt noch benötige oder ob freie Funktionen nicht genügen würden. Auf shared_ptr würde ich wahrscheinlich verzichten, schliesslich kann ich auch gleich die Referenzzählung der C Bibliothek verwenden. Damit könnte ich auch eine bessere Kompabilität zu den C Funktionen anbieten.

    Die Idee werde ich definitiv etwas weiterverfolgen und ausbauen. Danke dafür!

    pumuckl schrieb:

    Find ich zu pingelig, wo du schon so fragst 😉
    Ein Handle ist halt nichts als ein besserer Pointer, Proxy oder wie auch immer - warum solltest du nicht Funktionalität bieten, um den Zugiff über das handle auf die dahinterliegende Ressource zu vereinfachen?

    Naja, aus folgendem Grund:

    FileHandle fh = select_that();
    fh.get_name();
    

    Dass liest sich als: Get the name of the file handle.
    Dabei sollte es heissen: Get the name of the file.

    Ich habe das Gefühl, dass die Bedeutung der Funktionen, worauf sie nun tatsächlich angewendet wird, bei sowas markant verfälscht wird und dadurch entsteht Vewirrung beim Leser.

    krümelkacker schrieb:

    Falls Du C++11 Mittel benutzen darfst, könntest Du Deinen Wrapper "move-only" machen. Ich würde mir dann aber wahrscheinlich nicht mal die Mühe machen und stattdessen einfach unique_ptr oder shared_ptr mit entsprechendem deleter nehmen.

    Steht im ersten Beitrag: Ich darf C++11 Mittel nicht verwenden. Sonst hätte ich wahrscheinlich auch nur eine Move-Semantik angeboten. Ich habe die Idee nicht ganz durchgespielt, da ich C++11 sowieso nicht verwenden kann, aber ich meine, dass es alles abgedeckt hätte. Leider ist C++11 zu neu. Habe schon mit anderen Technologien Probleme, weil sehr sehr aktuelle verwendet werden (6-12 Monate), will mir da nicht noch mehr Probleme schaffen mit der Verwendung von C++11. Von daher bin ich auch etwas froh, dass C++11 nicht zum Einsatz kommt und werde daher auch nicht auf deren Verwendung drängen. 🙂

    Danke für die bisherige Teilnahme!

    Grüssli



  • Spontane Idee:

    class File : boost::noncopyable {
      FILE *handle;
      friend class FileHandle; // der Freund darf kopieren
    public:
      /* ... */
    };
    
    class FileHandle {
      File file; // kein Pointer
    public:
      File* operator->() { return &file; }
      // falls vorhanden, solche Sachen dann hier:
      size_t reference_count() const { return c_core_file_get_ref_count(file.handle); }
    };
    

    Damit wäre, mit etwas Mogelei, die Doppelfunktionalität des Handles wieder in zwei getrennte Klassen untergebracht. (Ironischerweise geht das mit C++11 nicht mehr.) Wobei, irgendwie kommt mir das komisch vor...



  • Ich würde mich an std::unique_ptr orientieren und das Handle move-only machen. Alles andre macht imo keinen Sinn, außer du willst es sharen. In dem Fall dann eben Reference-Counting. Alternativ kannst du dir auch überlegen, ob du das Handle beim Kopieren nicht vielleicht duplizieren willst.



  • pumuckl schrieb:

    Dravere schrieb:

    a) Die Klasse umbenennen? Z.B. FileHandle? Mich stört dann irgendwie, dass eine FileHandle Klasse Methoden für das Lesen und Schreiben aufweist. Bin ich da zu pingelig?

    Find ich zu pingelig, wo du schon so fragst 😉
    Ein Handle ist halt nichts als ein besserer Pointer, Proxy oder wie auch immer - warum solltest du nicht Funktionalität bieten, um den Zugiff über das handle auf die dahinterliegende Ressource zu vereinfachen?

    Ich finde aber auch, dass die Klasse, wenn sie wirklich nur ein RAII Wrapper um ein low-level Handle sein soll, nicht unbedingt über Methoden wie read() verfügen sollte. Ich würde mir überlegen, ob es nicht vielleicht sinnvoll wäre, eine benutzerdefinierte Typumwandlung nach FILE* zu bieten, sodass man das RAII Objekt völlig gleich wie einen normalen FILE* verwenden kann. Wenn es eine vordefinierte read() Methode hat, dann ist es imo schon eher eine vollwertige File-Klasse und kein einfacher Handle-Wrapper mehr. Allerdings wirft das natürlich sofort die Frage auf: Warum keine iostreams?


  • Administrator

    @dot,
    Hast du meinen ersten Beitrag gelesen?
    1. C++11 kann ich nicht verwenden, daher liegt die Move-Semantik nicht drin.
    2. FILE ist hier nur als Platzhalter zu verstehen. Tatsächlich geht es um ganz andere Objekte und Funktionalitäten (heisst auch alles ganz anders), aber die C Schnittstelle ist eben genau gleich gebaut. Habe hier nur FILE verwendet, weil damit allen klar sein sollte, wie die Schnittstelle in etwa aussieht.

    dot schrieb:

    Alternativ kannst du dir auch überlegen, ob du das Handle beim Kopieren nicht vielleicht duplizieren willst.

    Was genau verstehst du darunter? Ich kann über die C Schnittstelle aus einem bestehenden Handle nicht ein neues machen. Ich muss eine neue Abfrage absetzen, heisst dieselbe wie vorher, um ein neues Handle auf das Gleiche zu erhalten.

    @giest,
    Hmmm ... mehr fällt mir dazu aktuell auch nicht ein. Kommt mir ebenfalls seltsam vor...
    Dann irgendwie lieber meine Idee, dass man File lokal über FileHandle baut:

    class file_handle
    {
      FILE* m_raw_handle;
    
    public:
      file_handle(FILE* raw_handle)
        : m_raw_handle(raw_handle)
      {
      }
    
      friend FILE* get_raw_handle(file_handle);
    }
    
    FILE* get_raw_handle(file_handle handle)
    {
      return handle.m_raw_handle;
    }
    
    class file : boost::noncopyable
    {
      FILE* m_raw_handle;
    
    public:
      file(file_handle handle)
        : m_raw_handle(get_raw_handle(handle))
      {
      }
    
      // Methoden
    }
    
    // ...
    
    void foo(file_handle foo_handle)
    {
      file foo_file(foo_handle);
    
      // ...
    }
    

    Grüssli



  • Dravere schrieb:

    1. C++11 kann ich nicht verwenden, daher liegt die Move-Semantik nicht drin.

    Das ist mir leider entgangen.

    Dravere schrieb:

    1. FILE ist hier nur als Platzhalter zu verstehen. Tatsächlich geht es um ganz andere Objekte und Funktionalitäten (heisst auch alles ganz anders), aber die C Schnittstelle ist eben genau gleich gebaut. Habe hier nur FILE verwendet, weil damit allen klar sein sollte, wie die Schnittstelle in etwa aussieht.

    Vergisses, für einen Moment hab ich da an Posix-Handles gedacht...



  • Dravere schrieb:

    Dann irgendwie lieber meine Idee, dass man File lokal über FileHandle baut

    Finde ich verkehrt. Normalerweise wrappt man ja die ursprüngliche, unkopierbare Ressource und macht sie kopierbar ( FILE* macht das). Nur das ist logisch. Man baut aus T einen shared_ptr<T> und nicht umgekehrt, das wäre dann völlig unintuitiv in der Handhabung.

    Meins leuchtet ein: Unkopierbare File-Klasse -> Smart Pointer drum und gut. Das einzig spezielle ist, dass der erste Wrapper Funktionalität vorbehält. Genau genommen ist das jedoch ein Fehler von FILE* und nicht vom Wrapper, insofern kann man das nicht anders trennen. Und die Trennung ist ja genau das, was du willst. Wenn ein FileHandle selbst keinen Namen hat, muss es etwas drunter haben und ein File selbst kann keine Referenzzählung vornehmen. Es läuft auf diese zwei Klassen hinaus und das FileHandle muss das File wrappen.

    Je mehr ich drüber nachdenke, desto passender finde ich meinen Vorschlag. Und die seltsame Implementierung ist kein Designfehler vom Wrapper, sondern vom Gewrappten.

    Vielleicht wird der Wrap schmackhafter, wenn du FileHandle in file_ptr umbenennst.



  • Destructive Copy?

    Edit: Vergiss es, das geht ja mit vector nicht.


  • Administrator

    @giest,
    So ganz unrecht hast du nicht. Hab mir heute deine Lösung nochmals angeschaut und in den Kontext reingedacht. Es hätte ein paar bestechende Vorteile. Ich nehme die Lösung nun zumindest in die engere Auswahl mit auf. Danke.

    Ich muss mich zum Glück noch nicht heute oder morgen entscheiden und kann nun nochmals alles in Ruhe evaluieren. Danke für die Vorschläge 🙂

    Falls ihr noch weitere habt oder Kommentare loswerden wollt, dann nur zu 🙂

    Grüssli



  • Hm, noch ne Idee. Du könntest den Datenmember mutable machen und im op= moven. Wäre allerdings ziemlich unintuitiv. Noch eine Möglichkeit wäre eine Art Move-Proxy.



  • 314159265358979 schrieb:

    Hm, noch ne Idee. Du könntest den Datenmember mutable machen und im op= moven. Wäre allerdings ziemlich unintuitiv.

    Vor allem recht buganfällig, wenn sich STL-Container und -Algorithmen auf normale Kopiersemantik verlassen.



  • Achja richtig. Das war wohl der Grund, warum man auto_ptr nicht in Containern speichern kann.


Log in to reply