Como uso um deleter personalizado com um membro std :: unique_ptr?

133

Eu tenho uma classe com um membro unique_ptr.

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

A barra é uma classe de terceiros que possui uma função create () e uma função destroy ().

Se eu quisesse usar um std::unique_ptrcom ele em uma função autônoma, eu poderia fazer:

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

Existe uma maneira de fazer isso std::unique_ptrcomo membro de uma classe?

huitlarc
fonte

Respostas:

133

Supondo que createe destroysejam funções livres (o que parece ser o caso do snippet de código do OP) com as seguintes assinaturas:

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

Você pode escrever sua turma Fooassim

class Foo {

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

    // ...

public:

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

    // ...
};

Observe que você não precisa escrever nenhum deleter lambda ou personalizado aqui, porque ele destroyjá é um deleter.

Cassio Neri
fonte
156
Com C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe
1
A desvantagem dessa solução é que ela dobra a sobrecarga de todas unique_ptr( todas elas devem armazenar o ponteiro da função junto com o ponteiro nos dados reais), requer passar a função de destruição todas as vezes, não pode ser incorporada (já que o modelo não pode especialize-se na função específica, apenas a assinatura) e deve chamar a função pelo ponteiro (mais caro que a chamada direta). As respostas do rici e do Deduplicator evitam todos esses custos ao se especializar em um functor.
ShadowRanger 21/11
@ShadowRanger não está definido para default_delete <T> e o ponteiro de função armazenada toda vez que você o passa explicitamente ou não?
Herrgott 24/06
117

É possível fazer isso corretamente usando um lambda no C ++ 11 (testado no G ++ 4.8.2).

Dado este reutilizável typedef:

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

Você pode escrever:

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

Por exemplo, com um FILE*:

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

Com isso, você obtém os benefícios da limpeza com exceção de segurança usando RAII, sem a necessidade de tentar / capturar ruído.

Drew Noakes
fonte
2
Esta deve ser a resposta, imo. É uma solução mais bonita. Ou existem desvantagens, como por exemplo, ter std::functionna definição ou algo parecido?
j00hi
17
@ j00hi, na minha opinião, esta solução tem sobrecarga desnecessária por causa de std::function. A lambda ou a classe personalizada, como na resposta aceita, pode ser incorporada diferentemente desta solução. Mas essa abordagem tem vantagem no caso em que você deseja isolar toda a implementação no módulo dedicado.
Magras
5
Isto irá vazar memória se construtor std :: função lança (o que pode acontecer se lambda é grande demais para caber dentro de objeto std :: função)
StaceyGirl
4
O lambda realmente requer aqui? Pode ser simples deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);se customdeleterseguir a convenção (retorna nulo e aceita ponteiro bruto como argumento).
Victor Polevoy
Há uma desvantagem nessa abordagem. A função std :: não é necessária para usar o construtor move sempre que possível. Isso significa que quando você std :: move (my_deleted_unique_ptr), o conteúdo incluído pelo lambda possivelmente será copiado em vez de movido, o que pode ou não ser o que você deseja.
GeniusIsme
70

Você só precisa criar uma classe deleter:

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

e forneça-o como o argumento do modelo de unique_ptr. Você ainda precisará inicializar o unique_ptr em seus construtores:

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

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

Até onde eu sei, todas as bibliotecas populares do c ++ implementam isso corretamente; como BarDeleter na verdade não tem nenhum estado, não precisa ocupar nenhum espaço no unique_ptr.

rici
fonte
8
essa opção é a única que funciona com matrizes, std :: vector e outras coleções, pois pode usar o construtor std :: unique_ptr do parâmetro zero. outras respostas usam soluções que não têm acesso a esse construtor de parâmetro zero porque uma instância Deleter deve ser fornecida ao construir um ponteiro exclusivo. Mas essa solução fornece uma classe Deleter ( struct BarDeleter) para std::unique_ptr( std::unique_ptr<Bar, BarDeleter>) que permite ao std::unique_ptrconstrutor criar uma instância Deleter por conta própria. ou seja, o seguinte código é permitidostd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF 08/07
12
Gostaria de criar um typedef para o uso fáciltypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
davidf
@DavidF: Ou use a abordagem do Deduplicator , que tem as mesmas vantagens (exclusão inline, sem armazenamento extra em cada um unique_ptr, sem necessidade de fornecer uma instância do deleter ao construir) e agrega o benefício de poder usar em std::unique_ptr<Bar>qualquer lugar sem a necessidade de lembrar para usar o typedefprovedor especial ou explicitamente o segundo parâmetro do modelo. (Para ser claro, esta é uma boa solução, eu up-votado, mas ele pára um passo tímido de uma solução seamless)
ShadowRanger
22

A menos que você precise alterar o deleter em tempo de execução, recomendo fortemente o uso de um tipo personalizado. Por exemplo, se você usar um ponteiro de função para seu deleter sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*),. Em outras palavras, metade dos bytes do unique_ptrobjeto são desperdiçados.

Escrever um deleter personalizado para agrupar todas as funções é um problema. Felizmente, podemos escrever um tipo de modelo na função:

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

Antes do 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
fonte
Bacana. Estou certo de que isso alcança os mesmos benefícios (sobrecarga de memória reduzida pela metade, chamar a função diretamente, e não através do ponteiro da função, a chamada de função embutida potencial desaparecer completamente) que o functor da resposta de rici , apenas com menos clichê?
ShadowRanger
Sim, isso deve fornecer todos os benefícios de uma classe deleter personalizada, pois é isso que deleter_from_fné.
rmcclellan
6

Você pode simplesmente usar std::binduma função de destruição.

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

Mas é claro que você também pode usar um lambda.

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

Você sabe, usar um deleter personalizado não é o melhor caminho a percorrer, pois você precisará mencioná-lo em todo o seu código.
Em vez disso, como você pode adicionar especializações a classes no nível de namespace no::std , desde que tipos personalizados estejam envolvidos e você respeite a semântica, faça o seguinte:

Especializar 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 talvez também faça 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 };
}
Desduplicador
fonte
2
Eu teria muito cuidado com isso. A abertura stdabre uma nova lata de vermes. Observe também que a especialização de std::make_uniquenão é permitida após o C ++ 20 (portanto, isso não deveria ser feito antes) porque o C ++ 20 desaprova a especialização de coisas nas stdquais não são modelos de classe ( std::make_uniqueé um modelo de função). Observe que você provavelmente também terminará com UB se o ponteiro passado std::unique_ptr<Bar>não tiver sido alocado de create(), mas de alguma outra função de alocação.
Justin
Não estou convencido de que isso seja permitido. Parece-me difícil provar que essa especialização std::default_deleteatende aos requisitos do modelo original. Eu imaginaria que std::default_delete<Foo>()(p)seria uma maneira válida de escrever delete p;, portanto, se delete p;seria válido escrever (ou seja, se Fooestiver completo), esse não seria o mesmo comportamento. Além disso, se delete p;a gravação fosse inválida ( Fooestá incompleta), isso seria especificar um novo comportamento para std::default_delete<Foo>, em vez de manter o mesmo comportamento.
11747 Justin
A make_uniqueespecialização é problemática, mas eu definitivamente usei a std::default_deletesobrecarga (não modelada com enable_if, apenas para estruturas C como o OpenSSL BIGNUMque usam uma função de destruição conhecida, onde a subclasse não vai acontecer), e é de longe a abordagem mais fácil, pois o restante do seu código pode ser usado apenas unique_ptr<special_type>sem a necessidade de passar o tipo de functor como o modelo Deletertodo, nem usar typedef/ usingpara dar um nome ao referido tipo para evitar esse problema.
ShadowRanger