¿Cómo es posible (si lo es) implementar shared_ptr sin requerir que las clases polimórficas tengan un destructor virtual?

6 minutos de lectura

Avatar de usuario de Armen Tsirunyan
Armen Tsirunyan

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

La afirmación 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? ¿El ptr_compartido limpiar correctamente? ¿Podría, en este caso, demostrar cómo se podría implementar ese efecto?”

Daniel Lidström: “Él ptr_compartido utiliza su propio destructor para eliminar la instancia de Concrete. Esto se conoce como RAII dentro de la comunidad de C++. Mi consejo es que aprendas todo lo que puedas sobre RAII. Hará que su codificación C++ sea mucho más fácil cuando use RAII en todas las situaciones”.

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

Daniel Lidström: “Él ptr_compartido sabe que el tipo estático es Concreto. ¡Lo sabe desde que lo pasé en su constructor! Parece un poco mágico, pero les puedo asegurar que es por diseño y extremadamente agradable”.

Entonces, júzguenos. ¿Cómo es posible (si lo es) implementar ptr_compartido sin requerir clases polimórficas para tener destructor virtual?

  • Otra cosa interesante es que shared_ptr<void> p(new Derived) también destruirá el Derived objeto por su destructor, sin importar si es virtual O no.

    – dalle

    10 de octubre de 2010 a las 9:57

  • Impresionante manera de hacer una pregunta 🙂

    – rubenvb

    10 de octubre de 2010 a las 11:42

  • Aunque shared_ptr permite esto, es un muy mala idea para diseñar una clase como base sin un dtor virtual. Los comentarios de Daniel sobre RAII son engañosos, no tiene nada que ver con esto, pero la conversación citada parece una simple falta de comunicación (y una suposición incorrecta de cómo funciona shared_ptr).

    Roger Paté

    11/10/2010 a las 18:52


  • No RAII, sino que borra el tipo del destructor. Hay que tener cuidado, porque shared_ptr<T>( (T*)new U() ) dónde struct U:T no hará lo correcto (y esto se puede hacer indirectamente fácilmente, como una función que toma un T* y se pasa un U*)

    – Yakk – Adam Nevraumont

    23 de julio de 2014 a las 0:52

  • Vaya, esta pregunta se hizo el 10/10/10 a las (casi) 10:00.

    – David G.

    12 de diciembre de 2017 a las 1:23

avatar de usuario de sellibitze
vender

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 administra más que solo un contador de referencia. El llamado borrador 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 borrador no es parte del tipo shared_ptr. Esto se llama “borrado de tipos” 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 funtor 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 Derived

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

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

… el constructor con plantilla con Y=Derived se utiliza para construir el shared_ptr objeto. 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 previamente creado y DerivedSe utilizará un eliminador consciente para deshacerse del objeto.

El estándar C++11 dice lo siguiente sobre 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 en negrita es mío).

  • the upcoming standard also requires this behaviour: (a) ¿Qué norma y (b) puede proporcionar una referencia (a la norma)?

    – Kevinarpe

    3 oct 2016 a las 9:30


  • Solo quiero agregar un comentario a la respuesta de @sellibitze ya que no tengo suficientes puntos para add a comment. OMI, es más Boost does this que the Standard requires. No creo que el Estándar requiera eso por lo que entiendo. Hablando del ejemplo de @sellibitze shared_ptr<Base> sp (new Derived);, Requiere de constructor solo pide delete Derived estar bien definido y bien formado. Para la especificación de destructortambién hay una ppero no creo que se refiera a la p en la especificación de constructor.

    – Lujun Weng

    6 de mayo de 2019 a las 5:06

Avatar de usuario de Yakov Galka
Yakov Galka

Cuando se crea shared_ptr, almacena un borrador objeto dentro de sí mismo. Se llama a este objeto cuando shared_ptr está a punto de liberar el recurso apuntado. Como sabe cómo destruir el recurso en el punto de construcción, puede usar shared_ptr con tipos incompletos. Quienquiera que haya creado 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á a DeleteDerived para destruir el objeto puntiagudo. La implementación hace esto automáticamente.

  • +1 por el comentario sobre tipos incompletos, muy útil cuando se usa un shared_ptr como un atributo.

    – Matthieu M.

    10 de octubre de 2010 a las 10:28

Simplemente,

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

Algo como eso

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

  • hmm… interesante, estoy empezando a creer esto 🙂

    – Armen Tsirunyan

    10 de octubre de 2010 a las 9:50

  • @Armen Tsirunyan Debería haber echado un vistazo a la descripción del diseño de shared_ptr antes de comenzar la discusión. Esta ‘captura del eliminador’ es una de las características esenciales de shared_ptr…

    – Paul Michalik

    10 de octubre de 2010 a las 10:16

  • @paul_71: Estoy de acuerdo contigo. Por otro lado, creo que esta discusión fue útil no solo para mí, sino también para otras personas que no sabían este hecho sobre shared_ptr. Así que supongo que no fue un gran pecado comenzar este hilo de todos modos 🙂

    – Armen Tsirunyan

    10 de octubre de 2010 a las 10:20

  • @Armen Por supuesto que no. Más bien, hizo un buen trabajo al señalar esta característica realmente muy importante de shared_ptr que con frecuencia es supervisada incluso por desarrolladores experimentados de C++.

    – Paul Michalik

    10 de octubre de 2010 a las 10:42

¿Ha sido útil esta solución?