Jak używać niestandardowego narzędzia do usuwania z elementem członkowskim std :: unique_ptr?

137

Mam klasę z członkiem unique_ptr.

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

Bar jest klasą strony trzeciej, która ma funkcje create () i niszczą ().

Gdybym chciał użyć std::unique_ptrz nim jako samodzielnej funkcji, mógłbym zrobić:

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

Czy można to zrobić std::unique_ptrjako członek klasy?

huitlarc
źródło

Odpowiedzi:

137

Zakładając, że createi destroysą wolne funkcje (co wydaje się mieć miejsce w przypadku fragmentu kodu OP) z następującymi podpisami:

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

Możesz napisać swoją klasę w Footen sposób

class Foo {

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

    // ...

public:

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

    // ...
};

Zauważ, że nie musisz pisać tutaj żadnej wartości lambda ani niestandardowego narzędzia do usuwania, ponieważ destroyjest już usuwaczem.

Cassio Neri
źródło
163
W C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe
2
Wadą tego rozwiązania jest to, że podwaja narzut każdego unique_ptr(wszystkie muszą przechowywać wskaźnik funkcji wraz ze wskaźnikiem do rzeczywistych danych), wymaga przekazywania funkcji zniszczenia za każdym razem, nie może być wbudowana (ponieważ szablon nie może specjalizują się w określonej funkcji, tylko sygnatura) i muszą wywoływać funkcję za pomocą wskaźnika (droższe niż bezpośrednie wywołanie). Oba RICI i Deduplicator męska odpowiedzi uniknąć wszystkich tych kosztów przez specjalizującą się w funktora.
ShadowRanger
1
@ShadowRanger nie jest zdefiniowany jako default_delete <T> i przechowywany wskaźnik funkcji za każdym razem, niezależnie od tego, czy przekazujesz go jawnie, czy nie?
Herrgott
121

Można to zrobić czysto, używając lambdy w C ++ 11 (testowane w G ++ 4.8.2).

Biorąc pod uwagę to wielokrotnego użytku typedef:

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

Możesz pisać:

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

Na przykład z FILE*:

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

Dzięki temu uzyskujesz korzyści z wyjątkowo bezpiecznego czyszczenia przy użyciu RAII, bez konieczności próbowania / łapania hałasu.

Drew Noakes
źródło
2
To powinna być odpowiedź, imo. To piękniejsze rozwiązanie. Czy są jakieś wady, jak np. Posiadanie std::functionw definicji lub tym podobne?
j00hi
19
@ j00hi, moim zdaniem to rozwiązanie ma niepotrzebne obciążenie z powodu std::function. W przeciwieństwie do tego rozwiązania można wstawić klasę lambda lub niestandardową, jak w zaakceptowanej odpowiedzi. Ale takie podejście ma przewagę w przypadku, gdy chcesz odizolować całą implementację w dedykowanym module.
magras
5
Będzie to wyciek pamięci, jeśli konstruktor std :: funkcja rzuca (co może się zdarzyć, jeśli lambda jest zbyt duży, aby zmieścić wewnątrz obiektu std :: function)
StaceyGirl
4
Czy lambda naprawdę tu wymaga? Może być proste, deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);jeśli jest customdeleterzgodne z konwencją (zwraca void i przyjmuje surowy wskaźnik jako argument).
Victor Polevoy,
Takie podejście ma jedną wadę. std :: function nie jest wymagane, aby używać konstruktora przenoszenia, gdy tylko jest to możliwe. Oznacza to, że kiedy std :: move (my_deleted_unique_ptr), zawartość ujęta w lambdę prawdopodobnie zostanie skopiowana zamiast przeniesiona, co może być lub nie być tym, czego chcesz.
GeniusIsme
74

Musisz tylko utworzyć klasę usuwającą:

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

i podaj go jako argument szablonu unique_ptr. Nadal będziesz musiał zainicjować unique_ptr w swoich konstruktorach:

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

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

O ile wiem, wszystkie popularne biblioteki C ++ implementują to poprawnie; ponieważ w BarDeleter rzeczywistości nie ma żadnego stanu, nie musi zajmować żadnego miejsca w unique_ptr.

rici
źródło
8
ta opcja jest jedyną, która działa z tablicami, std :: vector i innymi kolekcjami, ponieważ może używać konstruktora std :: unique_ptr z parametrem zerowym. inne odpowiedzi używają rozwiązań, które nie mają dostępu do tego konstruktora o zerowym parametrze, ponieważ podczas konstruowania unikatowego wskaźnika należy podać instancję Deleter. Ale to rozwiązanie zapewnia klasę Deleter ( struct BarDeleter) to std::unique_ptr( std::unique_ptr<Bar, BarDeleter>), która umożliwia std::unique_ptrkonstruktorowi samodzielne utworzenie instancji Deleter. tj. następujący kod jest dozwolonystd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF
13
typedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
Stworzyłbym
@DavidF: Lub użyj podejścia Deduplicator , które ma te same zalety (usuwanie wewnętrzne, brak dodatkowego miejsca na każdym z nich unique_ptr, nie ma potrzeby dostarczania wystąpienia elementu usuwającego podczas konstruowania) i dodaje korzyści w postaci możliwości użycia w std::unique_ptr<Bar>dowolnym miejscu bez konieczności pamiętania aby użyć specjalnego typedeflub jawnego dostawcy drugiego parametru szablonu. (Żeby było jasne, to dobre rozwiązanie, zagłosowałem za, ale zatrzymuje się o jeden krok przed bezproblemowym rozwiązaniem)
ShadowRanger
24

O ile nie musisz mieć możliwości zmiany usuwania w czasie wykonywania, zdecydowanie zalecamy użycie niestandardowego typu usuwania. Na przykład, jeśli użyć wskaźnika funkcji dla Deleter, sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). Innymi słowy, unique_ptrmarnowana jest połowa bajtów obiektu.

Pisanie niestandardowego narzędzia do pakowania w celu zawijania każdej funkcji jest jednak kłopotliwe. Na szczęście możemy napisać typ szablonu na funkcji:

Od 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()};

Przed 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
źródło
Ładne. Czy mam rację, że daje to te same korzyści (zmniejszenie o połowę narzutu pamięci, wywoływanie funkcji bezpośrednio, a nie przez wskaźnik funkcji, potencjalne wywołanie funkcji wbudowanej całkowicie), co funktor z odpowiedzi rici , tylko z mniejszą liczbą schematów ?
ShadowRanger
Tak, powinno to zapewnić wszystkie zalety niestandardowej klasy usuwającej, ponieważ tak właśnie deleter_from_fnjest.
rmcclellan
8

Wiesz, używanie niestandardowego narzędzia do usuwania nie jest najlepszym rozwiązaniem, ponieważ będziesz musiał wspomnieć o tym w całym kodzie.
Zamiast tego, ponieważ możesz dodawać specjalizacje do klas na poziomie przestrzeni nazw, ::stdo ile są to typy niestandardowe i szanujesz semantykę, zrób to:

Specjalizacja std::default_delete:

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

A może też 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
źródło
2
Byłbym z tym bardzo ostrożny. Otwarcie stdotwiera zupełnie nową puszkę robaków. Zauważ również, że specjalizacja std::make_uniquenie jest dozwolona po C ++ 20 (dlatego nie powinna być wykonywana wcześniej), ponieważ C ++ 20 zabrania specjalizacji rzeczy, stdktóre nie są szablonami klas ( std::make_uniquejest szablonem funkcji). Zauważ, że prawdopodobnie skończysz z UB, jeśli wskaźnik przekazany do std::unique_ptr<Bar>nie został przydzielony z create(), ale z innej funkcji alokacji.
Justin,
Nie jestem przekonany, że jest to dozwolone. Wydaje mi się, że ciężko jest udowodnić, że ta specjalizacja std::default_deletespełnia wymagania oryginalnego szablonu. Wyobrażam sobie, że std::default_delete<Foo>()(p)byłby to prawidłowy sposób pisania delete p;, więc gdyby delete p;można było pisać (tj. Jeśli Foojest kompletny), nie byłoby to takie samo zachowanie. Ponadto, jeśli delete p;zapis Foobyłby nieprawidłowy ( jest niekompletny), oznaczałoby to określenie nowego zachowania std::default_delete<Foo>zamiast zachowania tego samego zachowania.
Justin,
make_uniqueSpecjalizacja jest problematyczne, ale ja na pewno wykorzystał std::default_deleteprzeciążenia (nie na matrycy z enable_if, tylko dla struktur C jak OpenSSL BIGNUM, które wykorzystują znaną funkcję zniszczenie, gdzie podklasy nie zdarzy), a to zdecydowanie najprostszy podejście, jak reszta twojego kodu może po prostu użyć unique_ptr<special_type>bez konieczności przekazywania typu funktora jako szablonu na Deletercałym świecie, ani używać typedef/ usingdo nadania nazwy wspomnianemu typowi, aby uniknąć tego problemu.
ShadowRanger
6

Możesz po prostu użyć std::bindfunkcji zniszczenia.

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

Ale oczywiście możesz też użyć lambdy.

std::unique_ptr<Bar, std::function<void(Bar*)>> ptr(create(), [](Bar* b){ destroy(b);});
mkaes
źródło