Gutes Entwurfsmuster für einen C ++ - Wrapper um ein Wechselstromobjekt

8

Ich habe einen erweiterbaren C ++ - Wrapper um eine sehr schwer zu verwendende, aber auch sehr nützliche C-Bibliothek geschrieben. Das Ziel ist es, c ++ für die Zuweisung des Objekts, die Offenlegung seiner Eigenschaften, die Freigabe des Objekts, das Kopieren der Semantik usw. zu nutzen.

Das Problem ist folgendes: Manchmal möchte die c-Bibliothek das zugrunde liegende Objekt (einen Zeiger auf das Objekt), und der Klassendestruktor sollte den zugrunde liegenden Speicher nicht zerstören. Während der meisten Zeit sollte der Destruktor die Zuordnung des zugrunde liegenden Objekts aufheben. Ich habe mit dem Setzen eines bool hasOwnershipFlags in der Klasse experimentiert, damit der Destruktor, der Zuweisungsoperator usw. weiß, ob er den zugrunde liegenden Speicher freigeben soll oder nicht. Dies ist jedoch für den Benutzer umständlich, und manchmal gibt es auch keine Möglichkeit zu wissen, wann ein anderer Prozess diesen Speicher verwendet.

Derzeit habe ich es so eingerichtet, dass ich das hasOwnership-Flag setze, wenn die Zuweisung von einem Zeiger des gleichen Typs wie der zugrunde liegende Typ stammt. Ich mache dasselbe, wenn der überladene Konstruktor mit dem Zeiger aus der c-Bibliothek aufgerufen wird. Dies ist jedoch immer noch nicht der Fall, wenn der Benutzer das Objekt erstellt und an eine meiner Funktionen übergeben hat, die c_api aufruft und die Bibliothek den Zeiger zur späteren Verwendung speichert. Wenn sie ihr Objekt löschen würden, würde dies zweifellos einen Segfault in der c-Bibliothek verursachen.

Gibt es ein Entwurfsmuster, das diesen Prozess vereinfachen würde? Vielleicht eine Art Referenzzählung?

Jonathan Henson
quelle
Sie scheinen keine Gedanken über die Fadensicherheit dieser Objekte zu haben ...
Ratschenfreak
@ratchetfreak Der Wrapper ruft lediglich den Accessor / die Mutatoren auf, stellt eine gute Kopiersemantik sicher, verarbeitet den Speicher, Kopierlisten usw. Ich berühre die Daten nicht direkt. Die C-Bibliothek ist bereits threadsicher. Außerdem haben meine Benutzer nur synchron mit dem System der Rückrufe usw. Zugriff auf meine Wrapper. Bei Bedarf habe ich Sperren beim Lesen / Schreiben von Daten.
Jonathan Henson
2
unique_ptrIn den meisten Fällen kann diese Art von Ressourcen bereits verarbeitet werden, sodass Sie die Ressourcenverwaltung nicht selbst implementieren müssen. unique_ptrverwendet die releaseMethode, um das Eigentum an dem gespeicherten Objekt aufzugeben.
Philipp

Antworten:

4

Wenn sich die Verantwortung für das Bereinigen dynamisch zugeordneter Inhalte zwischen Ihrer Wrapper-Klasse und der C-Bibliothek ändert, je nachdem, wie die Dinge verwendet werden, haben Sie es entweder mit einer schlecht gestalteten C-Bibliothek zu tun oder Sie versuchen, in Ihrer Wrapper-Klasse zu viel zu tun.

Im ersten Fall können Sie nur nachverfolgen, wer für die Bereinigung verantwortlich ist, und hoffen, dass keine Fehler gemacht werden (weder von Ihnen noch von den Betreuern der C-Bibliothek).

Im zweiten Fall sollten Sie Ihr Wrapper-Design überdenken. Gehören alle Funktionen zu derselben Klasse oder können sie in mehrere Klassen aufgeteilt werden? Möglicherweise verwendet die C-Bibliothek etwas Ähnliches wie das Facade-Entwurfsmuster, und Sie sollten eine ähnliche Struktur in Ihrem C ++ - Wrapper beibehalten.

Selbst wenn die C-Bibliothek für die Bereinigung einiger Dinge verantwortlich ist, ist es auf keinen Fall falsch, einen Verweis / Zeiger auf diese Dinge zu behalten. Sie müssen sich nur daran erinnern, dass Sie nicht dafür verantwortlich sind, das zu bereinigen, worauf sich der Zeiger bezieht.

Bart van Ingen Schenau
quelle
Was wäre, wenn ich sicherstellen würde, dass ein Klon des zugrunde liegenden Objekts erstellt wurde, wenn es an die Bibliothek übergeben wird? Dann kann ich mein Gedächtnis frei machen, wie es mir gefällt, und es wird nichts bewirken. Dann erhalte ich jedoch keine Änderungen am Objekt in meiner Klasse.
Jonathan Henson
Und ja, in dieser Bibliothek wurden einige sehr schlechte Praktiken angewendet. Beispiel: Anstatt dass der Benutzer ein const char * an eine Funktion übergibt, die eine Zeichenfolge in einer Struktur festlegt, und dann eine Kopie zum Speichern in der Struktur erstellt, erwartet er, dass der Benutzer die Zeichenfolge im laufenden Betrieb zuweist und ein char * übergibt Es wird nur gespeichert, ohne eine Kopie zu erstellen. Dies ist einer der Hauptgründe, warum ich einen Wrapper erstellt habe - um sicherzustellen, dass die richtige Kopiersemantik eingehalten wurde.
Jonathan Henson
@JonathanHenson: Es ist eine gängige Optimierungstechnik, den Client etwas zuweisen zu lassen und das Eigentum / die Verantwortung daran an die Bibliothek zu übergeben, um übermäßiges Kopieren zu vermeiden. Sie gilt vollständig für C-Bibliotheken (sofern dies konsistent erfolgt). In der Tat bedeutet dies jedoch, dass der Benutzer bei Verwendung von C ++ möglicherweise zusätzliche Kopien erstellen muss, um den Schnittstellenanforderungen der C-Bibliothek zu entsprechen.
Bart van Ingen Schenau
3

Oft können Sie ein Muster wie das folgende verwenden:

class C {
public:
  void foo() {
    underlying_foo(handle.get());
  }

  void bar() {
    // transfers ownership
    underlying_bar(handle.release());
  }

  // use default copy/move constructor and assignment operator

private:
  struct deleter {
    void operator()(T* ptr) {
      deleter_fn(ptr);
    }
  };
  std::unique_ptr<T, deleter> handle;
};

Durch die Verwendung können releaseSie das Eigentum explizit übertragen. Dies ist jedoch verwirrend und sollte nach Möglichkeit vermieden werden.

Die meisten C-Bibliotheken haben einen C ++ - ähnlichen Objektlebenszyklus (Objektzuweisung, Zugriffsmethoden, Zerstörung), der ohne Eigentumsübertragung gut auf das C ++ - Muster abgebildet werden kann.

Wenn Benutzer eine gemeinsame Eigentümerschaft benötigen, sollten sie diese shared_ptrfür Ihre Klassen verwenden. Versuchen Sie nicht, eine Eigentumsfreigabe selbst zu implementieren.


Update: Wenn Sie die Übertragung des Eigentums expliziter gestalten möchten, können Sie ein Referenzqualifikationsmerkmal verwenden:

void bar() && { ... }

Dann müssen Benutzer folgende barWerte aufrufen:

C o;
std::move(o).bar();  // transfer of ownership is explicit at call site
Philipp
quelle
@Phillip, das sieht genau so aus, wie ich es brauche. Ich werde es versuchen und mit dir zurückkommen.
Jonathan Henson
Eine Frage. Was genau ist Eigentum in diesem Fall. Bedeutet dies das Privileg, auf das Objekt zuzugreifen / es zu ändern, oder nur die Fähigkeit, das Objekt zu konstruieren und zu zerstören?
Jonathan Henson
Grundsätzlich sollte ich, sobald ich den zugrunde liegenden Speicher an die c-lib übergeben habe, niemals einen kostenlosen Aufruf des Speichers tätigen. Ich kann und muss jedoch häufig auf die Daten im Objekt zugreifen.
Jonathan Henson
2
Das Eigentum ist das Recht, das Objekt zu zerstören.
DeadMG
@JonathanHenson: Das ist ein interessanter Fall, der derzeit in dieser Redewendung nicht behandelt wird: Sobald Sie a veröffentlicht haben unique_ptr, wird der darin enthaltene Zeiger nicht mehr gespeichert. Die Modellierung der Eigentumsübertragung und gleichzeitig die Ermöglichung des Zugriffs auf nicht genutzte Ressourcen scheint für Benutzer ziemlich verwirrend zu sein. Wie steuern Sie, wann die C-Bibliothek die Objekte freigibt, die sie jetzt besitzt?
Philipp
0

Es gibt eine einfache Antwort auf Ihr Problem, intelligente Zeiger. Wenn Sie einen intelligenten Zeiger verwenden, um den Speicher der C-Bibliothek zu speichern, und eine Referenz hinzufügen, wenn sich der Zeiger ebenfalls in der Bibliothek befindet (und die Referenz löschen, wenn die C-Bibliothek zurückkehrt), wird der Speicher automatisch freigegeben, wenn der Referenzzähler auf Null fällt (und nur dann)

Michael Shaw
quelle
Dies behebt das Problem nicht, es sei denn, ich verstehe intelligente Zeiger nur schlecht. Das Problem ist, dass mein Destruktor manchmal den Speicher bereinigen sollte, manchmal bereinigt die C-Bibliothek den Speicher. Diese Strukturen haben spezielle init, freie Funktionen. Die Bibliothek kann selbstständig kostenlos aufrufen, bevor der Referenzzähler des Smart-Pointers 0 erreicht. Ich muss grundsätzlich über den Besitz entscheiden und ihn loslassen. Ich brauche keinen intelligenten Zeiger, um zu wissen, wann der Speicher bereinigt werden muss. Das Problem ist, dass ich manchmal nicht für die Bereinigung verantwortlich bin und ihn niemals im Destruktor freigeben muss.
Jonathan Henson
Ja, ich stimme zu, ich habe das Problem falsch verstanden. Nur eine Beobachtung: Teilen sich die verschiedenen Bibliotheken dieselbe Speicherzuordnungstabelle? Mein intensives C / C ++ - Zeug war vor 10 Jahren, und in Visual Studio damals war das Zuweisen von Speicher in einer Bibliothek und das Freigeben in einer anderen Bibliothek ein Speicherverlust, es sei denn, Sie haben einige Compileroptionen für BEIDE Bibliotheken geändert. Die Konsequenz daraus wäre dann eine Speicherverwaltungserweiterung für Anwendungen mit hohem Thread. Es ist jedoch sehr wahrscheinlich, dass dies veraltete Informationen sind.
Michael Shaw
nicht, wenn alles in den gleichen Adressraum geladen ist und es ist. Wie auch immer, ich kompiliere die c-lib in derselben Assembly wie mein Wrapper neu.
Jonathan Henson
0

Wenn die Bibliothek Dinge intern freigeben kann und die Szenarien, in denen dies geschehen kann, gut dokumentiert sind, können Sie nur ein Flag setzen, wie Sie es bereits tun.

James
quelle
Grundsätzlich sollte ich, sobald ich den zugrunde liegenden Speicher an die c-lib übergeben habe, niemals einen kostenlosen Aufruf des Speichers tätigen. Ich kann und muss jedoch häufig auf die Daten im Objekt zugreifen.
Jonathan Henson