2010-05-10 29 views

Respuesta

52

Básicamente, shared_ptr tiene dos punteros: un puntero al objeto compartido y un puntero a una estructura que contiene dos recuentos de referencia: uno para "referencias fuertes" o referencias que tienen propiedad y uno para "referencias débiles" o referencias que no tienen propiedad.

Al copiar un shared_ptr, el constructor de copias incrementa el recuento de referencias fuertes. Cuando destruye un shared_ptr, el destructor disminuye el recuento de referencias fuertes y prueba si el recuento de referencias es cero; si lo es, el destructor elimina el objeto compartido porque ya no lo señala shared_ptr.

La cuenta de referencia débil se utiliza para admitir weak_ptr; básicamente, cada vez que se crea un weak_ptr a partir del shared_ptr, el recuento de referencia débil se incrementa, y cada vez que se destruye uno, el recuento de referencia débil se reduce. Siempre que el recuento de referencia fuerte o el recuento de referencia débil sea mayor que cero, la estructura de recuento de referencia no se destruirá.

Efectivamente, siempre que el recuento de referencias fuertes sea mayor que cero, el objeto compartido no se eliminará. Siempre que el recuento de referencia fuerte o el recuento de referencia débil no sea cero, la estructura de recuento de referencia no se eliminará.

+0

s/Mientras el recuento de referencia débil sea mayor/Siempre que el recuento de referencias sea mayor /, por supuesto. – MSalters

+0

@MSalters: Gracias. –

+1

excelente respuesta ... Estoy familiarizado con cómo funcionan los punteros inteligentes en general, pero nunca supe los detalles de shared_ptr – Polaris878

0

Tienen un recuento de referencia interno que se incrementa en el constructor/asignador de copia shared_ptr y decrementa en el destructor. Cuando el conteo llega a cero, se borra el puntero retenido.

Aquí está la biblioteca de Boost documentation para punteros inteligentes. Creo que la implementación de TR1 es casi la misma que boost::shared_ptr.

0

"puntero compartido es un puntero inteligente (un objeto de C++ facturan con el operador *() sobrecargados y el operador ->()) que mantiene un puntero a un objeto y un puntero a una cuenta de referencia compartido Cada vez que una copia del gusano. El puntero inteligente se crea utilizando el constructor de copia, el recuento de referencias se incrementa. Cuando se destruye un puntero compartido, el recuento de referencias de su objeto disminuye. Los punteros compartidos construidos a partir de punteros sin procesar tienen inicialmente un recuento de referencia de 1. Cuando llega el recuento de referencia 0, el objeto puntiagudo se destruye y la memoria que ocupa se libera. No es necesario destruir objetos explícitamente: se realizará automáticamente cuando se ejecute el destructor del último puntero. " De here.

10

Existen al menos tres mecanismos bien conocidos.

contadores externos

Cuando se crea el primer puntero compartida a un objeto, se crea y se inicializa a 1. Cuando se copia el puntero un objeto de recuento de referencia separado, se incrementa el contador de referencia; cuando se destruye un puntero, se reduce. La asignación del puntero aumenta un conteo y disminuye otro (en ese orden, o la autoasignación ptr=ptr se romperá). Si el recuento de referencias llega a cero, no existen más punteros y el objeto se elimina.

Contadores internos

un contador interno requiere que el objeto apuntado tiene un campo de contador. Esto generalmente se logra derivando de una clase base específica.A cambio, esto ahorra un montón de asignación de la cuenta de referencia, y que permite la creación repetida de punteros compartidos de punteros primas (con contadores externos, que terminarías de dos cargos de un objeto)

enlaces circulares

En lugar de utilizar un contador, puede mantener todos los punteros compartidos en un objeto en un gráfico circular. El primer puntero creado señala a sí mismo. Cuando copia un puntero, inserta la copia en el círculo. Cuando lo eliminas, lo quitas del círculo. Pero cuando el puntero destruido apunta hacia sí mismo, es decir, cuando es el único puntero, elimina el objeto apuntado.

El inconveniente es que eliminar un nodo de una lista circular de un solo enlace es bastante caro ya que tiene que recorrer todos los nodos para encontrar el predecesor. Esto puede ser especialmente doloroso debido a la mala ubicación de referencia.

Variaciones

la 2ª y 3ª idea se pueden combinar: la clase base puede ser parte de ese gráfico circular, en lugar de contener un recuento. Por supuesto, esto significa que el objeto solo se puede eliminar cuando apunta a sí mismo (duración del ciclo 1, sin punteros restantes). De nuevo, la ventaja es que puede crear punteros inteligentes desde punteros débiles, pero el mal rendimiento de eliminar un puntero de la cadena sigue siendo un problema.

La estructura de gráficos exacta para la idea 3 no importa demasiado. También podría crear una estructura de árbol binario, con el objeto apuntado en la raíz. De nuevo, la operación difícil es eliminar un nodo de puntero compartido de ese gráfico. El beneficio es que si tiene muchos punteros en muchos subprocesos, el crecimiento de una parte del gráfico no es una operación altamente disputada.

+0

'boost :: shared_ptr' utiliza el enfoque' Contadores externos '. ¿Hay alguna manera de implementar weak_ptr con el método 'Internal Counters' (sin reescribir la Tierra)? – Pavel

+0

@Pavel: Sí, pero es bastante complejo. Necesitará un 'operador nuevo' personalizado y especialmente su' operador eliminatorio' coincidente en la clase base. Usas esto para crear una "lápida" donde solía estar la clase base, de modo que el 'weak_ptr' continúa apuntando a esa lápida. – MSalters

14

En general, estoy de acuerdo con la respuesta de James McNellis. Sin embargo, hay un punto más que debe mencionarse.

Como ya sabrán, shared_ptr<T> también puede usarse cuando el tipo de T no está completamente definido.

Eso es:

class AbraCadabra; 

boost::shared_ptr<AbraCadabra> myPtr; 
// ... 

Esto compilará & trabajo. A diferencia de muchas otras implementaciones de punteros inteligentes, que en realidad exigen que el tipo encapsulado esté completamente definido para poder usarlos. Esto está relacionado con el hecho de que se supone que el puntero inteligente sabe eliminar el objeto encapsulado cuando ya no se hace referencia a él, y para eliminar un objeto, uno debe saber cuál es.

Esto se consigue mediante el siguiente truco: shared_ptr consta en realidad de los siguientes:

  1. Un puntero opaco al objeto
  2. contadores de referencia compartidos (lo que James McNellis describe)
  3. Un puntero a la fábrica asignada que sabe cómo destruir su objeto.

La fábrica anterior es un objeto de ayuda con una sola función virtual, que se supone eliminar su objeto de una manera correcta.

Esta fábrica se crea realmente cuando asigna un valor a su puntero compartido.

Es decir, el siguiente código

AbraCadabra* pObj = /* get it from somewhere */; 
myPtr.reset(pObj); 

Aquí es donde se asigna esta fábrica. Nota: la función reset es realmente una función plantilla. En realidad, crea la fábrica para el tipo especificado (tipo de objeto pasado como parámetro). Aquí es donde su tipo debe estar completamente definido. Es decir, si aún no está definido, obtendrá un error de compilación.

Tenga en cuenta también: si realmente crea un objeto de un tipo derivado (derivado de AbraCadabra) y lo asigna al shared_ptr, se eliminará de forma correcta incluso si su destructor no es virtual. El shared_ptr siempre eliminará el objeto de acuerdo con el tipo que se ve en la función reset.

De modo que shared_ptr es una variante bastante sofisticada de un puntero inteligente. Da una impresionante flexibilidad. Sin embargo, debe saber que esta flexibilidad tiene un precio de un rendimiento extremadamente malo en comparación con otras posibles implementaciones del puntero inteligente.

Por otro lado, existen los denominados punteros inteligentes "intrusivos". No tienen toda esa flexibilidad, sin embargo, en cambio, ofrecen el mejor rendimiento.

Pros de shared_ptr en comparación con los punteros inteligentes inrusive:

  • uso muy flexible. Solo tiene que definir el tipo encapsulado al asignarlo al shared_ptr. Esto es muy valioso para grandes proyectos, reduce enormemente las dependencias.
  • El tipo encapsulado no tiene que tener un destructor virtual, aún los tipos polimórficos se eliminarán correctamente.
  • Puede usarse con punteros débiles.

contras de shared_ptr en comparación con los punteros inteligentes inrusive:

  1. rendimiento muy brutal y desperdicios de memoria del montón. En la asignación asigna 2 objetos más: contadores de referencia, más la fábrica (pérdida de memoria, lento). Sin embargo, esto solo ocurre en reset. Cuando uno shared_ptr se asigna a otro, no se asigna nada más.
  2. Lo anterior puede arrojar una excepción. (condición de falta de memoria). Por el contrario, los punteros inteligentes intrusivos nunca pueden arrojarse (aparte de las excepciones de proceso relacionadas con acceso a memoria no válido, desbordamiento de pila, etc.)
  3. La eliminación de su objeto también es lenta: necesita desasignar otras dos estructuras.
  4. Al trabajar con punteros inteligentes intrusivos, puede mezclar punteros inteligentes con los más crudos. Esto está bien porque el recuento de referencia real reside dentro del objeto en sí, que es único. En contraste - con shared_ptr puede no mezclar con punteros sin procesar.

    AbraCadabra * pObj =/* obtener de algún lugar * /; myPtr.reset (pObj); // ... pObj = myPtr.obtener(); boost :: shared_ptr myPtr2 (pObj); // oops

Lo anterior se bloqueará.

+0

Menciona weak_ptr como una ventaja del diseño shared_ptr. ¿No es posible tener weak_ptr con intrusivo ptr? ¿O implicaría reinventar shared_ptr junto con ptr intrusivo? – Pavel

Cuestiones relacionadas