Wie verwende ich einen benutzerdefinierten Löscher mit einem std :: unique_ptr-Mitglied?

133

Ich habe eine Klasse mit einem unique_ptr-Mitglied.

class Foo {
private:
    std::unique_ptr<Bar> bar;
    ...
};

Die Leiste ist eine Klasse von Drittanbietern mit einer Funktion create () und einer Funktion destroy ().

Wenn ich a std::unique_ptrdamit in einer eigenständigen Funktion verwenden wollte, könnte ich Folgendes tun:

void foo() {
    std::unique_ptr<Bar, void(*)(Bar*)> bar(create(), [](Bar* b){ destroy(b); });
    ...
}

Gibt es eine Möglichkeit, dies std::unique_ptrals Mitglied einer Klasse zu tun ?

huitlarc
quelle

Antworten:

133

Unter der Annahme , dass createund destroysind frei Funktionen mit den folgenden Signaturen (was der Fall aus dem OP des Code - Schnipsel zu sein scheint):

Bar* create();
void destroy(Bar*);

Sie können Ihre Klasse Fooso schreiben

class Foo {

    std::unique_ptr<Bar, void(*)(Bar*)> ptr_;

    // ...

public:

    Foo() : ptr_(create(), destroy) { /* ... */ }

    // ...
};

Beachten Sie, dass Sie hier kein Lambda oder benutzerdefiniertes Deleter schreiben müssen, da destroyes sich bereits um ein Deleter handelt.

Cassio Neri
quelle
156
Mit C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe
1
Der Nachteil dieser Lösung ist, dass sie den Overhead von jedem verdoppelt unique_ptr(alle müssen den Funktionszeiger zusammen mit dem Zeiger auf die tatsächlichen Daten speichern), die Zerstörungsfunktion jedes Mal übergeben muss und nicht inline sein kann (da die Vorlage dies nicht kann Spezialisieren Sie sich auf die spezifische Funktion (nur die Signatur) und müssen Sie die Funktion über den Zeiger aufrufen (teurer als direkter Aufruf). Beide rici und Deduplicator die Antworten vermeiden alle diese Kosten durch zu einem Funktor spezialisiert.
ShadowRanger
@ShadowRanger ist es nicht so definiert, dass default_delete <T> und der gespeicherte Funktionszeiger jedes Mal angezeigt werden, ob Sie ihn explizit übergeben oder nicht?
Herrgott
117

Es ist möglich, dies sauber mit einem Lambda in C ++ 11 (getestet in G ++ 4.8.2) zu tun.

Angesichts dieser wiederverwendbaren typedef:

template<typename T>
using deleted_unique_ptr = std::unique_ptr<T,std::function<void(T*)>>;

Du kannst schreiben:

deleted_unique_ptr<Foo> foo(new Foo(), [](Foo* f) { customdeleter(f); });

Zum Beispiel mit einem FILE*:

deleted_unique_ptr<FILE> file(
    fopen("file.txt", "r"),
    [](FILE* f) { fclose(f); });

Damit erhalten Sie die Vorteile einer ausnahmesicheren Bereinigung mit RAII, ohne dass Try / Catch-Rauschen erforderlich ist.

Drew Noakes
quelle
2
Dies sollte die Antwort sein, imo. Es ist eine schönere Lösung. Oder gibt es Nachteile, wie z. B. std::functionin der Definition oder ähnliches?
J00hi
17
@ j00hi, meiner Meinung nach hat diese Lösung wegen std::function. Lambda oder eine benutzerdefinierte Klasse wie in der akzeptierten Antwort können im Gegensatz zu dieser Lösung eingefügt werden. Dieser Ansatz hat jedoch Vorteile, wenn Sie die gesamte Implementierung in einem dedizierten Modul isolieren möchten.
Magras
5
Dies führt zu Speicherverlust, wenn der Konstruktor std :: function
auslöst
4
Wird Lambda hier wirklich benötigt? Es kann einfach sein, deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);wenn es customdeleterder Konvention folgt (es gibt void zurück und akzeptiert den Rohzeiger als Argument).
Victor Polevoy
Dieser Ansatz hat einen Nachteil. Die Funktion std :: ist nicht erforderlich, um den Verschiebungskonstruktor nach Möglichkeit zu verwenden. Dies bedeutet, dass beim Std :: move (my_deleted_unique_ptr) der von Lambda eingeschlossene Inhalt möglicherweise kopiert anstatt verschoben wird, was möglicherweise nicht Ihren Wünschen entspricht.
GeniusIsme
70

Sie müssen nur eine Deleter-Klasse erstellen:

struct BarDeleter {
  void operator()(Bar* b) { destroy(b); }
};

und geben Sie es als Vorlagenargument von an unique_ptr. Sie müssen das unique_ptr in Ihren Konstruktoren noch initialisieren:

class Foo {
  public:
    Foo() : bar(create()), ... { ... }

  private:
    std::unique_ptr<Bar, BarDeleter> bar;
    ...
};

Soweit ich weiß, implementieren alle gängigen C ++ - Bibliotheken dies korrekt. da BarDeleter es eigentlich keinen zustand hat, muss es keinen platz in der besetzen unique_ptr.

Rici
quelle
8
Diese Option ist die einzige, die mit Arrays, std :: vector und anderen Sammlungen funktioniert, da sie den Nullparameter std :: unique_ptr-Konstruktor verwenden kann. Andere Antworten verwenden Lösungen, die keinen Zugriff auf diesen Nullparameterkonstruktor haben, da beim Erstellen eines eindeutigen Zeigers eine Deleter-Instanz bereitgestellt werden muss. Diese Lösung bietet jedoch eine Deleter-Klasse ( struct BarDeleter) bis std::unique_ptr( std::unique_ptr<Bar, BarDeleter>), mit der der std::unique_ptrKonstruktor eine Deleter-Instanz selbst erstellen kann. dh der folgende Code ist erlaubtstd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF
12
Ich würde ein typedef für die einfache Verwendung erstellentypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
DavidF
@DavidF: Oder verwenden Sie den Ansatz von Deduplicator , der dieselben Vorteile bietet (Inlining-Löschung, kein zusätzlicher Speicherplatz unique_ptr, keine Notwendigkeit, beim Erstellen eine Instanz des Löschers bereitzustellen) und den Vorteil bietet, dass Sie ihn std::unique_ptr<Bar>überall verwenden können, ohne sich daran erinnern zu müssen um den speziellen typedefoder explizit anbieter den zweiten vorlagenparameter zu verwenden. (Um klar zu sein, dies ist eine gute Lösung, ich habe dafür gestimmt, aber es bleibt einen Schritt vor einer nahtlosen Lösung stehen)
ShadowRanger
22

Sofern Sie nicht in der Lage sein müssen, den Löscher zur Laufzeit zu ändern, würde ich dringend empfehlen, einen benutzerdefinierten Löschertyp zu verwenden. Wenn Sie beispielsweise einen Funktionszeiger für Ihren Deleter verwenden , sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). Mit anderen Worten, die Hälfte der Bytes des unique_ptrObjekts wird verschwendet.

Das Schreiben eines benutzerdefinierten Löschers zum Umschließen jeder Funktion ist jedoch problematisch. Zum Glück können wir einen Typ schreiben, der für die Funktion vorgesehen ist:

Seit C ++ 17:

template <auto fn>
using deleter_from_fn = std::integral_constant<decltype(fn), fn>;

template <typename T, auto fn>
using my_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;

// usage:
my_unique_ptr<Bar, destroy> p{create()};

Vor C ++ 17:

template <typename D, D fn>
using deleter_from_fn = std::integral_constant<D, fn>;

template <typename T, typename D, D fn>
using my_unique_ptr = std::unique_ptr<T, deleter_from_fn<D, fn>>;

// usage:
my_unique_ptr<Bar, decltype(destroy), destroy> p{create()};
Justin
quelle
Raffiniert. Stimmt es, dass dies die gleichen Vorteile erzielt (halbierter Speicheraufwand, Aufrufen der Funktion direkt statt über den Funktionszeiger, Aufrufen der potenziellen Inlining-Funktion vollständig) wie der Funktor aus Ricis Antwort , nur mit weniger Boilerplate?
ShadowRanger
Ja, dies sollte alle Vorteile einer benutzerdefinierten Löschklasse bieten, da dies der Fall deleter_from_fnist.
rmcclellan
6

Sie können einfach std::bindmit Ihrer Zerstörungsfunktion verwenden.

std::unique_ptr<Bar, std::function<void(Bar*)>> bar(create(), std::bind(&destroy,
    std::placeholders::_1));

Natürlich können Sie auch ein Lambda verwenden.

std::unique_ptr<Bar, std::function<void(Bar*)>> ptr(create(), [](Bar* b){ destroy(b);});
mkaes
quelle
6

Sie wissen, dass die Verwendung eines benutzerdefinierten Löschers nicht der beste Weg ist, da Sie ihn in Ihrem gesamten Code erwähnen müssen.
Stattdessen können Sie Spezialisierungen zu Klassen auf Namespace-Ebene in hinzufügen::std solange benutzerdefinierte Typen beteiligt sind und Sie die Semantik respektieren, gehen Sie folgendermaßen vor:

Spezialisiert std::default_delete:

template <>
struct ::std::default_delete<Bar> {
    default_delete() = default;
    template <class U, class = std::enable_if_t<std::is_convertible<U*, Bar*>()>>
    constexpr default_delete(default_delete<U>) noexcept {}
    void operator()(Bar* p) const noexcept { destroy(p); }
};

Und vielleicht auch std::make_unique():

template <>
inline ::std::unique_ptr<Bar> ::std::make_unique<Bar>() {
    auto p = create();
    if (!p) throw std::runtime_error("Could not `create()` a new `Bar`.");
    return { p };
}
Deduplikator
quelle
2
Ich würde damit sehr vorsichtig sein. Das Öffnen stderöffnet eine ganz neue Dose Würmer. Beachten Sie auch, dass die Spezialisierung von std::make_uniquenach C ++ 20 nicht zulässig ist (daher sollte dies vorher nicht erfolgen), da C ++ 20 die Spezialisierung von Dingen stdnicht zulässt, bei denen es sich nicht um Klassenvorlagen handelt ( std::make_uniqueist eine Funktionsvorlage). Beachten Sie, dass Sie wahrscheinlich auch mit UB enden werden, wenn der übergebene Zeiger std::unique_ptr<Bar>nicht von create(), sondern von einer anderen Zuordnungsfunktion zugewiesen wurde.
Justin
Ich bin nicht davon überzeugt, dass dies erlaubt ist. Es scheint mir schwer zu beweisen, dass diese Spezialisierung std::default_deletedie Anforderungen der Originalvorlage erfüllt. Ich würde mir vorstellen, dass std::default_delete<Foo>()(p)dies eine gültige Schreibweise wäre. delete p;Wenn delete p;also eine Schreibweise gültig wäre (dh wenn sie Foovollständig ist), wäre dies nicht dasselbe Verhalten. Wenn delete p;das Schreiben ungültig war ( Foounvollständig ist), würde dies außerdem ein neues Verhalten für angeben std::default_delete<Foo>, anstatt das Verhalten gleich zu halten.
Justin
Die make_uniqueSpezialisierung ist problematisch, aber ich habe definitiv die std::default_deleteÜberladung verwendet (nicht mit Vorlagen versehen enable_if, nur für C-Strukturen wie OpenSSLs BIGNUM, die eine bekannte Zerstörungsfunktion verwenden, bei der keine Unterklassifizierung stattfinden wird), und es ist bei weitem der einfachste Ansatz der Rest des Codes verwenden kann einfach , unique_ptr<special_type>ohne zu dem Funktors Typen passieren , wie das Templat am Deleterganzen Körper , noch Gebrauch typedef/ usingeinen Namen zu geben , um die Art , diese Frage zu vermeiden.
ShadowRanger