¿Cómo uso un eliminador personalizado con un miembro std :: unique_ptr?

133

Tengo una clase con un miembro unique_ptr.

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

The Bar es una clase de terceros que tiene una función create () y una función destroy ().

Si quisiera usar un std::unique_ptrcon él en una función independiente, podría hacer:

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

¿Hay alguna manera de hacer esto std::unique_ptrcomo miembro de una clase?

huitlarc
fuente

Respuestas:

133

Asumiendo eso createy destroyson funciones libres (que parece ser el caso del fragmento de código del OP) con las siguientes firmas:

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

Puedes escribir tu clase Fooasí

class Foo {

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

    // ...

public:

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

    // ...
};

Tenga en cuenta que no necesita escribir ningún borrador lambda o personalizado aquí porque destroyya es un borrador.

Cassio Neri
fuente
156
Con C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe
1
La desventaja de esta solución es que duplica la sobrecarga de todos unique_ptr(todos deben almacenar el puntero de la función junto con el puntero a los datos reales), requiere pasar la función de destrucción cada vez, no puede estar en línea (ya que la plantilla no puede se especializa en la función específica, solo la firma), y debe llamar a la función a través del puntero (más costoso que la llamada directa). Tanto las respuestas de rici como las de Deduplicator evitan todos estos costos al especializarse en un functor.
ShadowRanger
@ShadowRanger ¿no está definido para default_delete <T> y el puntero de función almacenado cada vez que lo pase explícitamente o no?
Herrgott
117

Es posible hacer esto limpiamente usando un lambda en C ++ 11 (probado en G ++ 4.8.2).

Dado esto reutilizable typedef:

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

Puedes escribir:

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

Por ejemplo, con un FILE*:

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

Con esto, obtiene los beneficios de una limpieza segura con excepción utilizando RAII, sin necesidad de intentar / detectar ruido.

Drew Noakes
fuente
2
Esta debería ser la respuesta, imo. Es una solución más hermosa. ¿O hay inconvenientes, como por ejemplo tener std::functionen la definición o algo así?
j00hi
17
@ j00hi, en mi opinión, esta solución tiene una sobrecarga innecesaria debido a std::function. Lambda o clase personalizada como en la respuesta aceptada se pueden incluir a diferencia de esta solución. Pero este enfoque tiene ventaja en caso de que desee aislar toda la implementación en un módulo dedicado.
magras
55
Esto perderá memoria si se produce std :: function constructor (lo que podría suceder si lambda es demasiado grande para caber dentro de std :: function object)
StaceyGirl
44
¿Lambda realmente requiere aquí? Puede ser simple deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);si customdeletersigue la convención (devuelve vacío y acepta un puntero sin formato como argumento).
Victor Polevoy
Hay un inconveniente en este enfoque. No se requiere std :: function para usar move constructor siempre que sea posible. Esto significa que cuando std :: move (my_deleted_unique_ptr), los contenidos encerrados por lambda posiblemente se copiarán en lugar de moverse, lo que puede o no ser lo que desea.
GeniusIsme
70

Solo necesita crear una clase de eliminación:

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

y proporcionarlo como argumento de plantilla de unique_ptr. Aún tendrá que inicializar el unique_ptr en sus constructores:

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

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

Hasta donde yo sé, todas las bibliotecas populares de c ++ implementan esto correctamente; dado BarDeleter que en realidad no tiene ningún estado, no necesita ocupar ningún espacio en el unique_ptr.

rici
fuente
8
esta opción es la única que funciona con matrices, std :: vector y otras colecciones, ya que puede usar el parámetro cero constructor std :: unique_ptr. otras respuestas usan soluciones que no tienen acceso a este constructor de parámetros cero porque se debe proporcionar una instancia de Deleter al construir un puntero único. Pero esta solución proporciona una clase Deleter ( struct BarDeleter) a std::unique_ptr( std::unique_ptr<Bar, BarDeleter>) que permite al std::unique_ptrconstructor crear una instancia Deleter por sí mismo. es decir, se permite el siguiente códigostd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF
12
typedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
Crearía
@DavidF: O use el enfoque de Deduplicator , que tiene las mismas ventajas (eliminación en línea, sin almacenamiento adicional en cada uno unique_ptr, no es necesario proporcionar una instancia del eliminador al construir), y agrega el beneficio de poder usar en std::unique_ptr<Bar>cualquier lugar sin necesidad de recordar para usar el typedefproveedor especial o explícito el segundo parámetro de plantilla. (Para ser claros, esta es una buena solución, voté a favor, pero se detiene un paso por
debajo
22

A menos que necesite poder cambiar el eliminador en tiempo de ejecución, le recomiendo utilizar un tipo de eliminador personalizado. Por ejemplo, si utiliza un puntero de función para su Deleter, sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). En otras palabras, la mitad de los bytes del unique_ptrobjeto se desperdician.

Sin embargo, escribir un eliminador personalizado para ajustar cada función es una molestia. Afortunadamente, podemos escribir un tipo con plantilla en la función:

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 de 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
fuente
Hábil. ¿Estoy en lo cierto de que esto logra los mismos beneficios (sobrecarga de memoria reducida a la mitad, llamada a la función directamente en lugar de a través del puntero de la función, llamada de función de alineación potencial por completo) como el functor de la respuesta de ricci , solo con menos repetitivo?
ShadowRanger
Sí, esto debería proporcionar todos los beneficios de una clase de eliminación personalizada, ya que eso deleter_from_fnes lo que es.
rmcclellan
6

Simplemente puede usar std::bindcon una función de destrucción.

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

Pero, por supuesto, también puedes usar una lambda.

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

Ya sabes, usar un eliminador personalizado no es la mejor manera de hacerlo, ya que tendrás que mencionarlo en todo tu código.
En cambio, como puede agregar especializaciones a las clases de nivel de espacio de nombres ::stdsiempre que estén involucrados tipos personalizados y respete la semántica, haga lo siguiente:

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

Y tal vez también lo haga 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 };
}
Deduplicador
fuente
2
Sería muy cuidadoso con esto. Abrir stdabre una nueva lata de gusanos. También tenga en cuenta que la especialización de std::make_uniqueno está permitida después de C ++ 20 (por lo tanto, no debería hacerse antes) porque C ++ 20 no permite la especialización de cosas en las stdque no hay plantillas de clase ( std::make_uniquees una plantilla de función). Tenga en cuenta que usted también probablemente terminará con UB cuando el puntero pasa al std::unique_ptr<Bar>no fueron asignados del create(), sino de alguna otra función de asignación.
Justin
No estoy convencido de que esto esté permitido. Me parece que es difícil demostrar que esta especialización std::default_deletecumple con el requisito de la plantilla original. Me imagino que std::default_delete<Foo>()(p)sería una forma válida de escribir delete p;, por lo que si delete p;fuera válido para escribir (es decir, si Fooestá completo), este no sería el mismo comportamiento. Además, si delete p;no fue válido escribir ( Fooestá incompleto), esto estaría especificando un nuevo comportamiento para std::default_delete<Foo>, en lugar de mantener el mismo comportamiento.
Justin
La make_uniqueespecialización es problemática, pero definitivamente he usado la std::default_deletesobrecarga (no con plantilla enable_if, solo para estructuras C como OpenSSL BIGNUMque usan una función de destrucción conocida, donde la subclasificación no va a suceder), y es, con mucho, el enfoque más fácil, ya que el resto de su código puede usarlo unique_ptr<special_type>sin necesidad de pasar el tipo de functor como el que está en plantilla Deleter, ni usar typedef/ usingpara dar un nombre a dicho tipo para evitar ese problema.
ShadowRanger