Comment utiliser un suppresseur personnalisé avec un membre std :: unique_ptr?

133

J'ai une classe avec un membre unique_ptr.

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

La barre est une classe tierce qui a une fonction create () et une fonction destroy ().

Si je voulais utiliser un std::unique_ptravec lui dans une fonction autonome, je pourrais faire:

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

Existe-t-il un moyen de faire cela en std::unique_ptrtant que membre d'une classe?

huitlarc
la source

Réponses:

133

En supposant que createet destroysont des fonctions gratuites (ce qui semble être le cas de l'extrait de code de l'OP) avec les signatures suivantes:

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

Tu peux écrire ta classe Foocomme ça

class Foo {

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

    // ...

public:

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

    // ...
};

Notez que vous n'avez pas besoin d'écrire de lambda ou de suppresseur personnalisé ici car il destroys'agit déjà d'un deleter.

Cassio Neri
la source
156
Avec C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe
1
L'inconvénient de cette solution est qu'elle double la surcharge de tout unique_ptr(ils doivent tous stocker le pointeur de fonction avec le pointeur vers les données réelles), nécessite de passer la fonction de destruction à chaque fois, elle ne peut pas être intégrée (car le modèle ne peut pas se spécialiser dans la fonction spécifique, uniquement la signature), et doit appeler la fonction via le pointeur (plus coûteux que l'appel direct). Les réponses de rici et de Deduplicator évitent tous ces coûts en se spécialisant dans un foncteur.
ShadowRanger
@ShadowRanger n'est-il pas défini sur default_delete <T> et le pointeur de fonction stocké à chaque fois que vous le transmettez explicitement ou non?
Herrgott le
117

Il est possible de le faire proprement en utilisant un lambda en C ++ 11 (testé dans G ++ 4.8.2).

Compte tenu de ce réutilisable typedef:

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

Tu peux écrire:

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

Par exemple, avec un FILE*:

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

Avec cela, vous bénéficiez des avantages d'un nettoyage sans risque d'exception à l'aide de RAII, sans avoir besoin de bruit d'essai / capture.

Drew Noakes
la source
2
Cela devrait être la réponse, imo. C'est une plus belle solution. Ou y a-t-il des inconvénients, comme par exemple avoir std::functiondans la définition ou autre?
j00hi
17
@ j00hi, à mon avis, cette solution a des frais généraux inutiles à cause de std::function. La classe Lambda ou personnalisée comme dans la réponse acceptée peut être intégrée contrairement à cette solution. Mais cette approche présente un avantage dans le cas où vous souhaitez isoler toute implémentation dans un module dédié.
magras
5
Cela entraînera une fuite de mémoire si le constructeur std :: function lance (ce qui peut arriver si lambda est trop grand pour tenir dans l'objet std :: function)
StaceyGirl
4
Est-ce que lambda est vraiment nécessaire ici? Cela peut être simple deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);si customdeletersuit la convention (il retourne void et accepte le pointeur brut comme argument).
Victor Polevoy
Il y a un inconvénient à cette approche. std :: function n'est pas obligé d'utiliser le constructeur de déplacement dans la mesure du possible. Cela signifie que lorsque vous std :: move (my_deleted_unique_ptr), le contenu entouré par lambda sera probablement copié au lieu d'être déplacé, ce qui peut être ou non ce que vous voulez.
GeniusIsme
71

Il vous suffit de créer une classe de suppression:

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

et fournissez-le comme argument modèle de unique_ptr. Vous devrez toujours initialiser le unique_ptr dans vos constructeurs:

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

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

Autant que je sache, toutes les bibliothèques c ++ populaires implémentent cela correctement; comme BarDeleter n'a en fait aucun état, il n'a pas besoin d'occuper d'espace dans le fichier unique_ptr.

rici
la source
8
cette option est la seule qui fonctionne avec les tableaux, std :: vector et d'autres collections puisqu'elle peut utiliser le constructeur std :: unique_ptr de paramètre zéro. d'autres réponses utilisent des solutions qui n'ont pas accès à ce constructeur de paramètre zéro car une instance Deleter doit être fournie lors de la construction d'un pointeur unique. Mais cette solution fournit une classe Deleter ( struct BarDeleter) à std::unique_ptr( std::unique_ptr<Bar, BarDeleter>) qui permet au std::unique_ptrconstructeur de créer lui-même une instance Deleter. c'est-à-dire que le code suivant est autoriséstd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF
13
Je typedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
créerais
@DavidF: Ou utilisez l'approche de Deduplicator , qui présente les mêmes avantages (suppression en ligne, pas de stockage supplémentaire sur chacun unique_ptr, pas besoin de fournir une instance du suppresseur lors de la construction), et ajoute l'avantage de pouvoir utiliser std::unique_ptr<Bar>n'importe où sans avoir besoin de se souvenir pour utiliser le typedeffournisseur spécial ou explicitement le deuxième paramètre de modèle. (Pour être clair, c'est une bonne solution, j'ai voté à la hausse, mais cela arrête une étape de moins qu'une solution transparente)
ShadowRanger
22

À moins que vous n'ayez besoin de pouvoir modifier le suppresseur au moment de l'exécution, je vous recommande vivement d'utiliser un type de suppression personnalisé. Par exemple, si l'on utilise un pointeur de fonction pour votre Deleter, sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). En d'autres termes, la moitié des octets de l' unique_ptrobjet sont gaspillés.

Cependant, écrire un suppresseur personnalisé pour envelopper chaque fonction est un problème. Heureusement, nous pouvons écrire un type basé sur la fonction:

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

Avant 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
la source
Nifty. Ai-je raison de dire que cela permet d'obtenir les mêmes avantages (réduction de moitié de la surcharge de mémoire, appel de la fonction directement plutôt que par le biais du pointeur de fonction, appel de la fonction inlining potentielle entièrement) que le foncteur de la réponse de rici , juste avec moins de passe-partout?
ShadowRanger
Oui, cela devrait offrir tous les avantages d'une classe de suppression personnalisée, car c'est ce qui deleter_from_fnest.
rmcclellan le
7

Vous savez, utiliser un suppresseur personnalisé n'est pas la meilleure façon de procéder, car vous devrez le mentionner partout dans votre code.
Au lieu de cela, comme vous êtes autorisé à ajouter des spécialisations aux classes au niveau de l'espace de noms ::stdtant que des types personnalisés sont impliqués et que vous respectez la sémantique, procédez comme suit :

Spécialisez 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); }
};

Et peut-être aussi faire 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 };
}
Déduplicateur
la source
2
Je serais très prudent avec cela. L'ouverture stdouvre une toute nouvelle boîte de vers. Notez également que la spécialisation de std::make_uniquen'est pas autorisée après C ++ 20 (donc ne devrait pas être faite avant) car C ++ 20 interdit la spécialisation de choses dans stdlesquelles ne sont pas des modèles de classe ( std::make_uniqueest un modèle de fonction). Notez que vous vous retrouverez probablement également avec UB si le pointeur passé dans std::unique_ptr<Bar>n'a pas été alloué depuis create(), mais depuis une autre fonction d'allocation.
Justin
Je ne suis pas convaincu que cela soit autorisé. Il me semble difficile de prouver que cette spécialisation std::default_deleterépond à l'exigence du modèle d'origine. J'imagine que ce std::default_delete<Foo>()(p)serait une manière valide d'écrire delete p;, donc si delete p;serait valide pour écrire (c'est-à-dire si Fooc'est complet), ce ne serait pas le même comportement. De plus, si l' delete p;écriture n'était pas valide ( Fooest incomplète), ce serait spécifier un nouveau comportement pour std::default_delete<Foo>, plutôt que de garder le même comportement.
Justin
La make_uniquespécialisation est problématique, mais j'ai définitivement utilisé la std::default_deletesurcharge (pas de modèle avec enable_if, juste pour les structures C comme OpenSSL BIGNUMqui utilisent une fonction de destruction connue, où le sous-classement ne se produira pas), et c'est de loin l'approche la plus simple, car le reste de votre code peut simplement être utilisé unique_ptr<special_type>sans avoir à passer le type de foncteur comme modèle Deleterpartout, ni utiliser typedef/ usingpour donner un nom audit type pour éviter ce problème.
ShadowRanger
6

Vous pouvez simplement utiliser std::bindavec une fonction de destruction.

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

Mais bien sûr, vous pouvez également utiliser un lambda.

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