c++ - make_unique - ¿Por qué es shared_ptr<void> legal, mientras que unique_ptr<void> está mal formado?
example shared-ptr (2)
Esto se debe a que
std::shared_ptr
implementa el borrado de tipos, mientras que
std::unique_ptr
no.
Dado que
std::shared_ptr
implementa type-
std::shared_ptr
, también admite
otra
propiedad interesante, a saber.
no
necesita el tipo del eliminador
como argumento de tipo de plantilla
para la plantilla de clase.
Mira sus declaraciones:
template<class T,class Deleter = std::default_delete<T> >
class unique_ptr;
que tiene
Deleter
como parámetro de tipo, mientras que
template<class T>
class shared_ptr;
no lo tiene
Ahora la pregunta es, ¿por qué
shared_ptr
implementa el borrado de tipos?
Bueno, lo hace, porque tiene que admitir el recuento de referencias, y para admitir esto, tiene que asignar memoria del montón y dado que
tiene que
asignar memoria de todos modos, va un paso más allá e implementa el borrado de tipo, que necesita montón asignación también.
¡Así que básicamente es solo ser oportunista!
Debido a la
std::shared_ptr
de tipos,
std::shared_ptr
puede admitir dos cosas:
-
Puede almacenar objetos de cualquier tipo como
void*
, pero aún así puede eliminar los objetos en la destrucción correctamente invocando correctamente su destructor . - El tipo de eliminador no se pasa como argumento de tipo a la plantilla de clase, lo que significa un poco de libertad sin comprometer la seguridad de tipo .
Bien.
De eso se trata
std::shared_ptr
.
Ahora la pregunta es, ¿puede
std::unique_ptr
almacenar objetos
como
void*
?
Bueno, la respuesta es
sí
, siempre que pase un eliminador adecuado como argumento.
Aquí hay una de esas demostraciones:
int main()
{
auto deleter = [](void const * data ) {
int const * p = static_cast<int const*>(data);
std::cout << *p << " located at " << p << " is being deleted";
delete p;
};
std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);
} //p will be deleted here, both p ;-)
Salida ( demostración en línea ):
959 located at 0x18aec20 is being deleted
Hiciste una pregunta muy interesante en el comentario:
En mi caso, necesitaré un borrador de borrado de tipo, pero también parece posible (a costa de una asignación de montón). Básicamente, ¿significa esto que en realidad hay un lugar de nicho para un tercer tipo de puntero inteligente: un puntero inteligente de propiedad exclusiva con borrado de tipo.
a lo que @Steve Jessop sugirió la siguiente solución,
En realidad nunca he intentado esto, pero ¿tal vez podrías lograrlo usando una
std::function
apropiada como el tipo deunique_ptr
conunique_ptr
? Supongamos que realmente funciona, entonces ya está hecho, propiedad exclusiva y un borrador de tipo borrado.
Siguiendo esta sugerencia, implementé esto (aunque no hace uso de
std::function
ya que no parece necesario):
using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;
template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
return unique_void_ptr(ptr, [](void const * data) {
T const * p = static_cast<T const*>(data);
std::cout << "{" << *p << "} located at [" << p << "] is being deleted./n";
delete p;
});
}
int main()
{
auto p1 = unique_void(new int(959));
auto p2 = unique_void(new double(595.5));
auto p3 = unique_void(new std::string("Hello World"));
}
Salida ( demostración en línea ):
{Hello World} located at [0x2364c60] is being deleted.
{595.5} located at [0x2364c40] is being deleted.
{959} located at [0x2364c20] is being deleted.
Espero que ayude.
La pregunta realmente encaja en el título: tengo curiosidad por saber cuál es la razón técnica de esta diferencia, pero también la razón.
std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;
Uno de los fundamentos está en uno de los muchos casos de uso de un
shared_ptr
, es decir, como un indicador de vida útil o centinela.
Esto se mencionó en la documentación original de impulso:
auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
auto closure_target = { closure, std::weak_ptr<void>(pv) };
...
// store the target somewhere, and later....
}
void call_closure(closure_target target)
{
// test whether target of the closure still exists
auto lock = target.sentinel.lock();
if (lock) {
// if so, call the closure
target.closure();
}
}
Donde
closure_target
es algo como esto:
struct closure_target {
std::function<void()> closure;
std::weak_ptr<void> sentinel;
};
La persona que llama registraría una devolución de llamada como esta:
struct active_object : std::enable_shared_from_this<active_object>
{
void start() {
event_emitter_.register_callback([this] { this->on_callback(); },
shared_from_this());
}
void on_callback()
{
// this is only ever called if we still exist
}
};
debido a que
shared_ptr<X>
siempre es convertible a
shared_ptr<void>
, el event_emitter ahora puede ser felizmente inconsciente del tipo de objeto al que está llamando de nuevo.
Este acuerdo libera a los suscriptores del emisor de eventos de la obligación de manejar los casos cruzados (¿qué sucede si la devolución de llamada ingresa en una cola, esperando ser procesada mientras active_object desaparece?), Y también significa que no hay necesidad de sincronizar la cancelación de la suscripción.
weak_ptr<void>::lock
es una operación sincronizada.