Pregunta shared_ptr magic :)


El Sr. Lidström y yo tuvimos una discusión :)

El reclamo del Sr. Lidström es que una construcción shared_ptr<Base> p(new Derived); no requiere que Base tenga un destructor virtual:

Armen Tsirunyan: "¿En serio? shared_ptr limpiar correctamente? ¿Podría por favor en este caso demostrar cómo se podría implementar ese efecto?

Daniel Lidström: "Los shared_ptr usa su propio destructor para eliminar la instancia de Concrete. Esto se conoce como RAII dentro de la comunidad C ++. Mi consejo es que aprendas todo lo que puedas sobre RAII. Hará que tu codificación C ++ sea mucho más fácil cuando usas RAII en todas las situaciones ".

Armen Tsirunyan: "Sé sobre RAII, y también sé que eventualmente shared_ptr destructor puede eliminar el px almacenado cuando pn llega a 0. Pero si px tiene un puntero de tipo estático para Base y un puntero de tipo dinámico para Derived, a menos que Base tiene un destructor virtual, esto dará como resultado un comportamiento indefinido. Corrígeme si estoy equivocado."

Daniel Lidström: "Los shared_ptr sabe que el tipo estático es concreto. ¡Lo sabe desde que lo pasé en su constructor! Parece un poco mágico, pero puedo asegurarte que es por diseño y extremadamente agradable ".

Por lo tanto, juzgarnos. ¿Cómo es posible (si lo es) implementar shared_ptr sin requerir que las clases polimórficas tengan destructor virtual? Gracias por adelantado


76
2017-10-10 09:41


origen


Respuestas:


Sí, es posible implementar shared_ptr de esa manera. Boost lo hace y el estándar C ++ 11 también requiere este comportamiento. Como una flexibilidad adicional, shared_ptr maneja más que solo un contador de referencia. El llamado eliminador generalmente se coloca en el mismo bloque de memoria que también contiene los contadores de referencia. Pero la parte divertida es que el tipo de este eliminador no es parte del tipo shared_ptr. Esto se llama "borrado de tipo" y es básicamente la misma técnica utilizada para implementar las funciones polimórficas boost :: function o std :: function para ocultar el tipo de functor real. Para que su ejemplo funcione, necesitamos un constructor con plantilla:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

Entonces, si usas esto con tus clases Base y Derivadas ...

class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

... el constructor con plantilla con Y = Derived se usa para construir el objeto shared_ptr. El constructor tiene así la oportunidad de crear el objeto eliminador apropiado y los contadores de referencia y almacena un puntero a este bloque de control como un miembro de datos. Si el contador de referencia llega a cero, el eliminador previamente creado y con reconocimiento derivado se usará para deshacerse del objeto.

El estándar C ++ 11 tiene lo siguiente que decir acerca de este constructor (20.7.2.2.1):

Requiere:  p debe ser convertible a T*. Y será un tipo completo. La expresion delete p estará bien formado, tendrá un comportamiento bien definido y no arrojará excepciones.

Efectos: Construye un shared_ptr objeto ese posee el puntero p.

...

Y para el destructor (20.7.2.2.2):

Efectos: Si *this es vacío o comparte la propiedad con otro shared_ptr instancia (use_count() > 1), No hay efectos secundarios.   De lo contrario, si *this posee un objeto p y un eliminador d, d(p) se llama.    De lo contrario, si *this posee un puntero py delete p se llama.

(el énfasis es negrita)


67
2017-10-10 11:18



Cuando se crea shared_ptr almacena un eliminador objeto dentro de sí mismo. Se llama a este objeto cuando shared_ptr está a punto de liberar el recurso apuntado. Como sabes cómo destruir el recurso en el punto de construcción, puedes usar shared_ptr con tipos incompletos. Quien creó el shared_ptr almacenó un eliminador correcto allí.

Por ejemplo, puede crear un eliminador personalizado:

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p llamará DeleteDerived para destruir el objeto puntiagudo. La implementación hace esto automáticamente.


26
2017-10-10 09:47



Simplemente,

shared_ptr utiliza la función especial de eliminación creada por el constructor que siempre usa el destructor del objeto dado y no el destructor de Base, esto es un poco de trabajo con la programación meta de la plantilla, pero funciona.

Algo como eso

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}

14
2017-10-10 09:46