Come posso utilizzare un deleter personalizzato con un membro std :: unique_ptr?


133

Ho una lezione con un membro unique_ptr.

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

La barra è una classe di terze parti che ha una funzione create () e una funzione destroy ().

Se volessi usare a std::unique_ptrcon esso in una funzione autonoma, potrei fare:

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

C'è un modo per farlo con std::unique_ptrcome membro di una classe?

Risposte:


133

Supponendo che createe destroysiano funzioni gratuite (che sembra essere il caso dello snippet di codice del PO) con le seguenti firme:

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

Puoi scrivere la tua lezione in Fooquesto modo

class Foo {

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

    // ...

public:

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

    // ...
};

Si noti che non è necessario scrivere lambda o deleter personalizzato qui perché destroyè già un deleter.


156
Con C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe,

1
L'aspetto negativo di questa soluzione è che raddoppia il sovraccarico di tutti unique_ptr(devono tutti memorizzare il puntatore della funzione insieme al puntatore ai dati effettivi), richiede di passare la funzione di distruzione ogni volta, non può essere in linea (poiché il modello non può specializzarsi nella funzione specifica, solo la firma) e deve chiamare la funzione tramite il puntatore (più costoso della chiamata diretta). Entrambi i rici e Deduplicator delle risposte evitare tutti questi costi specializzandosi ad un funtore.
ShadowRanger

@ShadowRanger non è definito in default_delete <T> e il puntatore della funzione memorizzata ogni volta che lo passi esplicitamente o no?
Herrgott,

117

È possibile farlo in modo pulito usando un lambda in C ++ 11 (testato in G ++ 4.8.2).

Dato questo riutilizzabile typedef:

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

Tu puoi scrivere:

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

Ad esempio, con a FILE* :

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

Con questo si ottengono i vantaggi di una pulizia sicura delle eccezioni utilizzando RAII, senza bisogno di provare / catturare il rumore.


2
Questa dovrebbe essere la risposta, imo. È una soluzione più bella. O ci sono degli aspetti negativi, come ad esempio avere std::functionnella definizione o simili?
j00hi,

17
@ j00hi, a mio avviso questa soluzione ha un sovraccarico non necessario a causa di std::function. Lambda o classe personalizzata come nella risposta accettata possono essere incorporati diversamente da questa soluzione. Ma questo approccio ha il vantaggio nel caso in cui si desideri isolare tutta l'implementazione in un modulo dedicato.
magras,

5
Questo perderà memoria se viene lanciato il costruttore std :: function (che potrebbe accadere se lambda è troppo grande per rientrare nell'oggetto std :: function)
StaceyGirl

4
Lambda richiede davvero qui? Può essere semplice deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);se customdeletersegue la convenzione (restituisce void e accetta il puntatore non elaborato come argomento).
Victor Polevoy,

C'è un aspetto negativo di questo approccio. std :: function non è necessario per usare il costruttore di mosse quando possibile. Ciò significa che quando std :: move (my_deleted_unique_ptr), i contenuti racchiusi da lambda potrebbero essere copiati anziché spostati, il che potrebbe essere o meno ciò che si desidera.
GeniusIsme,

71

Devi solo creare una classe deleter:

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

e forniscilo come argomento modello di unique_ptr. Dovrai comunque inizializzare unique_ptr nei tuoi costruttori:

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

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

Per quanto ne so, tutte le librerie c ++ popolari implementano questo correttamente; poiché in BarDeleter realtà non ha alcuno stato, non ha bisogno di occupare spazio nel unique_ptr.


8
questa opzione è l'unica che funziona con matrici, std :: vector e altre raccolte poiché può utilizzare il parametro zero costruttore std :: unique_ptr. altre risposte utilizzano soluzioni che non hanno accesso a questo costruttore di parametri zero poiché è necessario fornire un'istanza di Deleter durante la costruzione di un puntatore univoco. Ma questa soluzione fornisce una classe Deleter ( struct BarDeleter) a std::unique_ptr( std::unique_ptr<Bar, BarDeleter>) che consente al std::unique_ptrcostruttore di creare un'istanza Deleter da sola. cioè è consentito il seguente codicestd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF

13
Vorrei creare un typedef per un facile utilizzotypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
DavidF

@DavidF: Oppure usa l'approccio di Deduplicator , che presenta gli stessi vantaggi (incorporando la cancellazione, nessuna memoria aggiuntiva su ciascuno unique_ptr, nessuna necessità di fornire un'istanza del deleter durante la costruzione) e aggiunge il vantaggio di poter usare std::unique_ptr<Bar>ovunque senza bisogno di ricordare per utilizzare lo speciale typedefo esplicitamente il provider il secondo parametro del modello. (Per essere chiari, questa è una buona soluzione, ho votato a favore, ma si ferma un passo prima di una soluzione senza soluzione di continuità)
ShadowRanger

22

A meno che non sia necessario poter modificare il deleter in fase di esecuzione, consiglio vivamente di utilizzare un tipo di deleter personalizzato. Ad esempio, se utilizzare un puntatore a funzione per la vostra deleter, sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). In altre parole, la metà dei byte unique_ptrdell'oggetto viene sprecata.

Scrivere un deleter personalizzato per avvolgere ogni funzione è comunque un problema. Per fortuna, possiamo scrivere un tipo basato sulla funzione:

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

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

Nifty. Ho ragione a dire che questo ottiene gli stessi vantaggi (dimezzare l'overhead della memoria, chiamare la funzione direttamente anziché tramite il puntatore della funzione, la potenziale chiamata della funzione inlining completamente via) come il funzione di risposta di rici , solo con meno plateplate?
ShadowRanger

Sì, questo dovrebbe fornire tutti i vantaggi di una classe di deleter personalizzata, poiché è quello che deleter_from_fnè.
rmcclellan,

7

Sai, utilizzare un deleter personalizzato non è il modo migliore per andare, poiché dovrai menzionarlo in tutto il codice.
Invece, poiché ti è permesso aggiungere specializzazioni alle classi a livello di spazio dei nomi ::stdfintanto che sono coinvolti tipi personalizzati e rispetti la semantica, procedi come segue:

Specializzare 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); }
};

E forse anche fare 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 };
}

2
Starei molto attento con questo. L'apertura stdapre una nuova lattina di vermi. Si noti inoltre che la specializzazione di std::make_uniquenon è consentita dopo C ++ 20 (quindi non dovrebbe essere eseguita prima) perché C ++ 20 non consente la specializzazione di cose in stdcui non sono modelli di classe ( std::make_uniqueè un modello di funzione). Nota che probabilmente finirai anche con UB se il puntatore passato std::unique_ptr<Bar>non è stato allocato da create(), ma da qualche altra funzione di allocazione.
Giustino,

Non sono convinto che questo sia permesso. Mi sembra difficile dimostrare che questa specializzazione std::default_deletesoddisfa i requisiti del modello originale. Immagino che std::default_delete<Foo>()(p)sarebbe un modo valido per scrivere delete p;, quindi se delete p;fosse valido per scrivere (cioè se Fooè completo), questo non sarebbe lo stesso comportamento. Inoltre, se delete p;non fosse valido scrivere ( Fooè incompleto), ciò significherebbe specificare un nuovo comportamento std::default_delete<Foo>, anziché mantenere lo stesso comportamento.
Giustino,

La make_uniquespecializzazione è problematica, ma ho sicuramente usato il std::default_deletesovraccarico (non basato su modelli enable_if, solo per le strutture C come OpenSSL BIGNUMche usano una nota funzione di distruzione, in cui la sottoclasse non avverrà), ed è di gran lunga l'approccio più semplice, poiché il resto del codice può semplicemente usare unique_ptr<special_type>senza la necessità di passare il tipo di funzione come modello Deleterdappertutto, né usare typedef/ usingper dare un nome a quel tipo per evitare quel problema.
ShadowRanger

6

Puoi semplicemente usare std::bindcon una tua funzione di distruzione.

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

Ma ovviamente puoi anche usare una lambda.

std::unique_ptr<Bar, std::function<void(Bar*)>> ptr(create(), [](Bar* b){ destroy(b);});
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.