shared_ptr - smart pointers c++11
Punteros vacíos compartidos ¿Por qué funciona esto? (2)
Para resolver un problema muy peculiar en mi aplicación, necesito un puntero compartido para los datos asignados, pero para el mundo exterior, el tipo de datos subyacente debe permanecer oculto.
Podría resolver esto haciendo algún tipo de clase raíz de la cual todas mis otras clases heredan, y usar un shared_ptr en esta clase raíz, como esto:
std::shared_ptr<Root>
Sin embargo:
- No quiero que todas mis clases se hereden de esta clase raíz solo para poder tener este puntero compartido
- Algunas veces quiero devolver un puntero compartido a std :: vector, o std :: list, o std :: set, ... que obviamente no heredan de mi clase Root
Por extraño que parezca, parece que puedes crear un shared_ptr en void y esto parece funcionar correctamente, como se muestra en este ejemplo:
class X
{
public:
X() {std::cout << "X::ctor" << std::endl;}
virtual ~X() {std::cout << "X::dtor" << std::endl;}
};
typedef std::shared_ptr<void> SharedVoidPointer;
int main()
{
X *x = new X();
SharedVoidPointer sp1(x);
}
x se elimina correctamente y, en un experimento más amplio, pude verificar que el puntero compartido hace lo que debe hacer (eliminar x después de que la última shared_ptr apague la luz).
Por supuesto, esto resuelve mi problema, ya que ahora puedo devolver datos con un miembro de datos SharedVoidPointer y asegurarme de que se limpie correctamente donde debería estar.
¿Pero está garantizado que esto funcione en todos los casos? Claramente funciona en Visual Studio 2010, pero ¿esto también funciona correctamente en otros compiladores? ¿En otras plataformas?
Creo que el punto implícito de la pregunta es que no se puede eliminar por void*
, por lo que parece extraño que se pueda eliminar a través de shared_ptr<void>
.
No puedes eliminar un objeto a través de un void*
bruto principalmente porque no sabría a qué destructor llamar. Usar un destructor virtual no ayuda porque void
no tiene una vtable (y por lo tanto no tiene un destructor virtual).
James McNellis explicó claramente por qué shared_ptr<void>
funciona, pero hay algo más interesante aquí: Suponiendo que siga las mejores prácticas documentadas para usar siempre el siguiente formulario al invocar new
...
shared_ptr<T> p(new Y);
... no es necesario tener un destructor virtual cuando se usa shared_ptr . Esto es cierto ya sea que T es void
o en el caso más familiar donde T es una base polimórfica de Y.
Esto va en contra de una sabiduría convencional de larga data: que las clases de interfaz DEBEN tener destructores virtuales.
La preocupación de delete (void*)
del OP se resuelve por el hecho de que el constructor shared_ptr es una plantilla que recuerda el tipo de datos que necesita destruir . Este mecanismo resuelve el problema del destructor virtual exactamente de la misma manera.
Entonces, aunque el tipo real del objeto no se captura necesariamente en el tipo de shared_ptr en sí (ya que T no tiene que ser del mismo tipo que Y ), el shared_ptr recuerda qué tipo de objeto está sosteniendo y realiza una conversión a ese tipo (o hace algo equivalente a eso) cuando llega el momento de eliminar el objeto.
El constructor shared_ptr
que utiliza es en realidad una plantilla de constructor que tiene el siguiente aspecto:
template <typename U>
shared_ptr(U* p) { }
Sabe dentro del constructor cuál es el tipo real del puntero ( X
) y utiliza esta información para crear un functor que puede delete
correctamente el puntero y garantizar que se llame al destructor correcto. Este functor (llamado el "eliminador" de shared_ptr) generalmente se almacena junto con los recuentos de referencia utilizados para mantener la propiedad compartida del objeto.
Tenga en cuenta que esto solo funciona si pasa un puntero del tipo correcto al constructor shared_ptr
. Si hubieras dicho en su lugar:
SharedVoidPointer sp1(static_cast<void*>(x));
entonces esto no habría funcionado porque en la plantilla del constructor, U
sería void
, no X
El comportamiento habría sido indefinido, ya que no se le permite llamar a delete
con un puntero de anulación.
En general, está seguro si siempre llama a new
en la construcción de shared_ptr
y no separa la creación del objeto (el new
) de la toma de propiedad del objeto (la creación de shared_ptr
).