¿Cómo funcionan los punteros compartidos?


¿Cómo saben los punteros compartidos cuántos punteros apuntan a ese objeto? (shared_ptr, en este caso)

Author: Constructor, 2010-05-10

5 answers

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.

Cuando copia un shared_ptr, el constructor de copia incrementa el número de referencias fuertes. Cuando se destruye un shared_ptr, el destructor decrementa el recuento de referencia fuerte y prueba si el recuento de referencia es cero; si es decir, el destructor elimina el objeto compartido porque ya no hay shared_ptrs que lo apunten.

El recuento de referencia débil se utiliza para soportar 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 el recuento de referencia débil se decrementa. Mientras 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 y cuando el el recuento de referencia fuerte es mayor que cero, el objeto compartido no se eliminará. Mientras el recuento de referencia fuerte o el recuento de referencia débil no sea cero, la estructura de recuento de referencia no se eliminará.

 55
Author: James McNellis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-10 14:55:43

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

Como usted puede saber, shared_ptr<T> también se puede usar cuando el tipo T no está completamente definido.

Es decir:

class AbraCadabra;

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

Esto compilará y funcionará. 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 debe saber eliminar el objeto encapsulado cuando ya no es referenciado, y para eliminar un objeto uno debe saber lo que es.

Esto se logra mediante el siguiente truco: shared_ptr en realidad consiste en lo siguiente:

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

La fábrica anterior es un objeto auxiliar con una sola función virtual, que se supone que borra 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 en realidad una función template. En realidad crea la fábrica para el tipo especificado (tipo del objeto pasado como parámetro). Aquí es donde su tipo debe estar totalmente definido. Es decir, si todavía 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 a shared_ptr, se eliminará de manera 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.

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

Por otro lado, hay los llamados punteros inteligentes "intrusivos". No tienen toda esa flexibilidad, sin embargo, en contraste, dan el mejor rendimiento.

Pros de shared_ptr comparado con punteros inteligentes intrusivos:

  • Uso muy flexible. Solo hay que definir el tipo encapsulado al asignarlo a shared_ptr. Esto es muy valioso para grandes proyectos, reduce las dependencias en gran medida.
  • El tipo encapsulado no tiene que tener un destructor virtual, los tipos polimórficos se eliminarán correctamente.
  • Se puede usar con punteros débiles.

Contras de shared_ptr comparado con punteros inteligentes intrusivos:

  1. Rendimiento muy bárbaro y desperdicio de memoria. 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 sucede en reset. Cuando se asigna un shared_ptr a otro, no se asigna nada más.
  2. Lo anterior puede generar una excepción. (estado fuera de memoria). En contraste, los punteros inteligentes intrusivos nunca pueden lanzar (aparte de las excepciones de proceso relacionadas con el acceso a memoria no válida, desbordamiento de pila, etc.)
  3. La eliminación de su objeto también es lenta: necesita desasignar otras dos estructuras.
  4. Cuando se trabaja con intrusive punteros inteligentes puede mezclar libremente los punteros inteligentes con los raw. Esto está bien porque el recuento de referencia real reside dentro del propio objeto, que es único. En contraste-con shared_ptr puedes no mezclar con punteros crudos.

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

Lo anterior se bloqueará.

 16
Author: valdo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-10 15:30:04

Existen al menos tres mecanismos bien conocidos.

Contadores externos

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

Contadores internos

Un contador interno requiere que el objeto apuntado tenga un campo contador. Esto generalmente se logra derivando de una clase base específica. A cambio, esto guarda una asignación de montón del recuento de referencia, y permite la creación repetida de punteros compartidos a partir de punteros sin procesar (con contadores externos, terminarías con dos recuentos para un objeto)

Enlaces circulares

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

La desventaja es que eliminar un nodo de una lista circular de enlace único es bastante costoso, ya que tiene que iterar sobre todos los nodos para encontrar el predecesor. Este puede ser especialmente doloroso debido a la mala localidad de referencia.

Variaciones

La 2a y 3a idea se pueden combinar: la clase base puede ser parte de ese gráfico circular, en lugar de contener un conteo. Por supuesto, esto significa que el objeto solo se puede eliminar cuando apunta a sí mismo (longitud del ciclo 1, no hay punteros restantes a él). Una vez más, la ventaja es que puede crear punteros inteligentes a partir de punteros débiles, pero el bajo rendimiento de eliminar un puntero de la cadena sigue siendo un cuestión.

La estructura exacta del gráfico para idea 3 no importa demasiado. También puede crear una estructura de árbol binaria, con el objeto apuntado en la raíz. Una vez más, la operación dura es eliminar un nodo puntero compartido de ese gráfico. El beneficio es que si tiene muchos punteros en muchos hilos, el crecimiento de parte del gráfico no es una operación altamente disputada.

 10
Author: MSalters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-14 21:47:08

Tienen un recuento de referencia interno que se incrementa en el constructor de copia/operador de asignación shared_ptr y decrementa en el destructor. Cuando el recuento llega a cero, el puntero retenido se elimina.

Aquí está la documentación de Boost library para punteros inteligentes. Creo que la implementación de TR1 es principalmente la misma que boost::shared_ptr.

 0
Author: Michael Kristofik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-10 13:35:11

"Puntero compartido es un puntero inteligente (un objeto C++ con operador sobrecargado*() y operador->()) que mantiene un puntero a un objeto y un puntero a un recuento de referencias compartidas. Cada vez que se realiza una copia del puntero inteligente utilizando el constructor de copia, el recuento de referencia se incrementa. Cuando se destruye un puntero compartido, el recuento de referencia de su objeto se reduce. Los punteros compartidos construidos a partir de punteros raw inicialmente tienen un recuento de referencia de 1. Cuando el recuento de referencia alcanza 0, el objeto puntiagudo es destruido, y la memoria que ocupa es liberada. No es necesario destruir objetos explícitamente: se hará automáticamente cuando se ejecute el destructor del último puntero. " Desde aquí .

 0
Author: clyfe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-10 13:35:25